Browse Source

Better language config.

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
2fe0825849
  1. 19
      src/Squidex.Core/ContentEnricher.cs
  2. 12
      src/Squidex.Core/ContentExtensions.cs
  3. 107
      src/Squidex.Core/ContentValidator.cs
  4. 39
      src/Squidex.Core/Contents/ContentData.cs
  5. 2
      src/Squidex.Core/Contents/ContentFieldData.cs
  6. 25
      src/Squidex.Core/FieldExtensions.cs
  7. 44
      src/Squidex.Core/LanguageConfig.cs
  8. 184
      src/Squidex.Core/LanguagesConfig.cs
  9. 22
      src/Squidex.Core/Schemas/Field.cs
  10. 12
      src/Squidex.Core/Schemas/Schema.cs
  11. 2
      src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs
  12. 4
      src/Squidex.Core/Schemas/Validators/AssetsValidator.cs
  13. 2
      src/Squidex.Core/Schemas/Validators/IValidator.cs
  14. 4
      src/Squidex.Core/Schemas/Validators/PatternValidator.cs
  15. 2
      src/Squidex.Core/Schemas/Validators/RangeValidator.cs
  16. 4
      src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs
  17. 4
      src/Squidex.Core/Schemas/Validators/RequiredValidator.cs
  18. 4
      src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs
  19. 23
      src/Squidex.Events/Apps/AppLanguageUpdated.cs
  20. 9
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  21. 10
      src/Squidex.Infrastructure/Language.cs
  22. 42
      src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs
  23. 28
      src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs
  24. 14
      src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  25. 9
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs
  26. 6
      src/Squidex.Read/Apps/IAppEntity.cs
  27. 16
      src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs
  28. 6
      src/Squidex.Read/Contents/Repositories/IContentRepository.cs
  29. 5
      src/Squidex.Write/Apps/AppCommandHandler.cs
  30. 31
      src/Squidex.Write/Apps/AppDomainObject.cs
  31. 73
      src/Squidex.Write/Apps/AppLanguages.cs
  32. 30
      src/Squidex.Write/Apps/Commands/UpdateLanguage.cs
  33. 6
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  34. 32
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  35. 7
      src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs
  36. 16
      src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs
  37. 12
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  38. 15
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  39. 5
      tests/Squidex.Core.Tests/ContentEnrichmentTests.cs
  40. 59
      tests/Squidex.Core.Tests/ContentValidationTests.cs
  41. 55
      tests/Squidex.Core.Tests/Contents/ContentDataTests.cs
  42. 213
      tests/Squidex.Core.Tests/LanguagesConfigTests.cs
  43. 12
      tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs
  44. 8
      tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs
  45. 12
      tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs
  46. 10
      tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs
  47. 4
      tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs
  48. 12
      tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs
  49. 8
      tests/Squidex.Core.Tests/Schemas/SchemaTests.cs
  50. 14
      tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs
  51. 4
      tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs
  52. 6
      tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs
  53. 18
      tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs
  54. 8
      tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs
  55. 18
      tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs
  56. 16
      tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs
  57. 18
      tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs
  58. 16
      tests/Squidex.Infrastructure.Tests/LanguageTests.cs
  59. 10
      tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs
  60. 14
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  61. 67
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs
  62. 4
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

19
src/Squidex.Core/ContentEnricher.cs

@ -11,29 +11,27 @@ using Squidex.Core.Contents;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using System.Collections.Generic;
namespace Squidex.Core namespace Squidex.Core
{ {
public sealed class ContentEnricher public sealed class ContentEnricher
{ {
private readonly Schema schema; private readonly Schema schema;
private readonly HashSet<Language> languages; private readonly LanguagesConfig languagesConfig;
public ContentEnricher(HashSet<Language> languages, Schema schema) public ContentEnricher(LanguagesConfig languagesConfig, Schema schema)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languages, nameof(languages)); Guard.NotNull(languagesConfig, nameof(languagesConfig));
this.schema = schema; this.schema = schema;
this.languages = languages; this.languagesConfig = languagesConfig;
} }
public void Enrich(ContentData data) public void Enrich(ContentData data)
{ {
Guard.NotNull(data, nameof(data)); Guard.NotNull(data, nameof(data));
Guard.NotEmpty(languages, nameof(languages));
foreach (var field in schema.FieldsByName.Values) foreach (var field in schema.FieldsByName.Values)
{ {
@ -41,9 +39,9 @@ namespace Squidex.Core
if (field.RawProperties.IsLocalizable) if (field.RawProperties.IsLocalizable)
{ {
foreach (var language in languages) foreach (var languageConfig in languagesConfig)
{ {
Enrich(field, fieldData, language); Enrich(field, fieldData, languageConfig.Language);
} }
} }
else else
@ -61,7 +59,6 @@ namespace Squidex.Core
private static void Enrich(Field field, ContentFieldData fieldData, Language language) private static void Enrich(Field field, ContentFieldData fieldData, Language language)
{ {
Guard.NotNull(fieldData, nameof(fieldData)); Guard.NotNull(fieldData, nameof(fieldData));
Guard.NotNull(language, nameof(language));
var defaultValue = field.RawProperties.GetDefaultValue(); var defaultValue = field.RawProperties.GetDefaultValue();
@ -70,9 +67,9 @@ namespace Squidex.Core
return; return;
} }
if (!fieldData.TryGetValue(language.Iso2Code, out JToken value) || value == null || value.Type == JTokenType.Null) if (!fieldData.TryGetValue(language, out JToken value) || value == null || value.Type == JTokenType.Null)
{ {
fieldData.AddValue(language.Iso2Code, defaultValue); fieldData.AddValue(language, defaultValue);
} }
} }
} }

12
src/Squidex.Core/ContentExtensions.cs

@ -16,18 +16,18 @@ namespace Squidex.Core
{ {
public static class ContentExtensions public static class ContentExtensions
{ {
public static ContentData Enrich(this ContentData data, Schema schema, HashSet<Language> languages) public static ContentData Enrich(this ContentData data, Schema schema, LanguagesConfig languagesConfig)
{ {
var enricher = new ContentEnricher(languages, schema); var enricher = new ContentEnricher(languagesConfig, schema);
enricher.Enrich(data); enricher.Enrich(data);
return data; return data;
} }
public static async Task ValidateAsync(this ContentData data, Schema schema, HashSet<Language> languages, IList<ValidationError> errors) public static async Task ValidateAsync(this ContentData data, Schema schema, LanguagesConfig languagesConfig, IList<ValidationError> errors)
{ {
var validator = new ContentValidator(schema, languages); var validator = new ContentValidator(schema, languagesConfig);
await validator.ValidateAsync(data); await validator.ValidateAsync(data);
@ -37,9 +37,9 @@ namespace Squidex.Core
} }
} }
public static async Task ValidatePartialAsync(this ContentData data, Schema schema, HashSet<Language> languages, IList<ValidationError> errors) public static async Task ValidatePartialAsync(this ContentData data, Schema schema, LanguagesConfig languagesConfig, IList<ValidationError> errors)
{ {
var validator = new ContentValidator(schema, languages); var validator = new ContentValidator(schema, languagesConfig);
await validator.ValidatePartialAsync(data); await validator.ValidatePartialAsync(data);

107
src/Squidex.Core/ContentValidator.cs

@ -7,7 +7,6 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Core.Contents; using Squidex.Core.Contents;
@ -19,22 +18,22 @@ namespace Squidex.Core
public sealed class ContentValidator public sealed class ContentValidator
{ {
private readonly Schema schema; private readonly Schema schema;
private readonly HashSet<Language> languages; private readonly LanguagesConfig languagesConfig;
private readonly List<ValidationError> errors = new List<ValidationError>(); private readonly List<ValidationError> errors = new List<ValidationError>();
public ContentValidator(Schema schema, HashSet<Language> languages) public IReadOnlyList<ValidationError> Errors
{
get { return errors; }
}
public ContentValidator(Schema schema, LanguagesConfig languagesConfig)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languages, nameof(languages)); Guard.NotNull(languagesConfig, nameof(languagesConfig));
this.schema = schema; this.schema = schema;
this.languages = languages; this.languagesConfig = languagesConfig;
}
public IReadOnlyList<ValidationError> Errors
{
get { return errors; }
} }
public async Task ValidatePartialAsync(ContentData data) public async Task ValidatePartialAsync(ContentData data)
@ -47,51 +46,40 @@ namespace Squidex.Core
if (!schema.FieldsByName.TryGetValue(fieldData.Key, out Field field)) if (!schema.FieldsByName.TryGetValue(fieldData.Key, out Field field))
{ {
AddError("<FIELD> is not a known field", fieldName); errors.AddError("<FIELD> is not a known field", fieldName);
} }
else else
{ {
if (field.RawProperties.IsLocalizable) if (field.RawProperties.IsLocalizable)
{ {
await ValidateLocalizableFieldPartialAsync(field, fieldData.Value); await ValidateFieldPartialAsync(field, fieldData.Value, languagesConfig);
} }
else else
{ {
await ValidateNonLocalizableFieldPartialAsync(field, fieldData.Value); await ValidateFieldPartialAsync(field, fieldData.Value, LanguagesConfig.Invariant);
} }
} }
} }
} }
private async Task ValidateLocalizableFieldPartialAsync(Field field, ContentFieldData fieldData) private async Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData, LanguagesConfig languages)
{ {
foreach (var languageValue in fieldData) foreach (var languageValue in fieldData)
{ {
if (!Language.TryGetLanguage(languageValue.Key, out Language language)) if (!Language.TryGetLanguage(languageValue.Key, out var language))
{ {
AddError($"<FIELD> has an invalid language '{languageValue.Key}'", field); errors.AddError($"<FIELD> has an invalid language '{languageValue.Key}'", field);
} }
else if (!languages.Contains(language)) else if (!languages.TryGetConfig(language, out var languageConfig))
{ {
AddError($"<FIELD> has an unsupported language '{languageValue.Key}'", field); errors.AddError($"<FIELD> has an unsupported language '{languageValue.Key}'", field);
} }
else else
{ {
await ValidateAsync(field, languageValue.Value, language); var config = languageConfig;
}
}
}
private async Task ValidateNonLocalizableFieldPartialAsync(Field field, ContentFieldData fieldData) await field.ValidateAsync(languageValue.Value, config.IsOptional, m => errors.AddError(m, field, config.Language));
{
if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code))
{
AddError($"<FIELD> can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})", field);
} }
if (fieldData.TryGetValue(Language.Invariant.Iso2Code, out JToken value))
{
await ValidateAsync(field, value);
} }
} }
@ -107,11 +95,11 @@ namespace Squidex.Core
if (field.RawProperties.IsLocalizable) if (field.RawProperties.IsLocalizable)
{ {
await ValidateLocalizableFieldAsync(field, fieldData); await ValidateFieldAsync(field, fieldData, languagesConfig);
} }
else else
{ {
await ValidateNonLocalizableField(field, fieldData); await ValidateFieldAsync(field, fieldData, LanguagesConfig.Invariant);
} }
} }
} }
@ -122,69 +110,32 @@ namespace Squidex.Core
{ {
if (!schema.FieldsByName.ContainsKey(fieldData.Key)) if (!schema.FieldsByName.ContainsKey(fieldData.Key))
{ {
AddError("<FIELD> is not a known field", fieldData.Key); errors.AddError("<FIELD> is not a known field", fieldData.Key);
} }
} }
} }
private async Task ValidateLocalizableFieldAsync(Field field, ContentFieldData fieldData) private async Task ValidateFieldAsync(Field field, ContentFieldData fieldData, LanguagesConfig languages)
{ {
foreach (var valueLanguage in fieldData.Keys) foreach (var valueLanguage in fieldData.Keys)
{ {
if (!Language.TryGetLanguage(valueLanguage, out Language language)) if (!Language.TryGetLanguage(valueLanguage, out Language language))
{ {
AddError($"<FIELD> has an invalid language '{valueLanguage}'", field); errors.AddError($"<FIELD> has an invalid language '{valueLanguage}'", field);
} }
else if (!languages.Contains(language)) else if (!languages.Contains(language))
{ {
AddError($"<FIELD> has an unsupported language '{valueLanguage}'", field); errors.AddError($"<FIELD> has an unsupported language '{valueLanguage}'", field);
}
}
foreach (var language in languages)
{
var value = fieldData.GetOrCreate(language.Iso2Code, k => JValue.CreateNull());
await ValidateAsync(field, value, language);
}
}
private async Task ValidateNonLocalizableField(Field field, ContentFieldData fieldData)
{
if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code))
{
AddError($"<FIELD> can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})", field);
}
var value = fieldData.GetOrCreate(Language.Invariant.Iso2Code, k => JValue.CreateNull());
await ValidateAsync(field, value);
} }
private Task ValidateAsync(Field field, JToken value, Language language = null)
{
return field.ValidateAsync(value, m => AddError(m, field, language));
} }
private void AddError(string message, Field field, Language language = null) foreach (var languageConfig in languages)
{
var displayName = !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name;
if (language != null)
{ {
displayName += $" ({language.Iso2Code})"; var config = languageConfig;
} var value = fieldData.GetOrCreate(config.Language, k => JValue.CreateNull());
message = message.Replace("<FIELD>", displayName);
errors.Add(new ValidationError(message, field.Name)); await field.ValidateAsync(value, config.IsOptional, m => errors.AddError(m, field, config.Language));
} }
private void AddError(string message, string fieldName)
{
message = message.Replace("<FIELD>", fieldName);
errors.Add(new ValidationError(message, fieldName));
} }
} }
} }

39
src/Squidex.Core/Contents/ContentData.cs

@ -122,13 +122,13 @@ namespace Squidex.Core.Contents
return result; return result;
} }
public ContentData ToApiModel(Schema schema, IReadOnlyCollection<Language> languages, Language masterLanguage, bool excludeHidden = true) public ContentData ToApiModel(Schema schema, LanguagesConfig languagesConfig, IReadOnlyCollection<Language> languagePreferences = null, bool excludeHidden = true)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languages, nameof(languages)); Guard.NotNull(languagesConfig, nameof(languagesConfig));
Guard.NotNull(masterLanguage, nameof(masterLanguage));
var invariantCode = Language.Invariant.Iso2Code; var codeForInvariant = Language.Invariant.Iso2Code;
var codeForMasterLanguage = languagesConfig.Master.Language.Iso2Code;
var result = new ContentData(); var result = new ContentData();
@ -144,15 +144,15 @@ namespace Squidex.Core.Contents
if (field.RawProperties.IsLocalizable) if (field.RawProperties.IsLocalizable)
{ {
foreach (var language in languages) foreach (var languageConfig in languagesConfig)
{ {
var languageCode = language.Iso2Code; string languageCode = languageConfig.Language;
if (fieldValues.TryGetValue(languageCode, out JToken value)) if (fieldValues.TryGetValue(languageCode, out JToken value))
{ {
fieldResult.Add(languageCode, value); fieldResult.Add(languageCode, value);
} }
else if (language.Equals(masterLanguage) && fieldValues.TryGetValue(invariantCode, out value)) else if (languageConfig == languagesConfig.Master && fieldValues.TryGetValue(codeForInvariant, out value))
{ {
fieldResult.Add(languageCode, value); fieldResult.Add(languageCode, value);
} }
@ -160,17 +160,17 @@ namespace Squidex.Core.Contents
} }
else else
{ {
if (fieldValues.TryGetValue(invariantCode, out JToken value)) if (fieldValues.TryGetValue(codeForInvariant, out JToken value))
{ {
fieldResult.Add(invariantCode, value); fieldResult.Add(codeForInvariant, value);
} }
else if (fieldValues.TryGetValue(masterLanguage.Iso2Code, out value)) else if (fieldValues.TryGetValue(codeForMasterLanguage, out value))
{ {
fieldResult.Add(invariantCode, value); fieldResult.Add(codeForInvariant, value);
} }
else if (fieldValues.Count > 0) else if (fieldValues.Count > 0)
{ {
fieldResult.Add(invariantCode, fieldValues.Values.First()); fieldResult.Add(codeForInvariant, fieldValues.Values.First());
} }
} }
@ -180,13 +180,20 @@ namespace Squidex.Core.Contents
return result; return result;
} }
public object ToLanguageModel(IReadOnlyCollection<Language> languagePreferences = null) public object ToLanguageModel(LanguagesConfig languagesConfig, IReadOnlyCollection<Language> languagePreferences = null)
{ {
if (languagePreferences == null) Guard.NotNull(languagesConfig, nameof(languagesConfig));
if (languagePreferences == null || languagePreferences.Count == 0)
{ {
return this; return this;
} }
if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig))
{
languagePreferences = languagePreferences.Union(languageConfig.Fallback).ToList();
}
var result = new Dictionary<string, JToken>(); var result = new Dictionary<string, JToken>();
foreach (var fieldValue in this) foreach (var fieldValue in this)
@ -195,9 +202,7 @@ namespace Squidex.Core.Contents
foreach (var language in languagePreferences) foreach (var language in languagePreferences)
{ {
var languageCode = language.Iso2Code; if (fieldValues.TryGetValue(language, out JToken value) && value != null)
if (fieldValues.TryGetValue(languageCode, out JToken value) && value != null)
{ {
result[fieldValue.Key] = value; result[fieldValue.Key] = value;

2
src/Squidex.Core/Contents/ContentFieldData.cs

@ -22,7 +22,7 @@ namespace Squidex.Core.Contents
public ContentFieldData SetValue(JToken value) public ContentFieldData SetValue(JToken value)
{ {
this[Language.Invariant.Iso2Code] = value; this[Language.Invariant] = value;
return this; return this;
} }

25
src/Squidex.Core/FieldExtensions.cs

@ -7,6 +7,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
@ -17,7 +18,27 @@ namespace Squidex.Core
{ {
public static class FieldExtensions public static class FieldExtensions
{ {
public static async Task ValidateAsync(this Field field, JToken value, Action<string> addError) public static void AddError(this ICollection<ValidationError> errors, string message, Field field, Language language = null)
{
AddError(errors, message, !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name, field.Name, language);
}
public static void AddError(this ICollection<ValidationError> errors, string message, string fieldName, Language language = null)
{
AddError(errors, message, fieldName, fieldName, language);
}
public static void AddError(this ICollection<ValidationError> errors, string message, string displayName, string fieldName, Language language = null)
{
if (language != null && language != Language.Invariant)
{
displayName += $" ({language.Iso2Code})";
}
errors.Add(new ValidationError(message.Replace("<FIELD>", displayName), fieldName));
}
public static async Task ValidateAsync(this Field field, JToken value, bool isOptional, Action<string> addError)
{ {
Guard.NotNull(value, nameof(value)); Guard.NotNull(value, nameof(value));
@ -27,7 +48,7 @@ namespace Squidex.Core
foreach (var validator in field.Validators) foreach (var validator in field.Validators)
{ {
await validator.ValidateAsync(typedValue, addError); await validator.ValidateAsync(typedValue, isOptional, addError);
} }
} }
catch catch

44
src/Squidex.Core/LanguageConfig.cs

@ -0,0 +1,44 @@
// ==========================================================================
// LanguageConfig.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
using System.Collections.Immutable;
namespace Squidex.Core
{
public sealed class LanguageConfig
{
public bool IsOptional { get; }
public Language Language { get; }
public ImmutableList<Language> Fallback { get; }
public LanguageConfig(Language language, bool isOptional, params Language[] fallback)
: this(language, isOptional, fallback?.ToImmutableList())
{
}
public LanguageConfig(Language language, bool isOptional, IEnumerable<Language> fallback)
: this(language, isOptional, fallback?.ToImmutableList())
{
}
public LanguageConfig(Language language, bool isOptional = false, ImmutableList<Language> fallback = null)
{
Guard.NotNull(language, nameof(language));
Language = language;
IsOptional = isOptional;
Fallback = fallback ?? ImmutableList<Language>.Empty;
}
}
}

184
src/Squidex.Core/LanguagesConfig.cs

@ -0,0 +1,184 @@
// ==========================================================================
// LanguagesConfig.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
namespace Squidex.Core
{
public sealed class LanguagesConfig : IEnumerable<LanguageConfig>
{
private readonly ImmutableDictionary<Language, LanguageConfig> languages;
private readonly LanguageConfig master;
public static readonly LanguagesConfig Empty = Create();
public static readonly LanguagesConfig Invariant = Create(Language.Invariant);
public LanguageConfig Master
{
get { return master; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return languages.Values.GetEnumerator();
}
IEnumerator<LanguageConfig> IEnumerable<LanguageConfig>.GetEnumerator()
{
return languages.Values.GetEnumerator();
}
private LanguagesConfig(ImmutableDictionary<Language, LanguageConfig> languages, LanguageConfig master)
{
this.languages = ValidateLanguages(languages);
this.master = master;
}
public static LanguagesConfig Create(ICollection<LanguageConfig> languageConfigs)
{
Guard.NotNull(languageConfigs, nameof(languageConfigs));
var validated = ValidateLanguages(languageConfigs.ToImmutableDictionary(c => c.Language));
return new LanguagesConfig(validated, languageConfigs.FirstOrDefault());
}
public static LanguagesConfig Create(params Language[] languages)
{
Guard.NotNull(languages, nameof(languages));
var languageConfigs = languages.Select(l => new LanguageConfig(l)).ToList();
return Create(languageConfigs);
}
public LanguagesConfig MakeMaster(Language language)
{
ThrowIfNotFound(language);
return new LanguagesConfig(languages, languages[language]);
}
public LanguagesConfig Add(Language language)
{
ThrowIfFound(language, () => $"Cannot add language '{language.Iso2Code}'.");
var newLanguages = languages.Add(language, new LanguageConfig(language));
return new LanguagesConfig(newLanguages, master ?? newLanguages.Values.First());
}
public LanguagesConfig Update(Language language, bool isOptional, IEnumerable<Language> fallback)
{
ThrowIfNotFound(language);
if (isOptional)
{
ThrowIfMaster(language, () => $"Cannot cannot make language '{language.Iso2Code}' optional");
}
var newLanguages = ValidateLanguages(languages.SetItem(language, new LanguageConfig(language, isOptional, fallback)));
return new LanguagesConfig(newLanguages, master);
}
public LanguagesConfig Remove(Language language)
{
ThrowIfNotFound(language);
ThrowIfMaster(language, () => $"Cannot remove language '{language.Iso2Code}'");
var newLanguages = languages.Remove(language);
foreach (var languageConfig in newLanguages.Values)
{
if (languageConfig.Fallback.Contains(language))
{
newLanguages =
newLanguages.SetItem(languageConfig.Language,
new LanguageConfig(
languageConfig.Language,
languageConfig.IsOptional,
languageConfig.Fallback.Remove(language)));
}
}
return new LanguagesConfig(newLanguages, master);
}
public bool TryGetConfig(Language language, out LanguageConfig value)
{
return languages.TryGetValue(language, out value);
}
public bool Contains(Language language)
{
return language != null && languages.ContainsKey(language);
}
private static ImmutableDictionary<Language, LanguageConfig> ValidateLanguages(ImmutableDictionary<Language, LanguageConfig> languages)
{
var errors = new List<ValidationError>();
foreach (var languageConfig in languages.Values)
{
foreach (var fallback in languageConfig.Fallback)
{
if (!languages.ContainsKey(fallback))
{
var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'";
errors.Add(new ValidationError(message));
}
}
}
if (errors.Count > 0)
{
throw new ValidationException("Cannot configure language", errors);
}
return languages;
}
private void ThrowIfNotFound(Language language)
{
if (!Contains(language))
{
throw new DomainObjectNotFoundException(language, "Languages", typeof(LanguagesConfig));
}
}
private void ThrowIfFound(Language language, Func<string> message)
{
if (Contains(language))
{
var error = new ValidationError("Language is already part of the app", "Language");
throw new ValidationException(message(), error);
}
}
private void ThrowIfMaster(Language language, Func<string> message)
{
if (master?.Language == language)
{
var error = new ValidationError("Language is the master language", "Language");
throw new ValidationException(message(), error);
}
}
}
}

22
src/Squidex.Core/Schemas/Field.cs

@ -104,15 +104,15 @@ namespace Squidex.Core.Schemas
return Clone<Field>(clone => clone.name = newName); return Clone<Field>(clone => clone.name = newName);
} }
public void AddToEdmType(EdmStructuredType edmType, IEnumerable<Language> languages, string schemaName, Func<EdmComplexType, EdmComplexType> typeResolver) public void AddToEdmType(EdmStructuredType edmType, LanguagesConfig languagesConfig, string schemaName, Func<EdmComplexType, EdmComplexType> typeResolver)
{ {
Guard.NotNull(edmType, nameof(edmType)); Guard.NotNull(edmType, nameof(edmType));
Guard.NotNull(languages, nameof(languages));
Guard.NotNull(typeResolver, nameof(typeResolver)); Guard.NotNull(typeResolver, nameof(typeResolver));
Guard.NotNull(languagesConfig, nameof(languagesConfig));
if (!RawProperties.IsLocalizable) if (!RawProperties.IsLocalizable)
{ {
languages = new[] { Language.Invariant }; languagesConfig = LanguagesConfig.Invariant;
} }
var edmValueType = CreateEdmType(); var edmValueType = CreateEdmType();
@ -124,35 +124,35 @@ namespace Squidex.Core.Schemas
var languageType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{Name.ToPascalCase()}Property")); var languageType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{Name.ToPascalCase()}Property"));
foreach (var language in languages) foreach (var languageConfig in languagesConfig)
{ {
languageType.AddStructuralProperty(language.Iso2Code, edmValueType); languageType.AddStructuralProperty(languageConfig.Language, edmValueType);
} }
edmType.AddStructuralProperty(Name, new EdmComplexTypeReference(languageType, false)); edmType.AddStructuralProperty(Name, new EdmComplexTypeReference(languageType, false));
} }
public void AddToJsonSchema(JsonSchema4 schema, IEnumerable<Language> languages, string schemaName, Func<string, JsonSchema4, JsonSchema4> schemaResolver) public void AddToJsonSchema(JsonSchema4 schema, LanguagesConfig languagesConfig, string schemaName, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languages, nameof(languages));
Guard.NotNull(schemaResolver, nameof(schemaResolver)); Guard.NotNull(schemaResolver, nameof(schemaResolver));
Guard.NotNull(languagesConfig, nameof(languagesConfig));
if (!RawProperties.IsLocalizable) if (!RawProperties.IsLocalizable)
{ {
languages = new[] { Language.Invariant }; languagesConfig = LanguagesConfig.Invariant;
} }
var languagesProperty = CreateProperty(); var languagesProperty = CreateProperty();
var languagesObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false }; var languagesObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false };
foreach (var language in languages) foreach (var languageConfig in languagesConfig)
{ {
var languageProperty = new JsonProperty { Description = language.EnglishName, IsRequired = RawProperties.IsRequired }; var languageProperty = new JsonProperty { Description = languageConfig.Language.EnglishName, IsRequired = RawProperties.IsRequired };
PrepareJsonSchema(languageProperty, schemaResolver); PrepareJsonSchema(languageProperty, schemaResolver);
languagesObject.Properties.Add(language.Iso2Code, languageProperty); languagesObject.Properties.Add(languageConfig.Language, languageProperty);
} }
languagesProperty.SchemaReference = schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject); languagesProperty.SchemaReference = schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject);

12
src/Squidex.Core/Schemas/Schema.cs

@ -202,10 +202,10 @@ namespace Squidex.Core.Schemas
return new Schema(name, isPublished, properties, newFields); return new Schema(name, isPublished, properties, newFields);
} }
public EdmComplexType BuildEdmType(HashSet<Language> languages, Func<EdmComplexType, EdmComplexType> typeResolver) public EdmComplexType BuildEdmType(LanguagesConfig languagesConfig, Func<EdmComplexType, EdmComplexType> typeResolver)
{ {
Guard.NotEmpty(languages, nameof(languages));
Guard.NotNull(typeResolver, nameof(typeResolver)); Guard.NotNull(typeResolver, nameof(typeResolver));
Guard.NotNull(languagesConfig, nameof(languagesConfig));
var schemaName = Name.ToPascalCase(); var schemaName = Name.ToPascalCase();
@ -213,16 +213,16 @@ namespace Squidex.Core.Schemas
foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden)) foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden))
{ {
field.AddToEdmType(edmType, languages, schemaName, typeResolver); field.AddToEdmType(edmType, languagesConfig, schemaName, typeResolver);
} }
return edmType; return edmType;
} }
public JsonSchema4 BuildJsonSchema(HashSet<Language> languages, Func<string, JsonSchema4, JsonSchema4> schemaResolver) public JsonSchema4 BuildJsonSchema(LanguagesConfig languagesConfig, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{ {
Guard.NotEmpty(languages, nameof(languages));
Guard.NotNull(schemaResolver, nameof(schemaResolver)); Guard.NotNull(schemaResolver, nameof(schemaResolver));
Guard.NotNull(languagesConfig, nameof(languagesConfig));
var schemaName = Name.ToPascalCase(); var schemaName = Name.ToPascalCase();
@ -230,7 +230,7 @@ namespace Squidex.Core.Schemas
foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden)) foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden))
{ {
field.AddToJsonSchema(schema, languages, schemaName, schemaResolver); field.AddToJsonSchema(schema, languagesConfig, schemaName, schemaResolver);
} }
return schema; return schema;

2
src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs

@ -25,7 +25,7 @@ namespace Squidex.Core.Schemas.Validators
this.allowedValues = allowedValues; this.allowedValues = allowedValues;
} }
public Task ValidateAsync(object value, Action<string> addError) public Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
if (value == null) if (value == null)
{ {

4
src/Squidex.Core/Schemas/Validators/AssetsValidator.cs

@ -23,13 +23,13 @@ namespace Squidex.Core.Schemas.Validators
this.isRequired = isRequired; this.isRequired = isRequired;
} }
public async Task ValidateAsync(object value, Action<string> addError) public async Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
var assets = value as AssetsValue; var assets = value as AssetsValue;
if (assets == null || assets.AssetIds.Count == 0) if (assets == null || assets.AssetIds.Count == 0)
{ {
if (isRequired) if (isRequired && !isOptional)
{ {
addError("<FIELD> is required"); addError("<FIELD> is required");
} }

2
src/Squidex.Core/Schemas/Validators/IValidator.cs

@ -13,6 +13,6 @@ namespace Squidex.Core.Schemas.Validators
{ {
public interface IValidator public interface IValidator
{ {
Task ValidateAsync(object value, Action<string> addError); Task ValidateAsync(object value, bool isOptional, Action<string> addError);
} }
} }

4
src/Squidex.Core/Schemas/Validators/PatternValidator.cs

@ -28,11 +28,11 @@ namespace Squidex.Core.Schemas.Validators
regex = new Regex("^" + pattern + "$"); regex = new Regex("^" + pattern + "$");
} }
public Task ValidateAsync(object value, Action<string> addError) public Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
if (value is string stringValue) if (value is string stringValue)
{ {
if (!regex.IsMatch(stringValue)) if (!string.IsNullOrEmpty(stringValue) && !regex.IsMatch(stringValue))
{ {
if (string.IsNullOrWhiteSpace(errorMessage)) if (string.IsNullOrWhiteSpace(errorMessage))
{ {

2
src/Squidex.Core/Schemas/Validators/RangeValidator.cs

@ -28,7 +28,7 @@ namespace Squidex.Core.Schemas.Validators
this.max = max; this.max = max;
} }
public Task ValidateAsync(object value, Action<string> addError) public Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
if (value == null) if (value == null)
{ {

4
src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs

@ -21,9 +21,9 @@ namespace Squidex.Core.Schemas.Validators
this.validateEmptyStrings = validateEmptyStrings; this.validateEmptyStrings = validateEmptyStrings;
} }
public Task ValidateAsync(object value, Action<string> addError) public Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
if (value != null && !(value is string)) if (isOptional || (value != null && !(value is string)))
{ {
return TaskHelper.Done; return TaskHelper.Done;
} }

4
src/Squidex.Core/Schemas/Validators/RequiredValidator.cs

@ -14,9 +14,9 @@ namespace Squidex.Core.Schemas.Validators
{ {
public class RequiredValidator : IValidator public class RequiredValidator : IValidator
{ {
public Task ValidateAsync(object value, Action<string> addError) public Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
if (value == null) if (value == null && !isOptional)
{ {
addError("<FIELD> is required"); addError("<FIELD> is required");
} }

4
src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs

@ -30,9 +30,9 @@ namespace Squidex.Core.Schemas.Validators
this.maxLength = maxLength; this.maxLength = maxLength;
} }
public Task ValidateAsync(object value, Action<string> addError) public Task ValidateAsync(object value, bool isOptional, Action<string> addError)
{ {
if (value is string stringValue) if (value is string stringValue && !string.IsNullOrEmpty(stringValue))
{ {
if (minLength.HasValue && stringValue.Length < minLength.Value) if (minLength.HasValue && stringValue.Length < minLength.Value)
{ {

23
src/Squidex.Events/Apps/AppLanguageUpdated.cs

@ -0,0 +1,23 @@
// ==========================================================================
// AppLanguageUpdated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Events.Apps
{
[TypeName("AppLanguageUpdated")]
public sealed class AppLanguageUpdated : AppEvent
{
public Language Language { get; set; }
public bool IsOptional { get; set; }
public List<Language> Fallback { get; set; }
}
}

9
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -128,6 +128,11 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
var @event = ParseEvent(storedEvent); var @event = ParseEvent(storedEvent);
if (@event == null)
{
return;
}
await DispatchConsumer(@event, eventConsumer); await DispatchConsumer(@event, eventConsumer);
await eventConsumerInfoRepository.SetLastHandledEventNumberAsync(consumerName, storedEvent.EventNumber); await eventConsumerInfoRepository.SetLastHandledEventNumberAsync(consumerName, storedEvent.EventNumber);
} }
@ -213,6 +218,10 @@ namespace Squidex.Infrastructure.CQRS.Events
return @event; return @event;
} }
catch (ArgumentException)
{
return null;
}
catch (Exception ex) catch (Exception ex)
{ {
log.LogFatal(ex, w => w log.LogFatal(ex, w => w

10
src/Squidex.Infrastructure/Language.cs

@ -82,6 +82,16 @@ namespace Squidex.Infrastructure
return AllLanguagesField.TryGetValue(iso2Code, out language); return AllLanguagesField.TryGetValue(iso2Code, out language);
} }
public static implicit operator string(Language language)
{
return language?.Iso2Code;
}
public static implicit operator Language(string iso2Code)
{
return GetLanguage(iso2Code);
}
public static Language ParseOrNull(string input) public static Language ParseOrNull(string input)
{ {
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))

42
src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs

@ -6,34 +6,39 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Squidex.Core;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Apps; using Squidex.Read.Apps;
// ReSharper disable InvertIf
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
namespace Squidex.Read.MongoDb.Apps namespace Squidex.Read.MongoDb.Apps
{ {
public sealed class MongoAppEntity : MongoEntity, IAppEntity public sealed class MongoAppEntity : MongoEntity, IAppEntity
{ {
private LanguagesConfig languagesConfig = LanguagesConfig.Empty;
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public string Name { get; set; } public string Name { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public string MasterLanguage { get; set; } public long Version { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public long Version { get; set; } public string MasterLanguage { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public HashSet<string> Languages { get; set; } = new HashSet<string>(); public List<MongoAppLanguage> Languages { get; set; } = new List<MongoAppLanguage>();
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
@ -43,6 +48,11 @@ namespace Squidex.Read.MongoDb.Apps
[BsonElement] [BsonElement]
public Dictionary<string, MongoAppContributorEntity> Contributors { get; set; } = new Dictionary<string, MongoAppContributorEntity>(); public Dictionary<string, MongoAppContributorEntity> Contributors { get; set; } = new Dictionary<string, MongoAppContributorEntity>();
public LanguagesConfig LanguagesConfig
{
get { return languagesConfig ?? (languagesConfig = CreateLanguagesConfig()); }
}
IReadOnlyCollection<IAppClientEntity> IAppEntity.Clients IReadOnlyCollection<IAppClientEntity> IAppEntity.Clients
{ {
get { return Clients.Values; } get { return Clients.Values; }
@ -53,14 +63,32 @@ namespace Squidex.Read.MongoDb.Apps
get { return Contributors.Values; } get { return Contributors.Values; }
} }
IReadOnlyCollection<Language> IAppEntity.Languages public void UpdateLanguages(Func<LanguagesConfig, LanguagesConfig> updater)
{
var newConfig = updater(LanguagesConfig);
if (languagesConfig != newConfig)
{
languagesConfig = newConfig;
Languages = newConfig.Select(FromLanguageConfig).ToList();
MasterLanguage = newConfig.Master.Language;
}
}
private LanguagesConfig CreateLanguagesConfig()
{
return LanguagesConfig.Create(Languages.Select(ToLanguageConfig).ToList()).MakeMaster(MasterLanguage);
}
private static MongoAppLanguage FromLanguageConfig(LanguageConfig l)
{ {
get { return Languages.Select(Language.GetLanguage).ToList(); } return new MongoAppLanguage { Iso2Code = l.Language, IsOptional = l.IsOptional, Fallback = l.Fallback.Select(x => x.Iso2Code).ToList() };
} }
Language IAppEntity.MasterLanguage private static LanguageConfig ToLanguageConfig(MongoAppLanguage l)
{ {
get { return Language.GetLanguage(MasterLanguage); } return new LanguageConfig(l.Iso2Code, l.IsOptional, l.Fallback?.Select<string, Language>(f => f));
} }
} }
} }

28
src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs

@ -0,0 +1,28 @@
// ==========================================================================
// MongoAppLanguage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using MongoDB.Bson.Serialization.Attributes;
namespace Squidex.Read.MongoDb.Apps
{
public sealed class MongoAppLanguage
{
[BsonRequired]
[BsonElement]
public string Iso2Code { get; set; }
[BsonRequired]
[BsonElement]
public bool IsOptional { get; set; }
[BsonRequired]
[BsonElement]
public List<string> Fallback { get; set; }
}
}

14
src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs

@ -77,7 +77,7 @@ namespace Squidex.Read.MongoDb.Apps
{ {
return Collection.UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Languages.Add(@event.Language.Iso2Code); a.UpdateLanguages(c => c.Add(@event.Language));
}); });
} }
@ -85,7 +85,15 @@ namespace Squidex.Read.MongoDb.Apps
{ {
return Collection.UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Languages.Remove(@event.Language.Iso2Code); a.UpdateLanguages(c => c.Remove(@event.Language));
});
}
protected Task On(AppLanguageUpdated @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(@event, headers, a =>
{
a.UpdateLanguages(c => c.Update(@event.Language, @event.IsOptional, @event.Fallback));
}); });
} }
@ -93,7 +101,7 @@ namespace Squidex.Read.MongoDb.Apps
{ {
return Collection.UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.MasterLanguage = @event.Language.Iso2Code; a.UpdateLanguages(c => c.MakeMaster(@event.Language));
}); });
} }

9
src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs

@ -12,6 +12,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.Core; using Microsoft.OData.Core;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Core;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Contents; using Squidex.Read.Contents;
@ -49,7 +50,7 @@ namespace Squidex.Read.MongoDb.Contents
this.modelBuilder = modelBuilder; this.modelBuilder = modelBuilder;
} }
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages) public async Task<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig)
{ {
List<IContentEntity> result = null; List<IContentEntity> result = null;
@ -58,7 +59,7 @@ namespace Squidex.Read.MongoDb.Contents
IFindFluent<MongoContentEntity, MongoContentEntity> cursor; IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try try
{ {
var model = modelBuilder.BuildEdmModel(schemaEntity, languages); var model = modelBuilder.BuildEdmModel(schemaEntity, languagesConfig);
var parser = model.ParseQuery(odataQuery); var parser = model.ParseQuery(odataQuery);
@ -90,7 +91,7 @@ namespace Squidex.Read.MongoDb.Contents
return result; return result;
} }
public async Task<long> CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages) public async Task<long> CountAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig)
{ {
var result = 0L; var result = 0L;
@ -99,7 +100,7 @@ namespace Squidex.Read.MongoDb.Contents
IFindFluent<MongoContentEntity, MongoContentEntity> cursor; IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try try
{ {
var model = modelBuilder.BuildEdmModel(schemaEntity, languages); var model = modelBuilder.BuildEdmModel(schemaEntity, languagesConfig);
var parser = model.ParseQuery(odataQuery); var parser = model.ParseQuery(odataQuery);

6
src/Squidex.Read/Apps/IAppEntity.cs

@ -7,7 +7,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Core;
namespace Squidex.Read.Apps namespace Squidex.Read.Apps
{ {
@ -15,12 +15,10 @@ namespace Squidex.Read.Apps
{ {
string Name { get; } string Name { get; }
Language MasterLanguage { get; } LanguagesConfig LanguagesConfig { get; }
IReadOnlyCollection<IAppClientEntity> Clients { get; } IReadOnlyCollection<IAppClientEntity> Clients { get; }
IReadOnlyCollection<IAppContributorEntity> Contributors { get; } IReadOnlyCollection<IAppContributorEntity> Contributors { get; }
IReadOnlyCollection<Language> Languages { get; }
} }
} }

16
src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs

@ -7,11 +7,11 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Library; using Microsoft.OData.Edm.Library;
using Squidex.Core;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Read.Schemas; using Squidex.Read.Schemas;
@ -26,30 +26,32 @@ namespace Squidex.Read.Contents.Builders
{ {
} }
public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, HashSet<Language> languages) public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, LanguagesConfig languagesConfig)
{ {
Guard.NotNull(languages, nameof(languages)); Guard.NotNull(languagesConfig, nameof(languagesConfig));
Guard.NotNull(schemaEntity, nameof(schemaEntity)); Guard.NotNull(schemaEntity, nameof(schemaEntity));
var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{string.Join(",", languages.Select(x => x.Iso2Code).OrderBy(x => x))}"; var isoCodes = string.Join(",", languagesConfig.Select(x => x.Language.Iso2Code).OrderBy(x => x));
var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{isoCodes}";
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry => var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
{ {
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60);
return BuildEdmModel(schemaEntity.Schema, languages); return BuildEdmModel(schemaEntity.Schema, languagesConfig);
}); });
return result; return result;
} }
private static EdmModel BuildEdmModel(Schema schema, HashSet<Language> languages) private static EdmModel BuildEdmModel(Schema schema, LanguagesConfig languagesConfig)
{ {
var model = new EdmModel(); var model = new EdmModel();
var container = new EdmEntityContainer("Squidex", "Container"); var container = new EdmEntityContainer("Squidex", "Container");
var schemaType = schema.BuildEdmType(languages, x => var schemaType = schema.BuildEdmType(languagesConfig, x =>
{ {
model.AddElement(x); model.AddElement(x);

6
src/Squidex.Read/Contents/Repositories/IContentRepository.cs

@ -9,15 +9,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Core;
namespace Squidex.Read.Contents.Repositories namespace Squidex.Read.Contents.Repositories
{ {
public interface IContentRepository public interface IContentRepository
{ {
Task<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages); Task<IReadOnlyList<IContentEntity>> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig);
Task<long> CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet<Language> languages); Task<long> CountAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig);
Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id); Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id);
} }

5
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -109,6 +109,11 @@ namespace Squidex.Write.Apps
return handler.UpdateAsync<AppDomainObject>(context, a => a.RemoveLanguage(command)); return handler.UpdateAsync<AppDomainObject>(context, a => a.RemoveLanguage(command));
} }
protected Task On(UpdateLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a => a.UpdateLanguage(command));
}
protected Task On(SetMasterLanguage command, CommandContext context) protected Task On(SetMasterLanguage command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(context, a => a.SetMasterLanguage(command)); return handler.UpdateAsync<AppDomainObject>(context, a => a.SetMasterLanguage(command));

31
src/Squidex.Write/Apps/AppDomainObject.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Core;
using Squidex.Core.Apps; using Squidex.Core.Apps;
using Squidex.Events; using Squidex.Events;
using Squidex.Events.Apps; using Squidex.Events.Apps;
@ -26,8 +27,8 @@ namespace Squidex.Write.Apps
{ {
private static readonly Language DefaultLanguage = Language.EN; private static readonly Language DefaultLanguage = Language.EN;
private readonly AppContributors contributors = new AppContributors(); private readonly AppContributors contributors = new AppContributors();
private readonly AppLanguages languages = new AppLanguages();
private readonly AppClients clients = new AppClients(); private readonly AppClients clients = new AppClients();
private LanguagesConfig languagesConfig = LanguagesConfig.Empty;
private string name; private string name;
public string Name public string Name
@ -77,17 +78,22 @@ namespace Squidex.Write.Apps
protected void On(AppLanguageAdded @event) protected void On(AppLanguageAdded @event)
{ {
languages.Add(@event.Language); languagesConfig = languagesConfig.Add(@event.Language);
} }
protected void On(AppLanguageRemoved @event) protected void On(AppLanguageRemoved @event)
{ {
languages.Remove(@event.Language); languagesConfig = languagesConfig.Remove(@event.Language);
}
protected void On(AppLanguageUpdated @event)
{
languagesConfig = languagesConfig.Update(@event.Language, @event.IsOptional, @event.Fallback);
} }
protected void On(AppMasterLanguageSet @event) protected void On(AppMasterLanguageSet @event)
{ {
languages.SetMasterLanguage(@event.Language); languagesConfig = languagesConfig.MakeMaster(@event.Language);
} }
protected override void DispatchEvent(Envelope<IEvent> @event) protected override void DispatchEvent(Envelope<IEvent> @event)
@ -107,7 +113,6 @@ namespace Squidex.Write.Apps
RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command)));
RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId))); RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId)));
RaiseEvent(SimpleMapper.Map(command, CreateInitialMasterLanguage(appId)));
return this; return this;
} }
@ -189,6 +194,17 @@ namespace Squidex.Write.Apps
return this; return this;
} }
public AppDomainObject UpdateLanguage(UpdateLanguage command)
{
Guard.Valid(command, nameof(command), () => "Cannot update language");
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated()));
return this;
}
public AppDomainObject SetMasterLanguage(SetMasterLanguage command) public AppDomainObject SetMasterLanguage(SetMasterLanguage command)
{ {
Guard.Valid(command, nameof(command), () => "Cannot set master language"); Guard.Valid(command, nameof(command), () => "Cannot set master language");
@ -215,11 +231,6 @@ namespace Squidex.Write.Apps
return new AppLanguageAdded { AppId = id, Language = DefaultLanguage }; return new AppLanguageAdded { AppId = id, Language = DefaultLanguage };
} }
private static AppMasterLanguageSet CreateInitialMasterLanguage(NamedId<Guid> id)
{
return new AppMasterLanguageSet { AppId = id, Language = DefaultLanguage };
}
private static AppContributorAssigned CreateInitialOwner(NamedId<Guid> id, SquidexCommand command) private static AppContributorAssigned CreateInitialOwner(NamedId<Guid> id, SquidexCommand command)
{ {
return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = PermissionLevel.Owner }; return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = PermissionLevel.Owner };

73
src/Squidex.Write/Apps/AppLanguages.cs

@ -1,73 +0,0 @@
// ==========================================================================
// AppLanguages.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
namespace Squidex.Write.Apps
{
public class AppLanguages
{
private readonly HashSet<Language> languages = new HashSet<Language>();
private Language masterLanguage;
public void Add(Language language)
{
ThrowIfFound(language, () => "Cannot add language");
languages.Add(language);
}
public void Remove(Language language)
{
ThrowIfNotFound(language);
ThrowIfMasterLanguage(language, () => "Cannot remove language");
languages.Remove(language);
}
public void SetMasterLanguage(Language language)
{
ThrowIfNotFound(language);
ThrowIfMasterLanguage(language, () => "Cannot set master language");
masterLanguage = language;
}
private void ThrowIfNotFound(Language language)
{
if (!languages.Contains(language))
{
throw new DomainObjectNotFoundException(language.Iso2Code, "Languages", typeof(AppDomainObject));
}
}
private void ThrowIfFound(Language language, Func<string> message)
{
if (languages.Contains(language))
{
var error = new ValidationError("Language is already part of the app", "Language");
throw new ValidationException(message(), error);
}
}
private void ThrowIfMasterLanguage(Language language, Func<string> message)
{
if (masterLanguage != null && masterLanguage.Equals(language))
{
var error = new ValidationError("Language is the master language", "Language");
throw new ValidationException(message(), error);
}
}
}
}

30
src/Squidex.Write/Apps/Commands/UpdateLanguage.cs

@ -0,0 +1,30 @@
// ==========================================================================
// UpdateLanguage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Write.Apps.Commands
{
public sealed class UpdateLanguage : AppAggregateCommand, IValidatable
{
public Language Language { get; set; }
public bool IsOptional { get; set; }
public List<Language> Fallback { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (Language == null)
{
errors.Add(new ValidationError("Language cannot be null", nameof(Language)));
}
}
}
}

6
src/Squidex.Write/Contents/ContentCommandHandler.cs

@ -96,12 +96,10 @@ namespace Squidex.Write.Contents
await Task.WhenAll(taskForApp, taskForSchema); await Task.WhenAll(taskForApp, taskForSchema);
var languages = new HashSet<Language>(taskForApp.Result.Languages);
var schemaObject = taskForSchema.Result.Schema; var schemaObject = taskForSchema.Result.Schema;
var schemaErrors = new List<ValidationError>(); var schemaErrors = new List<ValidationError>();
await command.Data.ValidateAsync(schemaObject, languages, schemaErrors); await command.Data.ValidateAsync(schemaObject, taskForApp.Result.LanguagesConfig, schemaErrors);
if (schemaErrors.Count > 0) if (schemaErrors.Count > 0)
{ {
@ -110,7 +108,7 @@ namespace Squidex.Write.Contents
if (enrich) if (enrich)
{ {
command.Data.Enrich(schemaObject, languages); command.Data.Enrich(schemaObject, taskForApp.Result.LanguagesConfig);
} }
} }
} }

32
src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs

@ -6,6 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -60,12 +61,13 @@ namespace Squidex.Controllers.Api.Apps
return NotFound(); return NotFound();
} }
var model = entity.Languages.Select(x => var model = entity.LanguagesConfig.Select(x =>
SimpleMapper.Map(x.Language,
new AppLanguageDto
{ {
var isMasterLanguage = x.Equals(entity.MasterLanguage); IsMaster = x == entity.LanguagesConfig.Master,
IsOptional = x.IsOptional
return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage }); })).ToList();
}).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
@ -112,9 +114,11 @@ namespace Squidex.Controllers.Api.Apps
[Route("apps/{app}/languages/{language}")] [Route("apps/{app}/languages/{language}")]
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto model) public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto model)
{ {
if (model.IsMasterLanguage) await CommandBus.PublishAsync(SimpleMapper.Map(model, new UpdateLanguage()));
if (model.IsMaster == true)
{ {
await CommandBus.PublishAsync(new SetMasterLanguage { Language = Language.GetLanguage(language) }); await CommandBus.PublishAsync(new SetMasterLanguage { Language = ParseLanguage(language) });
} }
return NoContent(); return NoContent();
@ -135,9 +139,21 @@ namespace Squidex.Controllers.Api.Apps
[Route("apps/{app}/languages/{language}")] [Route("apps/{app}/languages/{language}")]
public async Task<IActionResult> DeleteLanguage(string app, string language) public async Task<IActionResult> DeleteLanguage(string app, string language)
{ {
await CommandBus.PublishAsync(new RemoveLanguage { Language = Language.GetLanguage(language) }); await CommandBus.PublishAsync(new RemoveLanguage { Language = ParseLanguage(language) });
return NoContent(); return NoContent();
} }
private static Language ParseLanguage(string language)
{
try
{
return Language.GetLanguage(language);
}
catch (NotSupportedException)
{
throw new ValidationException($"Language '{language}' is not valid.");
}
}
} }
} }

7
src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs

@ -27,6 +27,11 @@ namespace Squidex.Controllers.Api.Apps.Models
/// <summary> /// <summary>
/// Indicates if the language is the master language. /// Indicates if the language is the master language.
/// </summary> /// </summary>
public bool IsMasterLanguage { get; set; } public bool IsMaster { get; set; }
/// <summary>
/// Indicates if the language is optional.
/// </summary>
public bool IsOptional { get; set; }
} }
} }

16
src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs

@ -5,6 +5,10 @@
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public class UpdateAppLanguageDto public class UpdateAppLanguageDto
@ -12,6 +16,16 @@ namespace Squidex.Controllers.Api.Apps.Models
/// <summary> /// <summary>
/// Set the value to true to make the language to the master language. /// Set the value to true to make the language to the master language.
/// </summary> /// </summary>
public bool IsMasterLanguage { get; set; } public bool? IsMaster { get; set; }
/// <summary>
/// Set the value to true to make the language optional.
/// </summary>
public bool IsOptional { get; set; }
/// <summary>
/// Optional fallback languages.
/// </summary>
public List<Language> Fallback { get; set; }
} }
} }

12
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -7,7 +7,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -17,7 +16,6 @@ using NSwag.Annotations;
using Squidex.Controllers.ContentApi.Models; using Squidex.Controllers.ContentApi.Models;
using Squidex.Core.Contents; using Squidex.Core.Contents;
using Squidex.Core.Identity; using Squidex.Core.Identity;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
@ -55,12 +53,10 @@ namespace Squidex.Controllers.ContentApi
return NotFound(); return NotFound();
} }
var languages = new HashSet<Language>(App.Languages);
var query = Request.QueryString.ToString(); var query = Request.QueryString.ToString();
var taskForContents = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, query, languages); var taskForContents = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, query, App.LanguagesConfig);
var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, query, languages); var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, query, App.LanguagesConfig);
await Task.WhenAll(taskForContents, taskForCount); await Task.WhenAll(taskForContents, taskForCount);
@ -73,7 +69,7 @@ namespace Squidex.Controllers.ContentApi
if (x.Data != null) if (x.Data != null)
{ {
itemModel.Data = x.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage); itemModel.Data = x.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig);
} }
return itemModel; return itemModel;
@ -105,7 +101,7 @@ namespace Squidex.Controllers.ContentApi
if (entity.Data != null) if (entity.Data != null)
{ {
model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, hidden);
} }
Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); Response.Headers["ETag"] = new StringValues(entity.Version.ToString());

15
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -42,7 +42,6 @@ namespace Squidex.Controllers.ContentApi.Generator
private readonly MyUrlsOptions urlOptions; private readonly MyUrlsOptions urlOptions;
private readonly string schemaQueryDescription; private readonly string schemaQueryDescription;
private readonly string schemaBodyDescription; private readonly string schemaBodyDescription;
private HashSet<Language> languages;
private JsonSchema4 errorDtoSchema; private JsonSchema4 errorDtoSchema;
private string appBasePath; private string appBasePath;
private IAppEntity app; private IAppEntity app;
@ -62,15 +61,13 @@ namespace Squidex.Controllers.ContentApi.Generator
schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery"); schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery");
} }
public async Task<SwaggerDocument> Generate(IAppEntity appEntity, IEnumerable<ISchemaEntityWithSchema> schemas) public async Task<SwaggerDocument> Generate(IAppEntity targetApp, IEnumerable<ISchemaEntityWithSchema> schemas)
{ {
app = appEntity; app = targetApp;
languages = new HashSet<Language>(appEntity.Languages);
await GenerateBasicSchemas(); await GenerateBasicSchemas();
GenerateBasePath(appEntity); GenerateBasePath();
GenerateTitle(); GenerateTitle();
GenerateRequestInfo(); GenerateRequestInfo();
GenerateContentTypes(); GenerateContentTypes();
@ -84,9 +81,9 @@ namespace Squidex.Controllers.ContentApi.Generator
return document; return document;
} }
private void GenerateBasePath(IAppEntity appEntity) private void GenerateBasePath()
{ {
appBasePath = $"/content/{appEntity.Name}"; appBasePath = $"/content/{app.Name}";
} }
private void GenerateSchemes() private void GenerateSchemes()
@ -181,7 +178,7 @@ namespace Squidex.Controllers.ContentApi.Generator
Name = schemaName, Description = $"API to managed {schemaName} contents." Name = schemaName, Description = $"API to managed {schemaName} contents."
}); });
var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildJsonSchema(languages, AppendSchema)); var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildJsonSchema(app.LanguagesConfig, AppendSchema));
var schemaOperations = new List<SwaggerOperations> var schemaOperations = new List<SwaggerOperations>
{ {

5
tests/Squidex.Core.Tests/ContentEnrichmentTests.cs

@ -6,7 +6,6 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Moq; using Moq;
using NodaTime; using NodaTime;
using NodaTime.Text; using NodaTime.Text;
@ -19,7 +18,7 @@ namespace Squidex.Core
{ {
public class ContentEnrichmentTests public class ContentEnrichmentTests
{ {
private readonly HashSet<Language> languages = new HashSet<Language>(new[] { Language.DE, Language.EN }); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN);
[Fact] [Fact]
private void Should_enrich_with_default_values() private void Should_enrich_with_default_values()
@ -52,7 +51,7 @@ namespace Squidex.Core
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 456)); .AddValue("iv", 456));
data.Enrich(schema, languages); data.Enrich(schema, languagesConfig);
Assert.Equal(456, (int)data["my-number"]["iv"]); Assert.Equal(456, (int)data["my-number"]["iv"]);

59
tests/Squidex.Core.Tests/ContentValidationTests.cs

@ -18,7 +18,7 @@ namespace Squidex.Core
{ {
public class ContentValidationTests public class ContentValidationTests
{ {
private readonly HashSet<Language> languages = new HashSet<Language>(new[] { Language.DE, Language.EN }); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN);
private readonly List<ValidationError> errors = new List<ValidationError>(); private readonly List<ValidationError> errors = new List<ValidationError>();
private Schema schema = Schema.Create("my-name", new SchemaProperties()); private Schema schema = Schema.Create("my-name", new SchemaProperties());
@ -30,7 +30,7 @@ namespace Squidex.Core
.AddField("unknown", .AddField("unknown",
new ContentFieldData()); new ContentFieldData());
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -50,7 +50,7 @@ namespace Squidex.Core
new ContentFieldData() new ContentFieldData()
.SetValue(1000)); .SetValue(1000));
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -60,7 +60,7 @@ namespace Squidex.Core
} }
[Fact] [Fact]
public async Task Should_add_error_non_localizable_data_field_contains_language() public async Task Should_add_error_if_non_localizable_data_field_contains_language()
{ {
schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties()));
@ -71,12 +71,13 @@ namespace Squidex.Core
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("my-field can only contain a single entry for invariant language (iv)", "my-field") new ValidationError("my-field has an unsupported language 'es'", "my-field"),
new ValidationError("my-field has an unsupported language 'it'", "my-field")
}); });
} }
@ -88,7 +89,7 @@ namespace Squidex.Core
var data = var data =
new ContentData(); new ContentData();
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -106,7 +107,7 @@ namespace Squidex.Core
var data = var data =
new ContentData(); new ContentData();
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -127,7 +128,7 @@ namespace Squidex.Core
.AddValue("de", 1) .AddValue("de", 1)
.AddValue("xx", 1)); .AddValue("xx", 1));
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -136,6 +137,25 @@ namespace Squidex.Core
}); });
} }
[Fact]
public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language()
{
var optionalConfig =
LanguagesConfig.Create(Language.ES, Language.IT).Update(Language.IT, true, null);
schema = schema.AddOrUpdateField(new StringField(1, "my-field", new StringFieldProperties { IsLocalizable = true, IsRequired = true }));
var data =
new ContentData()
.AddField("my-field",
new ContentFieldData()
.AddValue("es", "value"));
await data.ValidateAsync(schema, optionalConfig, errors);
Assert.Equal(0, errors.Count);
}
[Fact] [Fact]
public async Task Should_add_error_if_data_contains_unsupported_language() public async Task Should_add_error_if_data_contains_unsupported_language()
{ {
@ -148,7 +168,7 @@ namespace Squidex.Core
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidateAsync(schema, languages, errors); await data.ValidateAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -166,7 +186,7 @@ namespace Squidex.Core
.AddField("unknown", .AddField("unknown",
new ContentFieldData()); new ContentFieldData());
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -186,7 +206,7 @@ namespace Squidex.Core
new ContentFieldData() new ContentFieldData()
.SetValue(1000)); .SetValue(1000));
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -196,7 +216,7 @@ namespace Squidex.Core
} }
[Fact] [Fact]
public async Task Should_add_error_non_localizable_partial_data_field_contains_language() public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language()
{ {
schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties()));
@ -207,12 +227,13 @@ namespace Squidex.Core
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("my-field can only contain a single entry for invariant language (iv)", "my-field") new ValidationError("my-field has an unsupported language 'es'", "my-field"),
new ValidationError("my-field has an unsupported language 'it'", "my-field")
}); });
} }
@ -224,7 +245,7 @@ namespace Squidex.Core
var data = var data =
new ContentData(); new ContentData();
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -237,7 +258,7 @@ namespace Squidex.Core
var data = var data =
new ContentData(); new ContentData();
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -254,7 +275,7 @@ namespace Squidex.Core
.AddValue("de", 1) .AddValue("de", 1)
.AddValue("xx", 1)); .AddValue("xx", 1));
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -275,7 +296,7 @@ namespace Squidex.Core
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidatePartialAsync(schema, languages, errors); await data.ValidatePartialAsync(schema, languagesConfig, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new List<ValidationError> new List<ValidationError>

55
tests/Squidex.Core.Tests/Contents/ContentDataTests.cs

@ -25,8 +25,7 @@ namespace Squidex.Core.Contents
.AddOrUpdateField(new NumberField(3, "field3", .AddOrUpdateField(new NumberField(3, "field3",
new NumberFieldProperties { IsLocalizable = false })) new NumberFieldProperties { IsLocalizable = false }))
.HideField(3); .HideField(3);
private readonly Language[] languages = { Language.DE, Language.EN }; private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE);
private readonly Language masterLanguage = Language.EN;
[Fact] [Fact]
public void Should_convert_to_id_model() public void Should_convert_to_id_model()
@ -109,7 +108,7 @@ namespace Squidex.Core.Contents
.AddValue("en", "en_string") .AddValue("en", "en_string")
.AddValue("de", "de_string")); .AddValue("de", "de_string"));
var actual = input.ToApiModel(schema, languages, masterLanguage); var actual = input.ToApiModel(schema, languagesConfig);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -131,7 +130,7 @@ namespace Squidex.Core.Contents
.AddValue("de", "de_string") .AddValue("de", "de_string")
.AddValue("it", "it_string")); .AddValue("it", "it_string"));
var actual = input.ToApiModel(schema, languages, masterLanguage); var actual = input.ToApiModel(schema, languagesConfig);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -152,7 +151,7 @@ namespace Squidex.Core.Contents
.AddValue("de", 2) .AddValue("de", 2)
.AddValue("en", 3)); .AddValue("en", 3));
var actual = input.ToApiModel(schema, languages, masterLanguage); var actual = input.ToApiModel(schema, languagesConfig);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -172,7 +171,7 @@ namespace Squidex.Core.Contents
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 3)); .AddValue("iv", 3));
var actual = input.ToApiModel(schema, languages, masterLanguage); var actual = input.ToApiModel(schema, languagesConfig);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -215,7 +214,7 @@ namespace Squidex.Core.Contents
.AddValue("de", 2) .AddValue("de", 2)
.AddValue("it", 3)); .AddValue("it", 3));
var actual = input.ToApiModel(schema, languages, masterLanguage); var actual = input.ToApiModel(schema, languagesConfig);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -238,7 +237,7 @@ namespace Squidex.Core.Contents
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 2)); .AddValue("iv", 2));
var actual = input.ToApiModel(schema, languages, masterLanguage); var actual = input.ToApiModel(schema, languagesConfig);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -252,7 +251,43 @@ namespace Squidex.Core.Contents
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 1)); .AddValue("iv", 1));
Assert.Same(data, data.ToLanguageModel()); Assert.Same(data, data.ToLanguageModel(languagesConfig));
}
[Fact]
public void Should_return_flat_list_when_single_languages_specified()
{
var data =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("de", 1)
.AddValue("en", 2))
.AddField("field2",
new ContentFieldData()
.AddValue("de", null)
.AddValue("en", 4))
.AddField("field3",
new ContentFieldData()
.AddValue("en", 6))
.AddField("field4",
new ContentFieldData()
.AddValue("it", 7));
var fallbackConfig =
LanguagesConfig.Create(Language.DE).Add(Language.EN)
.Update(Language.DE, false, new[] { Language.EN });
var output = (Dictionary<string, JToken>)data.ToLanguageModel(fallbackConfig, new List<Language> { Language.DE });
var expected = new Dictionary<string, JToken>
{
{ "field1", 1 },
{ "field2", 4 },
{ "field3", 6 }
};
Assert.True(expected.EqualsDictionary(output));
} }
[Fact] [Fact]
@ -275,7 +310,7 @@ namespace Squidex.Core.Contents
new ContentFieldData() new ContentFieldData()
.AddValue("it", 7)); .AddValue("it", 7));
var output = (Dictionary<string, JToken>)data.ToLanguageModel(new List<Language> { Language.DE, Language.EN }); var output = (Dictionary<string, JToken>)data.ToLanguageModel(languagesConfig, new List<Language> { Language.DE, Language.EN });
var expected = new Dictionary<string, JToken> var expected = new Dictionary<string, JToken>
{ {

213
tests/Squidex.Core.Tests/LanguagesConfigTests.cs

@ -0,0 +1,213 @@
// ==========================================================================
// LanguagesConfigTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Core
{
public class LanguagesConfigTests
{
[Fact]
public void Should_create_initial_config()
{
var config = LanguagesConfig.Create(Language.DE);
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE)
});
Assert.Equal(Language.DE, config.Master.Language);
}
[Fact]
public void Should_create_initial_config_with_multiple_languages()
{
var config = LanguagesConfig.Create(Language.DE, Language.EN, Language.IT);
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.EN),
new LanguageConfig(Language.IT)
});
Assert.Equal(Language.DE, config.Master.Language);
}
[Fact]
public void Should_create_initial_config_with_configs()
{
var configs = new[]
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.EN),
new LanguageConfig(Language.IT),
};
var config = LanguagesConfig.Create(configs);
config.ToList().ShouldBeEquivalentTo(configs);
Assert.Equal(configs[0], config.Master);
}
[Fact]
public void Should_add_language()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT);
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.IT)
});
}
[Fact]
public void Should_make_first_language_to_master()
{
var config = LanguagesConfig.Empty.Add(Language.IT);
Assert.Equal(Language.IT, config.Master.Language);
}
[Fact]
public void Should_throw_exception_if_language_to_add_already_exists()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<ValidationException>(() => config.Add(Language.DE));
}
[Fact]
public void Should_make_master_language()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).MakeMaster(Language.IT);
Assert.Equal(Language.IT, config.Master.Language);
}
[Fact]
public void Should_throw_exception_if_language_to_make_master_is_not_found()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<DomainObjectNotFoundException>(() => config.MakeMaster(Language.EN));
}
[Fact]
public void Should_not_throw_exception_if_language_is_already_master_language()
{
var config = LanguagesConfig.Create(Language.DE);
config.MakeMaster(Language.DE);
}
[Fact]
public void Should_remove_language()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Add(Language.RU).Remove(Language.IT);
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.RU)
});
}
[Fact]
public void Should_remove_fallbacks_when_removing_language()
{
var config =
LanguagesConfig.Create(Language.DE)
.Add(Language.IT)
.Add(Language.RU)
.Update(Language.DE, false, new[] { Language.RU, Language.IT })
.Update(Language.RU, false, new[] { Language.DE, Language.IT })
.Remove(Language.IT);
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE, false, Language.RU),
new LanguageConfig(Language.RU, false, Language.DE)
});
}
[Fact]
public void Should_throw_exception_if_language_to_remove_is_not_found()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<DomainObjectNotFoundException>(() => config.Remove(Language.EN));
}
[Fact]
public void Should_throw_exception_if_language_to_remove_is_master_language()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<ValidationException>(() => config.Remove(Language.DE));
}
[Fact]
public void Should_update_language()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, new[] { Language.DE });
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.IT, true, Language.DE)
});
}
[Fact]
public void Should_throw_exception_if_language_to_update_is_not_found()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<DomainObjectNotFoundException>(() => config.Update(Language.EN, true, null));
}
[Fact]
public void Should_throw_exception_if_fallback_language_is_invalid()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, new [] { Language.EN }));
}
[Fact]
public void Should_throw_exception_if_language_to_make_optional_is_master_language()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, null));
}
[Fact]
public void Should_provide_enumerators()
{
var config = LanguagesConfig.Create();
Assert.NotNull(((IEnumerable)config).GetEnumerator());
Assert.NotNull(((IEnumerable<LanguageConfig>)config).GetEnumerator());
}
}
}

12
tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs

@ -47,7 +47,7 @@ namespace Squidex.Core.Schemas
var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object);
await sut.ValidateAsync(CreateValue(assetId), errors); await sut.ValidateAsync(CreateValue(assetId), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -57,7 +57,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object);
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties { IsRequired = true }, assetTester.Object); var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties { IsRequired = true }, assetTester.Object);
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -78,7 +78,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties { IsRequired = true }, assetTester.Object); var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties { IsRequired = true }, assetTester.Object);
await sut.ValidateAsync(CreateValue(), errors); await sut.ValidateAsync(CreateValue(), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -89,7 +89,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object);
await sut.ValidateAsync("invalid", errors); await sut.ValidateAsync("invalid", false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });
@ -104,7 +104,7 @@ namespace Squidex.Core.Schemas
var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object);
await sut.ValidateAsync(CreateValue(assetId), errors); await sut.ValidateAsync(CreateValue(assetId), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { $"<FIELD> contains invalid asset '{assetId}'" }); new[] { $"<FIELD> contains invalid asset '{assetId}'" });

8
tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -49,7 +49,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties());
await sut.ValidateAsync(CreateValue(true), errors); await sut.ValidateAsync(CreateValue(true), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -59,7 +59,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { IsRequired = true }); var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -70,7 +70,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties());
await sut.ValidateAsync(CreateValue("Invalid"), errors); await sut.ValidateAsync(CreateValue("Invalid"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });

12
tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs

@ -41,7 +41,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -51,7 +51,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { IsRequired = true }); var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -62,7 +62,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MinValue = FutureDays(10) }); var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MinValue = FutureDays(10) });
await sut.ValidateAsync(CreateValue(FutureDays(0)), errors); await sut.ValidateAsync(CreateValue(FutureDays(0)), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { $"<FIELD> must be greater than '{FutureDays(10)}'" }); new[] { $"<FIELD> must be greater than '{FutureDays(10)}'" });
@ -73,7 +73,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MaxValue = FutureDays(10) }); var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MaxValue = FutureDays(10) });
await sut.ValidateAsync(CreateValue(FutureDays(20)), errors); await sut.ValidateAsync(CreateValue(FutureDays(20)), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { $"<FIELD> must be less than '{FutureDays(10)}'" }); new[] { $"<FIELD> must be less than '{FutureDays(10)}'" });
@ -84,7 +84,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties());
await sut.ValidateAsync(CreateValue("Invalid"), errors); await sut.ValidateAsync(CreateValue("Invalid"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });
@ -95,7 +95,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties());
await sut.ValidateAsync(CreateValue(123), errors); await sut.ValidateAsync(CreateValue(123), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });

10
tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties());
await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); await sut.ValidateAsync(CreateValue(JValue.CreateNull()), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -53,7 +53,7 @@ namespace Squidex.Core.Schemas
new JProperty("latitude", 0), new JProperty("latitude", 0),
new JProperty("longitude", 0)); new JProperty("longitude", 0));
await sut.ValidateAsync(CreateValue(geolocation), errors); await sut.ValidateAsync(CreateValue(geolocation), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas
new JProperty("latitude", 200), new JProperty("latitude", 200),
new JProperty("longitude", 0)); new JProperty("longitude", 0));
await sut.ValidateAsync(CreateValue(geolocation), errors); await sut.ValidateAsync(CreateValue(geolocation), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });
@ -83,7 +83,7 @@ namespace Squidex.Core.Schemas
new JProperty("latitude", 0), new JProperty("latitude", 0),
new JProperty("longitude", 0)); new JProperty("longitude", 0));
await sut.ValidateAsync(CreateValue(geolocation), errors); await sut.ValidateAsync(CreateValue(geolocation), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });
@ -94,7 +94,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { IsRequired = true }); var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); await sut.ValidateAsync(CreateValue(JValue.CreateNull()), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });

4
tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new JsonField(1, "my-json", new JsonFieldProperties()); var sut = new JsonField(1, "my-json", new JsonFieldProperties());
await sut.ValidateAsync(CreateValue(new JValue(1)), errors); await sut.ValidateAsync(CreateValue(new JValue(1)), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -49,7 +49,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new JsonField(1, "my-json", new JsonFieldProperties { IsRequired = true }); var sut = new JsonField(1, "my-json", new JsonFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); await sut.ValidateAsync(CreateValue(JValue.CreateNull()), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });

12
tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs

@ -40,7 +40,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new NumberField(1, "my-number", new NumberFieldProperties()); var sut = new NumberField(1, "my-number", new NumberFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -50,7 +50,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new NumberField(1, "my-number", new NumberFieldProperties { IsRequired = true }); var sut = new NumberField(1, "my-number", new NumberFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -61,7 +61,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new NumberField(1, "my-number", new NumberFieldProperties { MinValue = 10 }); var sut = new NumberField(1, "my-number", new NumberFieldProperties { MinValue = 10 });
await sut.ValidateAsync(CreateValue(5), errors); await sut.ValidateAsync(CreateValue(5), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must be greater than '10'" }); new[] { "<FIELD> must be greater than '10'" });
@ -72,7 +72,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new NumberField(1, "my-number", new NumberFieldProperties { MaxValue = 10 }); var sut = new NumberField(1, "my-number", new NumberFieldProperties { MaxValue = 10 });
await sut.ValidateAsync(CreateValue(20), errors); await sut.ValidateAsync(CreateValue(20), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must be less than '10'" }); new[] { "<FIELD> must be less than '10'" });
@ -83,7 +83,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new NumberField(1, "my-number", new NumberFieldProperties { AllowedValues = ImmutableList.Create(10d) }); var sut = new NumberField(1, "my-number", new NumberFieldProperties { AllowedValues = ImmutableList.Create(10d) });
await sut.ValidateAsync(CreateValue(20), errors); await sut.ValidateAsync(CreateValue(20), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not an allowed value" }); new[] { "<FIELD> is not an allowed value" });
@ -94,7 +94,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new NumberField(1, "my-number", new NumberFieldProperties()); var sut = new NumberField(1, "my-number", new NumberFieldProperties());
await sut.ValidateAsync(CreateValue("Invalid"), errors); await sut.ValidateAsync(CreateValue("Invalid"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not a valid value" }); new[] { "<FIELD> is not a valid value" });

8
tests/Squidex.Core.Tests/Schemas/SchemaTests.cs

@ -295,9 +295,9 @@ namespace Squidex.Core.Schemas
[Fact] [Fact]
public void Should_build_schema() public void Should_build_schema()
{ {
var languages = new HashSet<Language>(new[] { Language.DE, Language.EN }); var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN);
var jsonSchema = BuildMixedSchema().BuildJsonSchema(languages, (n, s) => new JsonSchema4 { SchemaReference = s }); var jsonSchema = BuildMixedSchema().BuildJsonSchema(languagesConfig, (n, s) => new JsonSchema4 { SchemaReference = s });
Assert.NotNull(jsonSchema); Assert.NotNull(jsonSchema);
} }
@ -305,9 +305,9 @@ namespace Squidex.Core.Schemas
[Fact] [Fact]
public void Should_build_edm_model() public void Should_build_edm_model()
{ {
var languages = new HashSet<Language>(new[] { Language.DE, Language.EN }); var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN);
var edmModel = BuildMixedSchema().BuildEdmType(languages, x => x); var edmModel = BuildMixedSchema().BuildEdmType(languagesConfig, x => x);
Assert.NotNull(edmModel); Assert.NotNull(edmModel);
} }

14
tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs

@ -40,7 +40,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "<FIELD>" }); var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "<FIELD>" });
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -50,7 +50,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { IsRequired = true }); var sut = new StringField(1, "my-string", new StringFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors); await sut.ValidateAsync(CreateValue(null), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -61,7 +61,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { MinLength = 10 }); var sut = new StringField(1, "my-string", new StringFieldProperties { MinLength = 10 });
await sut.ValidateAsync(CreateValue("123"), errors); await sut.ValidateAsync(CreateValue("123"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have more than '10' characters" }); new[] { "<FIELD> must have more than '10' characters" });
@ -72,7 +72,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { MaxLength = 5 }); var sut = new StringField(1, "my-string", new StringFieldProperties { MaxLength = 5 });
await sut.ValidateAsync(CreateValue("12345678"), errors); await sut.ValidateAsync(CreateValue("12345678"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have less than '5' characters" }); new[] { "<FIELD> must have less than '5' characters" });
@ -83,7 +83,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { AllowedValues = ImmutableList.Create("Foo") }); var sut = new StringField(1, "my-string", new StringFieldProperties { AllowedValues = ImmutableList.Create("Foo") });
await sut.ValidateAsync(CreateValue("Bar"), errors); await sut.ValidateAsync(CreateValue("Bar"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not an allowed value" }); new[] { "<FIELD> is not an allowed value" });
@ -94,7 +94,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}" }); var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}" });
await sut.ValidateAsync(CreateValue("abc"), errors); await sut.ValidateAsync(CreateValue("abc"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not valid" }); new[] { "<FIELD> is not valid" });
@ -105,7 +105,7 @@ namespace Squidex.Core.Schemas
{ {
var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message" }); var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message" });
await sut.ValidateAsync(CreateValue("abc"), errors); await sut.ValidateAsync(CreateValue("abc"), false, errors);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "Custom Error Message" }); new[] { "Custom Error Message" });

4
tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs

@ -14,9 +14,9 @@ namespace Squidex.Core.Schemas
{ {
public static class ValidationTestExtensions public static class ValidationTestExtensions
{ {
public static Task ValidateAsync(this Field field, JToken value, IList<string> errors) public static Task ValidateAsync(this Field field, JToken value, bool isOptional, IList<string> errors)
{ {
return field.ValidateAsync(value, errors.Add); return field.ValidateAsync(value, isOptional, errors.Add);
} }
} }
} }

6
tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs

@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new AllowedValuesValidator<int>(100, 200); var sut = new AllowedValuesValidator<int>(100, 200);
await sut.ValidateAsync(null, errors.Add); await sut.ValidateAsync(null, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new AllowedValuesValidator<int>(100, 200); var sut = new AllowedValuesValidator<int>(100, 200);
await sut.ValidateAsync(100, errors.Add); await sut.ValidateAsync(100, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -42,7 +42,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new AllowedValuesValidator<int>(100, 200); var sut = new AllowedValuesValidator<int>(100, 200);
await sut.ValidateAsync(50, errors.Add); await sut.ValidateAsync(50, false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not an allowed value" }); new[] { "<FIELD> is not an allowed value" });

18
tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs

@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); var sut = new PatternValidator("[a-z]{3}:[0-9]{2}");
await sut.ValidateAsync("abc:12", errors.Add); await sut.ValidateAsync("abc:12", false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -32,7 +32,17 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); var sut = new PatternValidator("[a-z]{3}:[0-9]{2}");
await sut.ValidateAsync(null, errors.Add); await sut.ValidateAsync(null, false, errors.Add);
Assert.Equal(0, errors.Count);
}
[Fact]
public async Task Should_not_add_error_if_value_is_empty()
{
var sut = new PatternValidator("[a-z]{3}:[0-9]{2}");
await sut.ValidateAsync("", false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -42,7 +52,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); var sut = new PatternValidator("[a-z]{3}:[0-9]{2}");
await sut.ValidateAsync("foo", errors.Add); await sut.ValidateAsync("foo", false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is not valid" }); new[] { "<FIELD> is not valid" });
@ -53,7 +63,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new PatternValidator("[a-z]{3}:[0-9]{2}", "Custom Error Message"); var sut = new PatternValidator("[a-z]{3}:[0-9]{2}", "Custom Error Message");
await sut.ValidateAsync("foo", errors.Add); await sut.ValidateAsync("foo", false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "Custom Error Message" }); new[] { "Custom Error Message" });

8
tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs

@ -23,7 +23,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RangeValidator<int>(100, 200); var sut = new RangeValidator<int>(100, 200);
await sut.ValidateAsync(null, errors.Add); await sut.ValidateAsync(null, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -37,7 +37,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RangeValidator<int>(min, max); var sut = new RangeValidator<int>(min, max);
await sut.ValidateAsync(1500, errors.Add); await sut.ValidateAsync(1500, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -55,7 +55,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RangeValidator<int>(2000, null); var sut = new RangeValidator<int>(2000, null);
await sut.ValidateAsync(1500, errors.Add); await sut.ValidateAsync(1500, false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must be greater than '2000'" }); new[] { "<FIELD> must be greater than '2000'" });
@ -66,7 +66,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RangeValidator<int>(null, 1000); var sut = new RangeValidator<int>(null, 1000);
await sut.ValidateAsync(1500, errors.Add); await sut.ValidateAsync(1500, false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must be less than '1000'" }); new[] { "<FIELD> must be less than '1000'" });

18
tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs

@ -26,7 +26,17 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredStringValidator(); var sut = new RequiredStringValidator();
await sut.ValidateAsync(value, errors.Add); await sut.ValidateAsync(value, false, errors.Add);
Assert.Equal(0, errors.Count);
}
[Fact]
public async Task Should_not_add_error_if_optional()
{
var sut = new RequiredStringValidator();
await sut.ValidateAsync(string.Empty, true, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -36,7 +46,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredStringValidator(); var sut = new RequiredStringValidator();
await sut.ValidateAsync(true, errors.Add); await sut.ValidateAsync(true, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -46,7 +56,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredStringValidator(true); var sut = new RequiredStringValidator(true);
await sut.ValidateAsync(string.Empty, errors.Add); await sut.ValidateAsync(string.Empty, false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });
@ -57,7 +67,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredStringValidator(); var sut = new RequiredStringValidator();
await sut.ValidateAsync(null, errors.Add); await sut.ValidateAsync(null, false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });

16
tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs

@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredValidator(); var sut = new RequiredValidator();
await sut.ValidateAsync(true, errors.Add); await sut.ValidateAsync(true, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -32,7 +32,17 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredValidator(); var sut = new RequiredValidator();
await sut.ValidateAsync(string.Empty, errors.Add); await sut.ValidateAsync(string.Empty, false, errors.Add);
Assert.Equal(0, errors.Count);
}
[Fact]
public async Task Should_not_add_error_if_optional()
{
var sut = new RequiredValidator();
await sut.ValidateAsync(null, true, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -42,7 +52,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new RequiredValidator(); var sut = new RequiredValidator();
await sut.ValidateAsync(null, errors.Add); await sut.ValidateAsync(null, false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> is required" }); new[] { "<FIELD> is required" });

18
tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs

@ -24,7 +24,17 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new StringLengthValidator(100, 200); var sut = new StringLengthValidator(100, 200);
await sut.ValidateAsync(null, errors.Add); await sut.ValidateAsync(null, false, errors.Add);
Assert.Equal(0, errors.Count);
}
[Fact]
public async Task Should_not_error_if_value_is_empty()
{
var sut = new StringLengthValidator(100, 200);
await sut.ValidateAsync(string.Empty, false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -38,7 +48,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new StringLengthValidator(min, max); var sut = new StringLengthValidator(min, max);
await sut.ValidateAsync(CreateString(1500), errors.Add); await sut.ValidateAsync(CreateString(1500), false, errors.Add);
Assert.Equal(0, errors.Count); Assert.Equal(0, errors.Count);
} }
@ -56,7 +66,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new StringLengthValidator(2000, null); var sut = new StringLengthValidator(2000, null);
await sut.ValidateAsync(CreateString(1500), errors.Add); await sut.ValidateAsync(CreateString(1500), false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have more than '2000' characters" }); new[] { "<FIELD> must have more than '2000' characters" });
@ -67,7 +77,7 @@ namespace Squidex.Core.Schemas.Validators
{ {
var sut = new StringLengthValidator(null, 1000); var sut = new StringLengthValidator(null, 1000);
await sut.ValidateAsync(CreateString(1500), errors.Add); await sut.ValidateAsync(CreateString(1500), false, errors.Add);
errors.ShouldBeEquivalentTo( errors.ShouldBeEquivalentTo(
new[] { "<FIELD> must have less than '1000' characters" }); new[] { "<FIELD> must have less than '1000' characters" });

16
tests/Squidex.Infrastructure.Tests/LanguageTests.cs

@ -66,6 +66,22 @@ namespace Squidex.Infrastructure
Assert.False(Language.IsValidLanguage("xx")); Assert.False(Language.IsValidLanguage("xx"));
} }
[Fact]
public void Should_make_implicit_conversion_to_language()
{
Language language = "de";
Assert.Equal(Language.DE, language);
}
[Fact]
public void Should_make_implicit_conversion_to_string()
{
string iso2Code = Language.DE;
Assert.Equal("de", iso2Code);
}
[Theory] [Theory]
[InlineData("de", "German")] [InlineData("de", "German")]
[InlineData("en", "English")] [InlineData("en", "English")]

10
tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs

@ -7,7 +7,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -15,6 +14,7 @@ using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Moq; using Moq;
using Squidex.Core;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -45,11 +45,7 @@ namespace Squidex.Read.MongoDb.Contents
private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry;
private readonly IBsonSerializer<MongoContentEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); private readonly IBsonSerializer<MongoContentEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
private readonly IEdmModel edmModel; private readonly IEdmModel edmModel;
private readonly HashSet<Language> languages = new HashSet<Language> private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE);
{
Language.EN,
Language.DE
};
static ODataQueryTests() static ODataQueryTests()
{ {
@ -65,7 +61,7 @@ namespace Squidex.Read.MongoDb.Contents
schemaEntity.Setup(x => x.Version).Returns(3); schemaEntity.Setup(x => x.Version).Returns(3);
schemaEntity.Setup(x => x.Schema).Returns(schema); schemaEntity.Setup(x => x.Schema).Returns(schema);
edmModel = builder.BuildEdmModel(schemaEntity.Object, languages); edmModel = builder.BuildEdmModel(schemaEntity.Object, languagesConfig);
} }
[Fact] [Fact]

14
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -227,6 +227,20 @@ namespace Squidex.Write.Apps
}); });
} }
[Fact]
public async Task UpdateLanguage_should_update_domain_object()
{
CreateApp()
.AddLanguage(CreateCommand(new AddLanguage { Language = language }));
var context = CreateContextForCommand(new UpdateLanguage { Language = language });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
private AppDomainObject CreateApp() private AppDomainObject CreateApp()
{ {
app.Create(CreateCommand(new CreateApp { Name = AppName })); app.Create(CreateCommand(new CreateApp { Name = AppName }));

67
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -7,6 +7,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Core.Apps; using Squidex.Core.Apps;
using Squidex.Events.Apps; using Squidex.Events.Apps;
@ -64,8 +65,7 @@ namespace Squidex.Write.Apps
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }), CreateEvent(new AppCreated { Name = AppName }),
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = PermissionLevel.Owner }), CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = PermissionLevel.Owner }),
CreateEvent(new AppLanguageAdded { Language = Language.EN }), CreateEvent(new AppLanguageAdded { Language = Language.EN })
CreateEvent(new AppMasterLanguageSet { Language = Language.EN })
); );
} }
@ -432,7 +432,7 @@ namespace Squidex.Write.Apps
public void RemoveLanguage_should_create_events() public void RemoveLanguage_should_create_events()
{ {
CreateApp(); CreateApp();
CreateLanguage(); CreateLanguage(Language.DE);
sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE }));
@ -474,27 +474,72 @@ namespace Squidex.Write.Apps
} }
[Fact] [Fact]
public void SetMasterLanguage_should_throw_if_already_master_language() public void SetMasterLanguage_should_create_events()
{
CreateApp();
CreateLanguage(Language.DE);
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.DE }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppMasterLanguageSet { Language = Language.DE })
);
}
[Fact]
public void UpdateLanguage_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN }));
});
}
[Fact]
public void UpdateLanguage_should_throw_if_command_is_not_valid()
{ {
CreateApp(); CreateApp();
Assert.Throws<ValidationException>(() => Assert.Throws<ValidationException>(() =>
{ {
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.EN })); sut.UpdateLanguage(CreateCommand(new UpdateLanguage()));
}); });
} }
[Fact] [Fact]
public void SetMasterLanguage_should_create_events() public void UpdateLanguage_should_throw_if_language_not_found()
{ {
CreateApp(); CreateApp();
CreateLanguage();
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.DE })); Assert.Throws<DomainObjectNotFoundException>(() =>
{
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE }));
});
}
[Fact]
public void UpdateLanguage_should_throw_if_master_language()
{
CreateApp();
Assert.Throws<ValidationException>(() =>
{
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN, IsOptional = true }));
});
}
[Fact]
public void UpdateLanguage_should_create_events()
{
CreateApp();
CreateLanguage(Language.DE);
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } }));
sut.GetUncomittedEvents() sut.GetUncomittedEvents()
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppMasterLanguageSet { Language = Language.DE }) CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List<Language> { Language.EN } })
); );
} }
@ -512,9 +557,9 @@ namespace Squidex.Write.Apps
((IAggregate)sut).ClearUncommittedEvents(); ((IAggregate)sut).ClearUncommittedEvents();
} }
private void CreateLanguage() private void CreateLanguage(Language language)
{ {
sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); sut.AddLanguage(CreateCommand(new AddLanguage { Language = language }));
((IAggregate)sut).ClearUncommittedEvents(); ((IAggregate)sut).ClearUncommittedEvents();
} }

4
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -9,6 +9,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Moq; using Moq;
using Squidex.Core;
using Squidex.Core.Contents; using Squidex.Core.Contents;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -34,6 +35,7 @@ namespace Squidex.Write.Contents
private readonly Mock<ISchemaEntityWithSchema> schemaEntity = new Mock<ISchemaEntityWithSchema>(); private readonly Mock<ISchemaEntityWithSchema> schemaEntity = new Mock<ISchemaEntityWithSchema>();
private readonly Mock<IAppEntity> appEntity = new Mock<IAppEntity>(); private readonly Mock<IAppEntity> appEntity = new Mock<IAppEntity>();
private readonly ContentData data = new ContentData().AddField("my-field", new ContentFieldData().SetValue(1)); private readonly ContentData data = new ContentData().AddField("my-field", new ContentFieldData().SetValue(1));
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE);
private readonly Guid contentId = Guid.NewGuid(); private readonly Guid contentId = Guid.NewGuid();
public ContentCommandHandlerTests() public ContentCommandHandlerTests()
@ -47,7 +49,7 @@ namespace Squidex.Write.Contents
sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object); sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object);
appEntity.Setup(x => x.Languages).Returns(new[] { Language.DE }); appEntity.Setup(x => x.LanguagesConfig).Returns(languagesConfig);
appProvider.Setup(x => x.FindAppByIdAsync(AppId)).Returns(Task.FromResult(appEntity.Object)); appProvider.Setup(x => x.FindAppByIdAsync(AppId)).Returns(Task.FromResult(appEntity.Object));
schemaEntity.Setup(x => x.Schema).Returns(schema); schemaEntity.Setup(x => x.Schema).Returns(schema);

Loading…
Cancel
Save