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; 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)) 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.placeholder": "Placeholder",
"schemas.field.placeholderHint": "Define the placeholder for the input control.", "schemas.field.placeholderHint": "Define the placeholder for the input control.",
"schemas.field.required": "Required", "schemas.field.required": "Required",
"schemas.field.requiredOnPublish": "Required when publishing",
"schemas.field.show": "Show in API", "schemas.field.show": "Show in API",
"schemas.field.tabCommon": "Common", "schemas.field.tabCommon": "Common",
"schemas.field.tabEditing": "Editing", "schemas.field.tabEditing": "Editing",
@ -741,6 +742,7 @@
"schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.patternMessage": "Pattern Message",
"schemas.fieldTypes.string.suggestions": "Suggestions", "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.words": "Words",
"schemas.fieldTypes.string.wordsMax": "Max Words", "schemas.fieldTypes.string.wordsMax": "Max Words",
"schemas.fieldTypes.string.wordsMin": "Min Words", "schemas.fieldTypes.string.wordsMin": "Min Words",
@ -805,6 +807,7 @@
"schemas.updateRulesFailed": "Failed to update schema rules. Please reload.", "schemas.updateRulesFailed": "Failed to update schema rules. Please reload.",
"schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.",
"schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.",
"schemas.validateOnPublish": "Validate when publishing",
"search.addFilter": "Add Filter", "search.addFilter": "Add Filter",
"search.addGroup": "Add Group", "search.addGroup": "Add Group",
"search.addSorting": "Add Sorting", "search.addSorting": "Add Sorting",

3
backend/i18n/frontend_it.json

@ -686,6 +686,7 @@
"schemas.field.placeholder": "Segnaposto", "schemas.field.placeholder": "Segnaposto",
"schemas.field.placeholderHint": "Definisci il segnaposto per la verifica dell'input.", "schemas.field.placeholderHint": "Definisci il segnaposto per la verifica dell'input.",
"schemas.field.required": "Obbligatorio", "schemas.field.required": "Obbligatorio",
"schemas.field.requiredOnPublish": "Required when publishing",
"schemas.field.show": "Mostra nelle API", "schemas.field.show": "Mostra nelle API",
"schemas.field.tabCommon": "Comune", "schemas.field.tabCommon": "Comune",
"schemas.field.tabEditing": "Modifica", "schemas.field.tabEditing": "Modifica",
@ -741,6 +742,7 @@
"schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Messaggio del Pattern", "schemas.fieldTypes.string.patternMessage": "Messaggio del Pattern",
"schemas.fieldTypes.string.suggestions": "Suggerimenti", "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.words": "Parole",
"schemas.fieldTypes.string.wordsMax": "Numero max di Parole", "schemas.fieldTypes.string.wordsMax": "Numero max di Parole",
"schemas.fieldTypes.string.wordsMin": "Numero min 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.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.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.updateUIFieldsFailed": "Non è stato possibile aggiornare i campi della UI. Per favore ricarica.",
"schemas.validateOnPublish": "Validate when publishing",
"search.addFilter": "Aggiungi un Filtro", "search.addFilter": "Aggiungi un Filtro",
"search.addGroup": "Aggiungi un Gruppo", "search.addGroup": "Aggiungi un Gruppo",
"search.addSorting": "Aggiungi ordinamento", "search.addSorting": "Aggiungi ordinamento",

3
backend/i18n/frontend_nl.json

@ -686,6 +686,7 @@
"schemas.field.placeholder": "Placeholder", "schemas.field.placeholder": "Placeholder",
"schemas.field.placeholderHint": "Definieer de tijdelijke aanduiding voor het invoerbesturingselement.", "schemas.field.placeholderHint": "Definieer de tijdelijke aanduiding voor het invoerbesturingselement.",
"schemas.field.required": "Vereist", "schemas.field.required": "Vereist",
"schemas.field.requiredOnPublish": "Required when publishing",
"schemas.field.show": "Weergeven in API", "schemas.field.show": "Weergeven in API",
"schemas.field.tabCommon": "Algemeen", "schemas.field.tabCommon": "Algemeen",
"schemas.field.tabEditing": "Bewerken", "schemas.field.tabEditing": "Bewerken",
@ -741,6 +742,7 @@
"schemas.fieldTypes.string.pattern": "Regex-patroon", "schemas.fieldTypes.string.pattern": "Regex-patroon",
"schemas.fieldTypes.string.patternMessage": "Patroonbericht", "schemas.fieldTypes.string.patternMessage": "Patroonbericht",
"schemas.fieldTypes.string.suggestions": "Suggesties", "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.words": "Woorden",
"schemas.fieldTypes.string.wordsMax": "Max. Woorden", "schemas.fieldTypes.string.wordsMax": "Max. Woorden",
"schemas.fieldTypes.string.wordsMin": "Min. Woorden", "schemas.fieldTypes.string.wordsMin": "Min. Woorden",
@ -805,6 +807,7 @@
"schemas.updateRulesFailed": "Updaten van schemaregels is mislukt. Laad opnieuw.", "schemas.updateRulesFailed": "Updaten van schemaregels is mislukt. Laad opnieuw.",
"schemas.updateScriptsFailed": "Updaten van schemascripts is mislukt. Laad opnieuw.", "schemas.updateScriptsFailed": "Updaten van schemascripts is mislukt. Laad opnieuw.",
"schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.", "schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.",
"schemas.validateOnPublish": "Validate when publishing",
"search.addFilter": "Filter toevoegen", "search.addFilter": "Filter toevoegen",
"search.addGroup": "Groep toevoegen", "search.addGroup": "Groep toevoegen",
"search.addSorting": "Sortering toevoegen", "search.addSorting": "Sortering toevoegen",

1
backend/i18n/source/backend_en.json

@ -119,7 +119,6 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflowStep": "Step", "common.workflowStep": "Step",
"common.workflowTransition": "Transition", "common.workflowTransition": "Transition",
"contents.alreadyDeleted": "Content has already been deleted.",
"contents.bulkInsertQueryNotUnique": "More than one content matches to the query.", "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.draftNotCreateForUnpublished": "You can only create a new version when the content is published.",
"contents.draftToDeleteNotFound": "There is nothing to delete.", "contents.draftToDeleteNotFound": "There is nothing to delete.",

1
backend/i18n/source/backend_it.json

@ -118,7 +118,6 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflowStep": "Step", "common.workflowStep": "Step",
"common.workflowTransition": "Transizione", "common.workflowTransition": "Transizione",
"contents.alreadyDeleted": "Il contento è stato già eliminato.",
"contents.bulkInsertQueryNotUnique": "Ci sono più contenuti che corrispondono alla query.", "contents.bulkInsertQueryNotUnique": "Ci sono più contenuti che corrispondono alla query.",
"contents.draftNotCreateForUnpublished": "Puoi creare versioni del contenuto solo se questo è pubblicato.", "contents.draftNotCreateForUnpublished": "Puoi creare versioni del contenuto solo se questo è pubblicato.",
"contents.draftToDeleteNotFound": "Non c'è niente da eliminare.", "contents.draftToDeleteNotFound": "Non c'è niente da eliminare.",

1
backend/i18n/source/backend_nl.json

@ -119,7 +119,6 @@
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflowStep": "Stap", "common.workflowStep": "Stap",
"common.workflowTransition": "Overgang", "common.workflowTransition": "Overgang",
"contents.alreadyDeleted": "Content is al verwijderd.",
"contents.bulkInsertQueryNotUnique": "Meer dan één inhoud komt overeen met de zoekopdracht.", "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.draftNotCreateForUnpublished": "Je kunt alleen een nieuwe versie maken wanneer de inhoud is gepubliceerd.",
"contents.draftToDeleteNotFound": "Er is niets te verwijderen.", "contents.draftToDeleteNotFound": "Er is niets te verwijderen.",

3
backend/i18n/source/frontend_en.json

@ -686,6 +686,7 @@
"schemas.field.placeholder": "Placeholder", "schemas.field.placeholder": "Placeholder",
"schemas.field.placeholderHint": "Define the placeholder for the input control.", "schemas.field.placeholderHint": "Define the placeholder for the input control.",
"schemas.field.required": "Required", "schemas.field.required": "Required",
"schemas.field.requiredOnPublish": "Required when publishing",
"schemas.field.show": "Show in API", "schemas.field.show": "Show in API",
"schemas.field.tabCommon": "Common", "schemas.field.tabCommon": "Common",
"schemas.field.tabEditing": "Editing", "schemas.field.tabEditing": "Editing",
@ -741,6 +742,7 @@
"schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.patternMessage": "Pattern Message",
"schemas.fieldTypes.string.suggestions": "Suggestions", "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.words": "Words",
"schemas.fieldTypes.string.wordsMax": "Max Words", "schemas.fieldTypes.string.wordsMax": "Max Words",
"schemas.fieldTypes.string.wordsMin": "Min Words", "schemas.fieldTypes.string.wordsMin": "Min Words",
@ -805,6 +807,7 @@
"schemas.updateRulesFailed": "Failed to update schema rules. Please reload.", "schemas.updateRulesFailed": "Failed to update schema rules. Please reload.",
"schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.",
"schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.",
"schemas.validateOnPublish": "Validate when publishing",
"search.addFilter": "Add Filter", "search.addFilter": "Add Filter",
"search.addGroup": "Add Group", "search.addGroup": "Add Group",
"search.addSorting": "Add Sorting", "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 IsRequired { get; set; }
public bool IsRequiredOnPublish { get; set; }
public bool IsHalfWidth { get; set; } public bool IsHalfWidth { get; set; }
public string? Placeholder { 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 System.Collections.ObjectModel;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
@ -19,9 +18,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string? ContentSidebarUrl { get; set; } public string? ContentSidebarUrl { get; set; }
public bool DeepEquals(SchemaProperties properties) public bool ValidateOnPublish { get; set; }
{
return SimpleEquals.IsEquals(this, properties);
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
return @event; return @event;
} }
if (!source.Properties.DeepEquals(target.Properties)) if (!source.Properties.Equals(target.Properties))
{ {
yield return E(new SchemaUpdated { Properties = 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(context, nameof(context));
Guard.NotNull(factories, nameof(factories)); Guard.NotNull(factories, nameof(factories));
Guard.NotNull(partitionResolver, nameof(partitionResolver)); Guard.NotNull(partitionResolver, nameof(partitionResolver));
Guard.NotNull(log, nameof(log));
this.context = context; this.context = context;
this.factories = factories; this.factories = factories;
this.log = log;
this.partitionResolver = partitionResolver; this.partitionResolver = partitionResolver;
this.log = log;
} }
private void AddError(IEnumerable<string> path, string message) private void AddError(IEnumerable<string> path, string message)
@ -82,28 +84,28 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private IValidator CreateSchemaValidator(bool isPartial) 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) 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) private IValidator CreateFieldValidator(IRootField field, bool isPartial)
{ {
var partitioning = partitionResolver(field.Partitioning);
var fieldValidator = CreateFieldValidator(field); 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) foreach (var partitionKey in partitioning.AllKeys)
{ {
var optional = partitioning.IsOptional(partitionKey); var optional = partitioning.IsOptional(partitionKey);
fieldsValidators[partitionKey] = (optional, fieldValidator); partitioningValidators[partitionKey] = (optional, fieldValidator);
} }
var typeName = partitioning.ToString()!; var typeName = partitioning.ToString()!;
@ -111,7 +113,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return new AggregateValidator( return new AggregateValidator(
CreateFieldValidators(field) CreateFieldValidators(field)
.Union(Enumerable.Repeat( .Union(Enumerable.Repeat(
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, typeName), 1)), log); new ObjectValidator<IJsonValue>(partitioningValidators, isPartial, typeName), 1)), log);
} }
private IValidator CreateFieldValidator(IField field) 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>> internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumerable<IValidator>>
{ {
private readonly ValidatorContext context;
private readonly FieldValidatorFactory createFieldValidator; private readonly FieldValidatorFactory createFieldValidator;
private DefaultFieldValueValidatorsFactory(FieldValidatorFactory createFieldValidator) private DefaultFieldValueValidatorsFactory(ValidatorContext context, FieldValidatorFactory createFieldValidator)
{ {
this.context = context;
this.createFieldValidator = createFieldValidator; 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)); Guard.NotNull(field, nameof(field));
var visitor = new DefaultFieldValueValidatorsFactory(createFieldValidator); var visitor = new DefaultFieldValueValidatorsFactory(context, createFieldValidator);
return field.Accept(visitor); return field.Accept(visitor);
} }
@ -37,28 +40,32 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; 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) 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) public IEnumerable<IValidator> Visit(IField<AssetsFieldProperties> field)
{ {
var properties = field.Properties; 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) if (!properties.AllowDuplicates)
@ -71,7 +78,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; var properties = field.Properties;
if (properties.IsRequired) var isRequired = IsRequired(properties);
if (isRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -81,7 +90,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; var properties = field.Properties;
if (properties.IsRequired) var isRequired = IsRequired(properties);
if (isRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -96,7 +107,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; var properties = field.Properties;
if (properties.IsRequired) var isRequired = IsRequired(properties);
if (isRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -106,7 +119,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; var properties = field.Properties;
if (properties.IsRequired) var isRequired = IsRequired(properties);
if (isRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -116,7 +131,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; var properties = field.Properties;
if (properties.IsRequired) var isRequired = IsRequired(properties);
if (isRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -136,9 +153,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; 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) if (!properties.AllowDuplicates)
@ -151,7 +170,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; var properties = field.Properties;
if (properties.IsRequired) var isRequired = IsRequired(properties);
if (isRequired)
{ {
yield return new RequiredStringValidator(true); yield return new RequiredStringValidator(true);
} }
@ -200,9 +221,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
var properties = field.Properties; 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) if (properties.AllowedValues != null)
@ -220,5 +243,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return NoValueValidator.Instance; 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 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>) 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 public interface IValidatorsFactory
{ {
IEnumerable<IValidator> CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{ {
yield break; yield break;
} }
IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{ {
yield break; yield break;
} }
IEnumerable<IValidator> CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator) IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, FieldValidatorFactory createFieldValidator)
{ {
yield break; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
public sealed class ValidationContext public sealed class ValidationContext : ValidatorContext
{ {
public ImmutableQueue<string> Path { get; } public ImmutableQueue<string> Path { get; private set; } = ImmutableQueue<string>.Empty;
public NamedId<DomainId> AppId { get; }
public NamedId<DomainId> SchemaId { get; }
public Schema Schema { get; }
public DomainId ContentId { get; } public DomainId ContentId { get; }
public bool IsOptional { get; } public bool IsOptional { get; private set; }
public ValidationMode Mode { get; }
public ValidationContext( public ValidationContext(
NamedId<DomainId> appId, NamedId<DomainId> appId,
NamedId<DomainId> schemaId, NamedId<DomainId> schemaId,
Schema schema, Schema schema,
DomainId contentId, DomainId contentId)
ValidationMode mode = ValidationMode.Default) : base(appId, schemaId, schema)
: this(appId, schemaId, schema, contentId, ImmutableQueue<string>.Empty, false, mode)
{ {
ContentId = contentId;
} }
private ValidationContext( public ValidationContext Optimized(bool optimized = true)
NamedId<DomainId> appId,
NamedId<DomainId> schemaId,
Schema schema,
DomainId contentId,
ImmutableQueue<string> path,
bool isOptional,
ValidationMode mode = ValidationMode.Default)
{ {
AppId = appId; return WithMode(optimized ? ValidationMode.Optimized : ValidationMode.Default);
}
ContentId = contentId;
Mode = mode;
Schema = schema; public ValidationContext AsPublishing(bool publish = true)
SchemaId = schemaId; {
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 (Action == action)
if (Mode == mode)
{ {
return this; 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 this;
} }
return Clone(Path, fieldIsOptional, Mode); return Clone(clone => clone.Mode = mode);
} }
public ValidationContext Nested(string property) 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 namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class AggregateValidator : IValidator internal sealed class AggregateValidator : IValidator
{ {
private readonly IValidator[]? validators; private readonly IValidator[]? validators;
private readonly ISemanticLog log; 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) 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) if (value is ICollection<DomainId> assetIds && assetIds.Count > 0)
{ {
var assets = await checkAssets(assetIds); var assets = await checkAssets(assetIds);
@ -61,12 +56,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) 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) 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 && 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) public CollectionItemValidator(params IValidator[] itemValidators)
{ {
Guard.NotNull(itemValidators, nameof(itemValidators));
Guard.NotEmpty(itemValidators, nameof(itemValidators)); Guard.NotEmpty(itemValidators, nameof(itemValidators));
this.itemValidators = 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,14 +18,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class FieldValidator : IValidator public sealed class FieldValidator : IValidator
{ {
private readonly IValidator[]? validators; private readonly IValidator[] validators;
private readonly IField field; private readonly IField field;
public FieldValidator(IEnumerable<IValidator>? validators, IField field) public FieldValidator(IEnumerable<IValidator>? validators, IField field)
{ {
Guard.NotNull(field, nameof(field)); Guard.NotNull(field, nameof(field));
this.validators = validators?.ToArray(); this.validators = validators?.ToArray() ?? Array.Empty<IValidator>();
this.field = field; this.field = field;
} }
@ -62,7 +63,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
return; return;
} }
if (validators?.Length > 0) if (validators.Length > 0)
{ {
var tasks = new List<Task>(); 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 public sealed class ObjectValidator<TValue> : IValidator
{ {
private static readonly IReadOnlyDictionary<string, TValue> DefaultValue = new Dictionary<string, TValue>(); 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 bool isPartial;
private readonly string fieldType; 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.fieldType = fieldType;
this.isPartial = isPartial; this.isPartial = isPartial;
} }
@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
var name = fieldData.Key; var name = fieldData.Key;
if (!schema.ContainsKey(name)) if (!fields.ContainsKey(name))
{ {
addError(context.Path.Enqueue(name), T.Get("contents.validation.unknownField", new { fieldType })); 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>(); var tasks = new List<Task>();
foreach (var (fieldName, fieldConfig) in schema) foreach (var (fieldName, fieldConfig) in fields)
{ {
var (isOptional, validator) = fieldConfig; 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) public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{ {
if (context.Mode == ValidationMode.Optimized)
{
return;
}
if (value is ICollection<DomainId> contentIds) if (value is ICollection<DomainId> contentIds)
{ {
var foundIds = await checkReferences(contentIds.ToHashSet()); 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) public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{ {
if (context.Mode == ValidationMode.Optimized)
{
return;
}
var count = context.Path.Count(); var count = context.Path.Count();
if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) 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) 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); 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) public async Task<bool> DoAsync(DomainId appId, DomainId contentId)
{ {
var currentId = DomainId.Combine(appId, contentId);
var filter = var filter =
Filter.And( Filter.And(
Filter.AnyEq(x => x.ReferencedIds, appId), Filter.AnyEq(x => x.ReferencedIds, contentId),
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId),
Filter.Ne(x => x.IsDeleted, true), Filter.Ne(x => x.IsDeleted, true),
Filter.Ne(x => x.Id, currentId)); Filter.Ne(x => x.Id, contentId));
var hasReferrerAsync = var hasReferrerAsync =
await Collection.Find(filter).Only(x => x.Id) 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 Instant? DueTime { get; set; }
public DomainId? StatusJobId { 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.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards; 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.Entities.Contents.State;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
@ -22,28 +22,19 @@ using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public class ContentDomainObject : LogSnapshotDomainObject<ContentState> public class ContentDomainObject : LogSnapshotDomainObject<ContentState>
{ {
private readonly IContentWorkflow contentWorkflow;
private readonly IContentRepository contentRepository;
private readonly ContentOperationContext context; private readonly ContentOperationContext context;
public ContentDomainObject(IStore<DomainId> store, ISemanticLog log, public ContentDomainObject(IStore<DomainId> store, ISemanticLog log,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository,
ContentOperationContext context) ContentOperationContext context)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
this.context = context; this.context = context;
} }
@ -67,8 +58,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
public override Task<object?> ExecuteAsync(IAggregateCommand command) public override Task<object?> ExecuteAsync(IAggregateCommand command)
{ {
VerifyNotDeleted();
switch (command) switch (command)
{ {
case UpsertContent uspertContent: case UpsertContent uspertContent:
@ -92,13 +81,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
await LoadContext(c.AppId, c.SchemaId, c, c.OptimizeValidation); 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) if (!c.DoNotValidate)
{ {
await context.ValidateInputAsync(c.Data); await context.ValidateInputAsync(c.Data, createContent.Publish);
} }
if (!c.DoNotScript) if (!c.DoNotScript)
@ -144,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
GuardContent.CanCreateDraft(c, Snapshot); GuardContent.CanCreateDraft(c, Snapshot);
var status = await contentWorkflow.GetInitialStatusAsync(context.Schema); var status = await context.GetInitialStatusAsync();
CreateDraft(c, status); CreateDraft(c, status);
@ -166,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case UpdateContent updateContent: case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, async c => 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); return await UpdateAsync(c, x => c.Data, false);
}); });
@ -174,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
case PatchContent patchContent: case PatchContent patchContent:
return UpdateReturnAsync(patchContent, async c => 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); 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 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) if (c.DueTime.HasValue)
{ {
@ -196,7 +185,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var change = GetChange(c); var change = GetChange(c);
if (!c.DoNotScript) if (!c.DoNotScript && context.HasScript(c => c.Change))
{ {
var data = Snapshot.Data.Clone(); 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); ChangeStatus(c, change);
} }
} }
@ -240,7 +234,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c);
GuardContent.CanDelete(context.Schema, c); await GuardContent.CanDelete(context.Schema, Snapshot, context.Repository, c);
if (!c.DoNotScript) 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); Delete(c);
}); });
@ -290,7 +274,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
else else
{ {
await context.ValidateInputAsync(command.Data); await context.ValidateInputAsync(command.Data, false);
} }
} }
@ -387,14 +371,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
return change; 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) private Task LoadContext(NamedId<DomainId> appId, NamedId<DomainId> schemaId, ContentCommand command, bool optimized = false)
{ {
return context.LoadAsync(appId, schemaId, command, optimized); 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 NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; 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.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; 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)); Guard.NotNull(command, nameof(command));
@ -120,6 +121,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{ {
throw new DomainException(T.Get("contents.singletonNotDeletable")); 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) 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.DefaultValues; using Squidex.Domain.Apps.Core.DefaultValues;
using Squidex.Domain.Apps.Core.Schemas; 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.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -23,7 +25,7 @@ using Squidex.Infrastructure.Validation;
#pragma warning disable IDE0016 // Use 'throw' expression #pragma warning disable IDE0016 // Use 'throw' expression
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents.Operations
{ {
public sealed class ContentOperationContext public sealed class ContentOperationContext
{ {
@ -37,20 +39,37 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly ISemanticLog log; private readonly ISemanticLog log;
private readonly IAppProvider appProvider; 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 ISchemaEntity schema;
private IAppEntity app; private IAppEntity app;
private ContentCommand command; private ContentCommand command;
private ValidationContext validationContext; private ValidationContext validationContext;
public IContentWorkflow Workflow => contentWorkflow;
public IContentRepository Repository => contentRepository;
public ContentOperationContext( public ContentOperationContext(
IAppProvider appProvider, IAppProvider appProvider,
IEnumerable<IValidatorsFactory> factories, IEnumerable<IValidatorsFactory> validators,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository,
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
ISemanticLog log) 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.appProvider = appProvider;
this.factories = factories; this.validators = validators;
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.log = log; this.log = log;
@ -84,16 +103,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
validationContext = new ValidationContext(appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized); validationContext = new ValidationContext(appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized);
} }
public Task<Status> GetInitialStatusAsync()
{
return contentWorkflow.GetInitialStatusAsync(schema);
}
public Task GenerateDefaultValuesAsync(NamedContentData data) public Task GenerateDefaultValuesAsync(NamedContentData data)
{ {
data.GenerateDefaultValues(schema.SchemaDef, app.PartitionResolver()); data.GenerateDefaultValues(schema.SchemaDef, Partition());
return Task.CompletedTask; 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); await validator.ValidateInputAsync(data);
@ -102,7 +128,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ValidateInputPartialAsync(NamedContentData data) 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); await validator.ValidateInputPartialAsync(data);
@ -111,8 +139,27 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ValidateContentAsync(NamedContentData data) 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); await validator.ValidateContentAsync(data);
CheckErrors(validator); 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) public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptVars context)
{ {
Enrich(context); Enrich(context);
@ -154,6 +206,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
await scriptEngine.ExecuteAsync(context, GetScript(script), ScriptOptions); await scriptEngine.ExecuteAsync(context, GetScript(script), ScriptOptions);
} }
private PartitionResolver Partition()
{
return app.PartitionResolver();
}
private void Enrich(ScriptVars context) private void Enrich(ScriptVars context)
{ {
context.ContentId = command.ContentId; 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; 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) if (field is IField<AssetsFieldProperties> assetsField)
{ {
var checkAssets = new CheckAssets(async ids => 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) public Q WithQuery(ClrQuery? query)
{ {
return Clone(c => c.Query = query); return Clone(clone => clone.Query = query);
} }
public Q WithODataQuery(string? odataQuery) public Q WithODataQuery(string? odataQuery)
{ {
return Clone(c => c.ODataQuery = odataQuery); return Clone(clone => clone.ODataQuery = odataQuery);
} }
public Q WithJsonQuery(string? jsonQuery) public Q WithJsonQuery(string? jsonQuery)
{ {
return Clone(c => c.JsonQuery = jsonQuery); return Clone(clone => clone.JsonQuery = jsonQuery);
} }
public Q WithJsonQuery(Query<IJsonValue>? 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) 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) public Q WithReference(DomainId? reference)
{ {
return Clone(c => c.Reference = reference); return Clone(clone => clone.Reference = reference);
} }
public Q WithIds(IEnumerable<DomainId> ids) 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) public Q WithIds(string? ids)
{ {
if (!string.IsNullOrEmpty(ids)) if (!string.IsNullOrEmpty(ids))
{ {
return Clone(c => return Clone(clone =>
{ {
var idsList = new List<DomainId>(); var idsList = new List<DomainId>();
@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities
idsList.Add(DomainId.Create(id)); 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"> <data name="common.workflowTransition" xml:space="preserve">
<value>Transizione</value> <value>Transizione</value>
</data> </data>
<data name="contents.alreadyDeleted" xml:space="preserve">
<value>Il contento è stato già eliminato.</value>
</data>
<data name="contents.bulkInsertQueryNotUnique" xml:space="preserve"> <data name="contents.bulkInsertQueryNotUnique" xml:space="preserve">
<value>Ci sono più contenuti che corrispondono alla query.</value> <value>Ci sono più contenuti che corrispondono alla query.</value>
</data> </data>

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

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

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

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

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

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

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

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

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

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

1
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Contents; 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;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Contents.Text; 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( public static ValidationContext CreateContext(
Schema? schema = null, Schema? schema = null,
ValidationMode mode = ValidationMode.Default, ValidationMode mode = ValidationMode.Default,
ValidationUpdater? updater = null) ValidationUpdater? updater = null,
ValidationAction action = ValidationAction.Upsert)
{ {
var context = new ValidationContext( var context = new ValidationContext(
AppId, AppId,
SchemaId, SchemaId,
schema ?? new Schema(SchemaId.Name), schema ?? new Schema(SchemaId.Name),
DomainId.NewGuid(), DomainId.NewGuid());
mode);
context = context.WithMode(mode).WithAction(action);
if (updater != null) 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." }); 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] [Fact]
public async Task Should_add_error_if_document_is_too_small() 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}'." }); 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] [Fact]
public async Task Should_not_add_error_if_schemas_not_defined() 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.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands; 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.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
@ -108,9 +109,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
var validators = Enumerable.Repeat(new DefaultValidatorsFactory(), 1); 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); sut.Setup(Id);
} }
@ -588,7 +593,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id)) A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId))
.Returns(true); .Returns(true);
await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command));
@ -601,7 +606,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id)) A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId))
.Returns(true); .Returns(true);
await PublishAsync(command); 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.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards; 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.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
public class GuardContentTests : IClassFixture<TranslationsFixture> public class GuardContentTests : IClassFixture<TranslationsFixture>
{ {
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(); 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 NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly ClaimsPrincipal user = Mocks.FrontendUser(); private readonly ClaimsPrincipal user = Mocks.FrontendUser();
private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1));
@ -254,7 +256,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
{ {
CreateSchema(false); CreateSchema(false);
var content = new ContentState(); var content = CreateContent(Status.Published);
var command = new DeleteContentDraft(); var command = new DeleteContentDraft();
Assert.Throws<DomainException>(() => GuardContent.CanDeleteDraft(command, content)); Assert.Throws<DomainException>(() => GuardContent.CanDeleteDraft(command, content));
@ -270,23 +272,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
} }
[Fact] [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 schema = CreateSchema(true);
var content = CreateContent(Status.Published);
var command = new DeleteContent(); 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] [Fact]
public void CanDelete_should_not_throw_exception() public async Task CanDelete_should_not_throw_exception()
{ {
var schema = CreateSchema(false); var schema = CreateSchema(false);
var content = CreateContent(Status.Published);
var command = new DeleteContent(); var command = new DeleteContent();
GuardContent.CanDelete(schema, command); await GuardContent.CanDelete(schema, content, contentRepository, command);
} }
private void SetupCanUpdate(bool canUpdate) 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)); 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 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 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] [Fact]
public void Should_read_from_store() 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 _) => .Invokes((Func<DefaultXmlRepository.State, long, Task> callback, CancellationToken _) =>
{ {
callback(new DefaultXmlRepository.State callback(new DefaultXmlRepository.State
{ {
Xml = new XElement("a").ToString() Xml = new XElement("xml").ToString()
}, 0); }, 0);
callback(new DefaultXmlRepository.State callback(new DefaultXmlRepository.State
{ {
Xml = new XElement("b").ToString() Xml = new XElement("xml").ToString()
}, 0); }, 0);
}); });
@ -50,7 +50,7 @@ namespace Squidex.Domain.Users
[Fact] [Fact]
public void Should_write_to_store() public void Should_write_to_store()
{ {
var xml = new XElement("x"); var xml = new XElement("xml");
sut.StoreElement(xml, "name"); 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"> <input type="text" class="form-control" id="name" readonly [ngModel]="schema.name" [ngModelOptions]="standalone">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="label">{{ 'common.label' | sqxTranslate }}</label> <label for="label">{{ 'common.label' | sqxTranslate }}</label>
@ -59,6 +59,15 @@
<sqx-form-hint>{{ 'schemas.schemaTagsHint' | sqxTranslate }}</sqx-form-hint> <sqx-form-hint>{{ 'schemas.schemaTagsHint' | sqxTranslate }}</sqx-form-hint>
</div> </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>
<div class="card-footer"> <div class="card-footer">
<button type="submit" class="float-right btn btn-primary" *ngIf="isEditable"> <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>
</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> </div>
<ng-container [ngSwitch]="field.rawProperties.fieldType"> <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>
</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"> <div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.contentType' | sqxTranslate }}</label> <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}`, label: `label${id}${suffix}`,
contentsSidebarUrl: `url/to/contents/${id}${suffix}`, contentsSidebarUrl: `url/to/contents/${id}${suffix}`,
contentSidebarUrl: `url/to/content/${id}${suffix}`, contentSidebarUrl: `url/to/content/${id}${suffix}`,
validateOnPublish: id % 2 === 1,
tags: [ tags: [
`tags${id}${suffix}` `tags${id}${suffix}`
], ],
@ -823,6 +824,7 @@ function createSchemaProperties(id: number, suffix = '') {
`hints${id}${suffix}`, `hints${id}${suffix}`,
`url/to/contents/${id}${suffix}`, `url/to/contents/${id}${suffix}`,
`url/to/content/${id}${suffix}`, `url/to/content/${id}${suffix}`,
id % 2 === 0,
[ [
`tags${id}${suffix}` `tags${id}${suffix}`
] ]

3
frontend/app/shared/services/schemas.service.ts

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

1
frontend/app/shared/services/schemas.types.ts

@ -135,6 +135,7 @@ export abstract class FieldPropertiesDto {
public readonly editorUrl?: string; public readonly editorUrl?: string;
public readonly hints?: string; public readonly hints?: string;
public readonly isRequired: boolean = false; public readonly isRequired: boolean = false;
public readonly isRequiredOnPublish: boolean = false;
public readonly isHalfWidth: boolean = false; public readonly isHalfWidth: boolean = false;
public readonly label?: string; public readonly label?: string;
public readonly placeholder?: 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, editorUrl: null,
isRequired: false, isRequired: false,
isRequiredOnPublish: false,
isHalfWidth: false, isHalfWidth: false,
tags: [] tags: []
})); }));
@ -235,6 +236,7 @@ export class EditSchemaForm extends Form<FormGroup, UpdateSchemaDto, SchemaPrope
], ],
contentsSidebarUrl: '', contentsSidebarUrl: '',
contentSidebarUrl: '', contentSidebarUrl: '',
validateOnPublish: false,
tags: [] tags: []
})); }));
} }

2
frontend/app/theme/_forms.scss

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

Loading…
Cancel
Save