Browse Source

Unify and improve the cleanup process when returning content items. (#478)

* Splitted the enrichment in several steps.

* Get rid of unused content.

* Improved value converter.

* Content enrichment improvements.

* Naming improved.

* Auto implementation reverted.

* Tests fixed.

* Index fixes.

* FIx to queries.

* Cleanup.

* Revert "Cleanup."

This reverts commit 4b59ce937c.

* Tests fixed.
pull/481/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
9cfd8f34b6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 59
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs
  2. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  3. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs
  4. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs
  6. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueGenerator.cs
  7. 84
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs
  8. 63
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  9. 41
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs
  10. 84
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  11. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs
  12. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonError.cs
  13. 96
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  14. 11
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  15. 41
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  16. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  17. 3
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  18. 52
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  19. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  20. 35
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/CleanupReferences.cs
  21. 15
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  23. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs
  24. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  25. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  26. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  28. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  29. 94
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs
  30. 316
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  31. 68
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs
  33. 21
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs
  34. 158
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  35. 36
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs
  36. 44
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs
  37. 82
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs
  38. 131
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
  39. 167
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
  40. 5
      backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs
  41. 40
      backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs
  42. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  43. 19
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  44. 86
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs
  45. 36
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs
  46. 14
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs
  47. 289
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs
  48. 11
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  49. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs
  50. 15
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs
  51. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs
  52. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  53. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs
  54. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  55. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs
  56. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs
  57. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs
  58. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  59. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs
  60. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs
  61. 31
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs
  62. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  63. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs
  64. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  65. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs
  66. 176
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs
  67. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  68. 146
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs
  69. 57
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs
  70. 79
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs
  71. 109
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs
  72. 92
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs
  73. 107
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs
  74. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs
  75. 2
      backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs
  76. 83
      backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs
  77. 4
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs

59
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs

@ -7,11 +7,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
@ -20,63 +18,6 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
private static readonly Func<IRootField, string> KeyNameResolver = f => f.Name;
private static readonly Func<IRootField, long> KeyIdResolver = f => f.Id;
public static string ToFullText<T>(this ContentData<T> data, int maxTotalLength = 1024 * 1024, int maxFieldLength = 1000, string separator = " ") where T : notnull
{
var stringBuilder = new StringBuilder();
foreach (var fieldValue in data.Values)
{
if (fieldValue != null)
{
foreach (var value in fieldValue.Values)
{
AppendText(value, stringBuilder, maxFieldLength, separator, false);
}
}
}
var result = stringBuilder.ToString();
if (result.Length > maxTotalLength)
{
result = result.Substring(0, maxTotalLength);
}
return result;
}
private static void AppendText(IJsonValue value, StringBuilder stringBuilder, int maxFieldLength, string separator, bool allowObjects)
{
if (value.Type == JsonValueType.String)
{
var text = value.ToString();
if (text.Length <= maxFieldLength)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(separator);
}
stringBuilder.Append(text);
}
}
else if (value is JsonArray array)
{
foreach (var item in array)
{
AppendText(item, stringBuilder, maxFieldLength, separator, true);
}
}
else if (value is JsonObject obj && allowObjects)
{
foreach (var item in obj.Values)
{
AppendText(item, stringBuilder, maxFieldLength, separator, true);
}
}
}
public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters)
{
Guard.NotNull(schema);

7
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -68,7 +68,12 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
try
{
JsonValueConverter.ConvertValue(field, value);
var (_, error) = JsonValueConverter.ConvertValue(field, value);
if (error != null)
{
return null;
}
}
catch
{

7
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs

@ -64,7 +64,12 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
try
{
JsonValueConverter.ConvertValue(field, value);
var (_, error) = JsonValueConverter.ConvertValue(field, value);
if (error != null)
{
return Value.Unset;
}
}
catch
{

8
backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs → backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs

@ -8,13 +8,13 @@
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.EnrichContent
namespace Squidex.Domain.Apps.Core.DefaultValues
{
public static class ContentEnrichmentExtensions
public static class DefaultValueExtensions
{
public static void Enrich(this NamedContentData data, Schema schema, PartitionResolver partitionResolver)
public static void GenerateDefaultValues(this NamedContentData data, Schema schema, PartitionResolver partitionResolver)
{
var enricher = new ContentEnricher(schema, partitionResolver);
var enricher = new DefaultValueGenerator(schema, partitionResolver);
enricher.Enrich(data);
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs → backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs

@ -11,7 +11,7 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.EnrichContent
namespace Squidex.Domain.Apps.Core.DefaultValues
{
public sealed class DefaultValueFactory : IFieldVisitor<IJsonValue>
{
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
{
if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now)
{
return JsonValue.Create(now.ToString());
return JsonValue.Create(now);
}
if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today)
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
return JsonValue.Create($"{now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z");
}
return JsonValue.Create(field.Properties.DefaultValue?.ToString());
return JsonValue.Create(field.Properties.DefaultValue);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs → backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueGenerator.cs

@ -11,14 +11,14 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.EnrichContent
namespace Squidex.Domain.Apps.Core.DefaultValues
{
public sealed class ContentEnricher
public sealed class DefaultValueGenerator
{
private readonly Schema schema;
private readonly PartitionResolver partitionResolver;
public ContentEnricher(Schema schema, PartitionResolver partitionResolver)
public DefaultValueGenerator(Schema schema, PartitionResolver partitionResolver)
{
Guard.NotNull(schema);
Guard.NotNull(partitionResolver);

84
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs

@ -17,79 +17,81 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ContentReferencesExtensions
{
public static IEnumerable<Guid> GetReferencedIds(this IdContentData source, Schema schema, Ids strategy = Ids.All)
public static HashSet<Guid> GetReferencedIds(this NamedContentData source, Schema schema)
{
Guard.NotNull(schema);
foreach (var field in schema.Fields)
{
var ids = source.GetReferencedIds(field, strategy);
var extractor = new ReferencesExtractor(new HashSet<Guid>());
foreach (var id in ids)
{
yield return id;
}
}
AddReferencedIds(source, schema.Fields, extractor);
return extractor.Result;
}
public static IEnumerable<Guid> GetReferencedIds(this IdContentData source, IField field, Ids strategy = Ids.All)
public static void AddReferencedIds(this NamedContentData source, Schema schema, HashSet<Guid> result)
{
Guard.NotNull(field);
Guard.NotNull(schema);
if (source.TryGetValue(field.Id, out var fieldData) && fieldData != null)
{
foreach (var partitionValue in fieldData)
{
var ids = field.GetReferencedIds(partitionValue.Value, strategy);
var extractor = new ReferencesExtractor(result);
foreach (var id in ids)
{
yield return id;
}
}
}
AddReferencedIds(source, schema.Fields, extractor);
}
public static IEnumerable<Guid> GetReferencedIds(this NamedContentData source, Schema schema, Ids strategy = Ids.All)
public static void AddReferencedIds(this NamedContentData source, IEnumerable<IField> fields, HashSet<Guid> result)
{
Guard.NotNull(schema);
Guard.NotNull(fields);
var extractor = new ReferencesExtractor(result);
return GetReferencedIds(source, schema.Fields, strategy);
AddReferencedIds(source, fields, extractor);
}
public static IEnumerable<Guid> GetReferencedIds(this NamedContentData source, IEnumerable<IField> fields, Ids strategy = Ids.All)
public static void AddReferencedIds(this NamedContentData source, IField field, HashSet<Guid> result)
{
Guard.NotNull(fields);
Guard.NotNull(field);
var extractor = new ReferencesExtractor(result);
AddReferencedIds(source, field, extractor);
}
private static void AddReferencedIds(NamedContentData source, IEnumerable<IField> fields, ReferencesExtractor extractor)
{
foreach (var field in fields)
{
var ids = source.GetReferencedIds(field, strategy);
foreach (var id in ids)
{
yield return id;
}
AddReferencedIds(source, field, extractor);
}
}
public static IEnumerable<Guid> GetReferencedIds(this NamedContentData source, IField field, Ids strategy = Ids.All)
private static void AddReferencedIds(NamedContentData source, IField field, ReferencesExtractor extractor)
{
Guard.NotNull(field);
if (source.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{
foreach (var partitionValue in fieldData)
{
var ids = field.GetReferencedIds(partitionValue.Value, strategy);
extractor.SetValue(partitionValue.Value);
foreach (var id in ids)
{
yield return id;
}
field.Accept(extractor);
}
}
}
public static HashSet<Guid> GetReferencedIds(this IField field, IJsonValue? value)
{
var result = new HashSet<Guid>();
if (value != null)
{
var extractor = new ReferencesExtractor(result);
extractor.SetValue(value);
field.Accept(extractor);
}
return result;
}
public static JsonObject FormatReferences(this NamedContentData data, Schema schema, IFieldPartitioning partitioning, string separator = ", ")
{
Guard.NotNull(schema);

63
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -8,25 +8,26 @@
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public sealed class ReferencesCleaner : IFieldVisitor<IJsonValue>
{
private readonly IJsonValue value;
private readonly ICollection<Guid>? oldReferences;
private readonly HashSet<Guid> validIds;
private IJsonValue value;
private ReferencesCleaner(IJsonValue value, ICollection<Guid>? oldReferences)
public ReferencesCleaner(HashSet<Guid> validIds)
{
this.value = value;
Guard.NotNull(validIds);
this.oldReferences = oldReferences;
this.validIds = validIds;
}
public static IJsonValue CleanReferences(IField field, IJsonValue value, ICollection<Guid>? oldReferences)
public void SetValue(IJsonValue newValue)
{
return field.Accept(new ReferencesCleaner(value, oldReferences));
value = newValue;
}
public IJsonValue Visit(IField<AssetsFieldProperties> field)
@ -36,31 +37,9 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
public IJsonValue Visit(IField<ReferencesFieldProperties> field)
{
if (oldReferences?.Contains(field.Properties.SingleId()) == true)
{
return JsonValue.Array();
}
return CleanIds();
}
private IJsonValue CleanIds()
{
var ids = value.ToGuidSet();
var isRemoved = false;
if (oldReferences != null)
{
foreach (var oldReference in oldReferences)
{
isRemoved |= ids.Remove(oldReference);
}
}
return isRemoved ? ids.ToJsonArray() : value;
}
public IJsonValue Visit(IField<BooleanFieldProperties> field)
{
return value;
@ -105,5 +84,31 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
return value;
}
private IJsonValue CleanIds()
{
if (value is JsonArray array)
{
var result = new JsonArray(array);
for (var i = 0; i < result.Count; i++)
{
if (!IsValidReference(result[i]))
{
result.RemoveAt(i);
i--;
}
}
return result;
}
return value;
}
private bool IsValidReference(IJsonValue item)
{
return item is JsonString s && Guid.TryParse(s.Value, out var guid) && validIds.Contains(guid);
}
}
}

41
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs

@ -7,51 +7,16 @@
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ReferencesExtensions
{
public static IEnumerable<Guid> GetReferencedIds(this IField field, IJsonValue? value, Ids strategy = Ids.All)
{
return ReferencesExtractor.ExtractReferences(field, value, strategy);
}
public static IJsonValue CleanReferences(this IField field, IJsonValue value, ICollection<Guid>? oldReferences)
{
if (IsNull(value))
{
return value;
}
return ReferencesCleaner.CleanReferences(field, value, oldReferences);
}
private static bool IsNull(IJsonValue value)
{
return value == null || value.Type == JsonValueType.Null;
}
public static JsonArray ToJsonArray(this HashSet<Guid> ids)
{
var result = JsonValue.Array();
foreach (var id in ids)
{
result.Add(JsonValue.Create(id.ToString()));
}
return result;
}
public static HashSet<Guid> ToGuidSet(this IJsonValue? value)
public static void AddIds(this IJsonValue? value, HashSet<Guid> result)
{
if (value is JsonArray array)
{
var result = new HashSet<Guid>();
foreach (var id in array)
{
if (id.Type == JsonValueType.String && Guid.TryParse(id.ToString(), out var guid))
@ -59,11 +24,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
result.Add(guid);
}
}
return result;
}
return new HashSet<Guid>();
}
}
}

84
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs

@ -9,31 +9,35 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public sealed class ReferencesExtractor : IFieldVisitor<IEnumerable<Guid>>
internal sealed class ReferencesExtractor : IFieldVisitor<None>
{
private readonly IJsonValue? value;
private readonly Ids strategy;
private readonly HashSet<Guid> result;
private IJsonValue? value;
private ReferencesExtractor(IJsonValue? value, Ids strategy)
public HashSet<Guid> Result
{
this.value = value;
this.strategy = strategy;
get { return result; }
}
public static IEnumerable<Guid> ExtractReferences(IField field, IJsonValue? value, Ids strategy)
public ReferencesExtractor(HashSet<Guid> result)
{
return field.Accept(new ReferencesExtractor(value, strategy));
Guard.NotNull(result);
this.result = result;
}
public IEnumerable<Guid> Visit(IArrayField field)
public void SetValue(IJsonValue? newValue)
{
var result = new List<Guid>();
value = newValue;
}
public None Visit(IArrayField field)
{
if (value is JsonArray array)
{
foreach (var item in array.OfType<JsonObject>())
@ -42,75 +46,69 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
if (item.TryGetValue(nestedField.Name, out var nestedValue))
{
result.AddRange(nestedField.Accept(new ReferencesExtractor(nestedValue, strategy)));
SetValue(nestedValue);
nestedField.Accept(this);
}
}
}
}
return result;
return None.Value;
}
public IEnumerable<Guid> Visit(IField<AssetsFieldProperties> field)
public None Visit(IField<AssetsFieldProperties> field)
{
var ids = value.ToGuidSet();
value.AddIds(result);
return ids;
return None.Value;
}
public IEnumerable<Guid> Visit(IField<ReferencesFieldProperties> field)
public None Visit(IField<ReferencesFieldProperties> field)
{
var ids = value.ToGuidSet();
if (strategy == Ids.All && field.Properties.SchemaIds != null)
{
foreach (var schemaId in field.Properties.SchemaIds)
{
ids.Add(schemaId);
}
}
value.AddIds(result);
return ids;
return None.Value;
}
public IEnumerable<Guid> Visit(IField<BooleanFieldProperties> field)
public None Visit(IField<BooleanFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<DateTimeFieldProperties> field)
public None Visit(IField<DateTimeFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<GeolocationFieldProperties> field)
public None Visit(IField<GeolocationFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<JsonFieldProperties> field)
public None Visit(IField<JsonFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<NumberFieldProperties> field)
public None Visit(IField<NumberFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<StringFieldProperties> field)
public None Visit(IField<StringFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<TagsFieldProperties> field)
public None Visit(IField<TagsFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
public IEnumerable<Guid> Visit(IField<UIFieldProperties> field)
public None Visit(IField<UIFieldProperties> field)
{
return Enumerable.Empty<Guid>();
return None.Value;
}
}
}

15
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs

@ -14,18 +14,25 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ValueReferencesConverter
{
public static ValueConverter CleanReferences(IEnumerable<Guid> deletedReferencedIds)
public static ValueConverter CleanReferences(HashSet<Guid>? validIds = null)
{
var ids = new HashSet<Guid>(deletedReferencedIds);
if (validIds == null || validIds.Count == 0)
{
return (value, field) => value;
}
var cleaner = new ReferencesCleaner(validIds);
return (value, field) =>
{
if (value.Type == JsonValueType.Null)
{
return value;
return value!;
}
return field.CleanReferences(value, ids);
cleaner.SetValue(value);
return field.Accept(cleaner);
};
}
}

12
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/Ids.cs → backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonError.cs

@ -5,11 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public enum Ids
public sealed class JsonError
{
All,
ContentOnly
public string Error { get; }
public JsonError(string error)
{
Error = error;
}
}
}

96
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class JsonValueConverter : IFieldVisitor<object>
public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonError? Error)>
{
private readonly IJsonValue value;
@ -23,67 +23,67 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
this.value = value;
}
public static object ConvertValue(IField field, IJsonValue json)
public static (object? Result, JsonError? Error) ConvertValue(IField field, IJsonValue json)
{
return field.Accept(new JsonValueConverter(json));
}
public object Visit(IArrayField field)
public (object? Result, JsonError? Error) Visit(IArrayField field)
{
return ConvertToObjectList();
}
public object Visit(IField<AssetsFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<AssetsFieldProperties> field)
{
return ConvertToGuidList();
}
public object Visit(IField<ReferencesFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<ReferencesFieldProperties> field)
{
return ConvertToGuidList();
}
public object Visit(IField<TagsFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<TagsFieldProperties> field)
{
return ConvertToStringList();
}
public object Visit(IField<BooleanFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<BooleanFieldProperties> field)
{
if (value is JsonScalar<bool> b)
{
return b.Value;
return (b.Value, null);
}
throw new InvalidCastException("Invalid json type, expected boolean.");
return (null, new JsonError("Invalid json type, expected boolean."));
}
public object Visit(IField<NumberFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<NumberFieldProperties> field)
{
if (value is JsonScalar<double> b)
if (value is JsonScalar<double> n)
{
return b.Value;
return (n.Value, null);
}
throw new InvalidCastException("Invalid json type, expected number.");
return (null, new JsonError("Invalid json type, expected number."));
}
public object Visit(IField<StringFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<StringFieldProperties> field)
{
if (value is JsonScalar<string> b)
if (value is JsonScalar<string> s)
{
return b.Value;
return (s.Value, null);
}
throw new InvalidCastException("Invalid json type, expected string.");
return (null, new JsonError("Invalid json type, expected string."));
}
public object Visit(IField<UIFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<UIFieldProperties> field)
{
return value;
return (value, null);
}
public object Visit(IField<DateTimeFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<DateTimeFieldProperties> field)
{
if (value.Type == JsonValueType.String)
{
@ -91,16 +91,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (!parseResult.Success)
{
throw parseResult.Exception;
return (null, new JsonError(parseResult.Exception.Message));
}
return parseResult.Value;
return (parseResult.Value, null);
}
throw new InvalidCastException("Invalid json type, expected string.");
return (null, new JsonError("Invalid json type, expected string."));
}
public object Visit(IField<GeolocationFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<GeolocationFieldProperties> field)
{
if (value is JsonObject geolocation)
{
@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(propertyName, "longitude", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidCastException("Geolocation can only have latitude and longitude property.");
return (null, new JsonError("Geolocation can only have latitude and longitude property."));
}
}
@ -119,12 +119,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (!lat.IsBetween(-90, 90))
{
throw new InvalidCastException("Latitude must be between -90 and 90.");
return (null, new JsonError("Latitude must be between -90 and 90."));
}
}
else
{
throw new InvalidCastException("Invalid json type, expected latitude/longitude object.");
return (null, new JsonError("Invalid json type, expected latitude/longitude object."));
}
if (geolocation.TryGetValue("longitude", out var lonValue) && lonValue is JsonScalar<double> lonNumber)
@ -133,30 +133,30 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (!lon.IsBetween(-180, 180))
{
throw new InvalidCastException("Longitude must be between -180 and 180.");
return (null, new JsonError("Longitude must be between -180 and 180."));
}
}
else
{
throw new InvalidCastException("Invalid json type, expected latitude/longitude object.");
return (null, new JsonError("Invalid json type, expected latitude/longitude object."));
}
return value;
return (value, null);
}
throw new InvalidCastException("Invalid json type, expected latitude/longitude object.");
return (null, new JsonError("Invalid json type, expected latitude/longitude object."));
}
public object Visit(IField<JsonFieldProperties> field)
public (object? Result, JsonError? Error) Visit(IField<JsonFieldProperties> field)
{
return value;
return (value, null);
}
private object ConvertToGuidList()
private (object? Result, JsonError? Error) ConvertToGuidList()
{
if (value is JsonArray array)
{
var result = new List<Guid>();
var result = new List<Guid>(array.Count);
foreach (var item in array)
{
@ -166,21 +166,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
}
else
{
throw new InvalidCastException("Invalid json type, expected array of guid strings.");
return (null, new JsonError("Invalid json type, expected array of guid strings."));
}
}
return result;
return (result, null);
}
throw new InvalidCastException("Invalid json type, expected array of guid strings.");
return (null, new JsonError("Invalid json type, expected array of guid strings."));
}
private object ConvertToStringList()
private (object? Result, JsonError? Error) ConvertToStringList()
{
if (value is JsonArray array)
{
var result = new List<string?>();
var result = new List<string?>(array.Count);
foreach (var item in array)
{
@ -194,21 +194,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
}
else
{
throw new InvalidCastException("Invalid json type, expected array of strings.");
return (null, new JsonError("Invalid json type, expected array of strings."));
}
}
return result;
return (result, null);
}
throw new InvalidCastException("Invalid json type, expected array of strings.");
return (null, new JsonError("Invalid json type, expected array of strings."));
}
private object ConvertToObjectList()
private (object? Result, JsonError? Error) ConvertToObjectList()
{
if (value is JsonArray array)
{
var result = new List<JsonObject>();
var result = new List<JsonObject>(array.Count);
foreach (var item in array)
{
@ -218,14 +218,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
}
else
{
throw new InvalidCastException("Invalid json type, expected array of objects.");
return (null, new JsonError("Invalid json type, expected array of objects."));
}
}
return result;
return (result, null);
}
throw new InvalidCastException("Invalid json type, expected array of objects.");
return (null, new JsonError("Invalid json type, expected array of objects."));
}
}
}

11
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs

@ -42,7 +42,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
else
{
typedValue = JsonValueConverter.ConvertValue(field, jsonValue);
var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue);
if (error != null)
{
addError(context.Path, error.Error);
}
else
{
typedValue = json;
}
}
}

41
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -43,13 +43,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.ParentId)
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Slug))
.Ascending(x => x.Slug)),
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileHash)),
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Id))
}, ct);
}
@ -89,15 +98,27 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids)
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, HashSet<Guid> ids)
{
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByIds"))
{
var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified);
var assetEntities =
await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id)
.ToListAsync();
var assetItems = await find.ToListAsync();
return assetEntities.Select(x => Guid.Parse(x["_si"].AsString)).ToList();
}
}
return ResultList.Create(assetItems.Count, assetItems.OfType<IAssetEntity>());
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids)
{
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByIds"))
{
var assetEntities =
await Collection.Find(BuildFilter(appId, ids)).SortByDescending(x => x.LastModified)
.ToListAsync();
return ResultList.Create(assetEntities.Count, assetEntities.OfType<IAssetEntity>());
}
}
@ -136,5 +157,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return assetEntity;
}
}
private static FilterDefinition<MongoAssetEntity> BuildFilter(Guid appId, HashSet<Guid> ids)
{
return Filter.And(
Filter.Eq(x => x.IndexedAppId, appId),
Filter.In(x => x.Id, ids),
Filter.Ne(x => x.IsDeleted, true));
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -19,6 +19,7 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
[BsonIgnoreExtraElements]
public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Guid>
{
private NamedContentData? data;
@ -42,12 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonRequired]
[BsonElement("rf")]
[BsonRepresentation(BsonType.String)]
public List<Guid>? ReferencedIds { get; set; }
[BsonRequired]
[BsonElement("rd")]
[BsonRepresentation(BsonType.String)]
public List<Guid> ReferencedIdsDeleted { get; set; } = new List<Guid>();
public HashSet<Guid>? ReferencedIds { get; set; }
[BsonRequired]
[BsonElement("ss")]
@ -122,11 +118,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public void ParseData(Schema schema, IJsonSerializer serializer)
{
data = DataByIds?.FromMongoModel(schema, ReferencedIdsDeleted, serializer);
data = DataByIds?.FromMongoModel(schema, serializer);
if (DataDraftByIds != null)
{
dataDraft = DataDraftByIds.FromMongoModel(schema, ReferencedIdsDeleted, serializer);
dataDraft = DataDraftByIds.FromMongoModel(schema, serializer);
}
}
}

3
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -36,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private readonly IJsonSerializer serializer;
private readonly string typeAssetDeleted;
private readonly string typeContentDeleted;
private readonly CleanupReferences cleanupReferences;
private readonly QueryContent queryContentAsync;
private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery;
@ -59,7 +58,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
this.serializer = serializer;
cleanupReferences = new CleanupReferences();
queryContentAsync = new QueryContent(serializer);
queryContentsById = new QueryContentsByIds(serializer, appProvider);
queryContentsByQuery = new QueryContentsByQuery(serializer, indexer);
@ -72,7 +70,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
await cleanupReferences.PrepareAsync(collection, ct);
await queryContentAsync.PrepareAsync(collection, ct);
await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct);

52
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -1,52 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public partial class MongoContentRepository : IEventConsumer
{
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^(content-)|(asset-)"; }
}
public bool Handles(StoredEvent @event)
{
return @event.Data.Type == typeAssetDeleted || @event.Data.Type == typeContentDeleted;
}
public Task On(Envelope<IEvent> @event)
{
switch (@event.Payload)
{
case AssetDeleted e:
return cleanupReferences.DoAsync(e.AssetId);
case ContentDeleted e:
return cleanupReferences.DoAsync(e.ContentId);
}
return TaskHelper.Done;
}
Task IEventConsumer.ClearAsync()
{
return TaskHelper.Done;
}
}
}

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -9,6 +9,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
@ -66,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id);
var idData = value.Data!.ToMongoModel(schema.SchemaDef, serializer);
var idData = value.Data.ToMongoModel(schema.SchemaDef, serializer);
var idDraftData = idData;
if (!ReferenceEquals(value.Data, value.DataDraft))
@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
IsDeleted = value.IsDeleted,
IndexedAppId = value.AppId.Id,
IndexedSchemaId = value.SchemaId.Id,
ReferencedIds = idData.ToReferencedIds(schema.SchemaDef),
ReferencedIds = value.Data.GetReferencedIds(schema.SchemaDef),
ScheduledAt = value.ScheduleJob?.DueTime,
Version = newVersion
});

35
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/CleanupReferences.cs

@ -1,35 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class CleanupReferences : OperationBase
{
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
new CreateIndexModel<MongoContentEntity>(
Index.Ascending(x => x.ReferencedIds));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public Task DoAsync(Guid id)
{
return Collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, id),
Filter.AnyNe(x => x.ReferencedIdsDeleted, id)),
Update.AddToSet(x => x.ReferencedIdsDeleted, id));
}
}
}

15
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs

@ -6,12 +6,10 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb;
@ -20,20 +18,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public static class Extensions
{
public static List<Guid> ToReferencedIds(this IdContentData data, Schema schema)
{
return data.GetReferencedIds(schema).Distinct().ToList();
}
public static NamedContentData FromMongoModel(this IdContentData result, Schema schema, List<Guid> deletedIds, IJsonSerializer serializer)
public static NamedContentData FromMongoModel(this IdContentData result, Schema schema, IJsonSerializer serializer)
{
return result.ConvertId2Name(schema,
FieldConverters.ForValues(
ValueConverters.DecodeJson(serializer),
ValueReferencesConverter.CleanReferences(deletedIds)),
ValueConverters.DecodeJson(serializer)),
FieldConverters.ForNestedId2Name(
ValueConverters.DecodeJson(serializer),
ValueReferencesConverter.CleanReferences(deletedIds)));
ValueConverters.DecodeJson(serializer)));
}
public static IdContentData ToMongoModel(this NamedContentData result, Schema schema, IJsonSerializer serializer)

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
try
{
var ctx = contextProvider.Context.Clone().WithNoAssetEnrichment();
var ctx = contextProvider.Context.Clone().WithoutAssetEnrichment();
var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash);

6
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs

@ -11,12 +11,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
private const string HeaderNoEnrichment = "X-NoAssetEnrichment";
public static bool IsNoAssetEnrichment(this Context context)
public static bool ShouldEnrichAsset(this Context context)
{
return context.Headers.ContainsKey(HeaderNoEnrichment);
return !context.Headers.ContainsKey(HeaderNoEnrichment);
}
public static Context WithNoAssetEnrichment(this Context context, bool value = true)
public static Context WithoutAssetEnrichment(this Context context, bool value = true)
{
if (value)
{

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs

@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private static bool ShouldEnrich(Context context)
{
return !context.IsNoAssetEnrichment();
return context.ShouldEnrichAsset();
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity?> FindAssetAsync(Guid id);
Task<IAssetEntity?> FindAssetBySlugAsync(Guid appId, string slug);

1
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs

@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
});
}
await ctx.EnrichAsync(c.Data);
await ctx.GenerateDefaultValuesAsync(c.Data);
if (!c.DoNotValidate)
{

6
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -9,7 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.EnrichContent;
using Squidex.Domain.Apps.Core.DefaultValues;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return context;
}
public Task EnrichAsync(NamedContentData data)
public Task GenerateDefaultValuesAsync(NamedContentData data)
{
data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver());
data.GenerateDefaultValues(schemaEntity.SchemaDef, appEntity.PartitionResolver());
return TaskHelper.Done;
}

94
backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs

@ -21,101 +21,67 @@ namespace Squidex.Domain.Apps.Entities.Contents
private const string HeaderResolveAssetUrls = "X-Resolve-Urls";
private const string HeaderNoResolveLanguages = "X-NoResolveLanguages";
private const string HeaderNoEnrichment = "X-NoEnrichment";
private const string HeaderNoCleanup = "X-NoCleanup";
private static readonly char[] Separators = { ',', ';' };
public static bool IsNoEnrichment(this Context context)
public static bool ShouldCleanup(this Context context)
{
return context.Headers.ContainsKey(HeaderNoEnrichment);
return !context.Headers.ContainsKey(HeaderNoCleanup);
}
public static Context WithoutContentEnrichment(this Context context, bool value = true)
public static Context WithoutCleanup(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderNoEnrichment] = "1";
}
else
{
context.Headers.Remove(HeaderNoEnrichment);
}
return SetBoolean(context, HeaderNoCleanup, value);
}
return context;
public static bool ShouldEnrichContent(this Context context)
{
return !context.Headers.ContainsKey(HeaderNoEnrichment);
}
public static bool IsUnpublished(this Context context)
public static Context WithoutContentEnrichment(this Context context, bool value = true)
{
return SetBoolean(context, HeaderNoEnrichment, value);
}
public static bool ShouldProvideUnpublished(this Context context)
{
return context.Headers.ContainsKey(HeaderUnpublished);
}
public static Context WithUnpublished(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderUnpublished] = "1";
}
else
{
context.Headers.Remove(HeaderUnpublished);
}
return context;
return SetBoolean(context, HeaderUnpublished, value);
}
public static bool IsFlatten(this Context context)
public static bool ShouldFlatten(this Context context)
{
return context.Headers.ContainsKey(HeaderFlatten);
}
public static Context WithFlatten(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderFlatten] = "1";
}
else
{
context.Headers.Remove(HeaderFlatten);
}
return context;
return SetBoolean(context, HeaderFlatten, value);
}
public static bool IsResolveFlow(this Context context)
public static bool ShouldResolveFlow(this Context context)
{
return context.Headers.ContainsKey(HeaderResolveFlow);
}
public static Context WithResolveFlow(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderResolveFlow] = "1";
}
else
{
context.Headers.Remove(HeaderResolveFlow);
}
return context;
return SetBoolean(context, HeaderResolveFlow, value);
}
public static bool IsNoResolveLanguages(this Context context)
public static bool ShouldResolveLanguages(this Context context)
{
return context.Headers.ContainsKey(HeaderNoResolveLanguages);
return !context.Headers.ContainsKey(HeaderNoResolveLanguages);
}
public static Context WithoutResolveLanguages(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderNoResolveLanguages] = "1";
}
else
{
context.Headers.Remove(HeaderNoResolveLanguages);
}
return context;
return SetBoolean(context, HeaderNoResolveLanguages, value);
}
public static IEnumerable<string> AssetUrls(this Context context)
@ -175,5 +141,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
return context;
}
private static Context SetBoolean(Context context, string key, bool value)
{
if (value)
{
context.Headers[key] = "1";
}
else
{
context.Headers.Remove(key);
}
return context;
}
}
}

316
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -9,15 +9,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
@ -25,30 +18,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public sealed class ContentEnricher : IContentEnricher
{
private const string DefaultColor = StatusColors.Draft;
private static readonly ILookup<Guid, IEnrichedContentEntity> EmptyContents = Enumerable.Empty<IEnrichedContentEntity>().ToLookup(x => x.Id);
private static readonly ILookup<Guid, IEnrichedAssetEntity> EmptyAssets = Enumerable.Empty<IEnrichedAssetEntity>().ToLookup(x => x.Id);
private readonly IAssetQueryService assetQuery;
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IEnumerable<IContentEnricherStep> steps;
private readonly Lazy<IContentQueryService> contentQuery;
private readonly IContentWorkflow contentWorkflow;
private IContentQueryService ContentQuery
{
get { return contentQuery.Value; }
}
public ContentEnricher(IAssetQueryService assetQuery, IAssetUrlGenerator assetUrlGenerator, Lazy<IContentQueryService> contentQuery, IContentWorkflow contentWorkflow)
public ContentEnricher(IEnumerable<IContentEnricherStep> steps, Lazy<IContentQueryService> contentQuery)
{
Guard.NotNull(assetQuery);
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(steps);
Guard.NotNull(contentQuery);
Guard.NotNull(contentWorkflow);
this.assetQuery = assetQuery;
this.assetUrlGenerator = assetUrlGenerator;
this.steps = steps;
this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow;
}
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, Context context)
@ -73,308 +58,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (contents.Any())
{
var cache = new Dictionary<(Guid, Status), StatusInfo>();
foreach (var content in contents)
{
var result = SimpleMapper.Map(content, new ContentEntity());
await EnrichColorAsync(content, result, cache);
if (ShouldEnrichWithStatuses(context))
{
await EnrichNextsAsync(content, result, context);
await EnrichCanUpdateAsync(content, result, context);
}
results.Add(result);
}
foreach (var group in results.GroupBy(x => x.SchemaId.Id))
{
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
foreach (var content in group)
{
content.CacheDependencies = new HashSet<object?>
{
app.Id,
app.Version,
schema.Id,
schema.Version
};
}
if (ShouldEnrichWithSchema(context))
{
var referenceFields = schema.SchemaDef.ReferencesFields().ToArray();
var schemaName = schema.SchemaDef.Name;
var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged();
foreach (var content in group)
{
content.ReferenceFields = referenceFields;
content.SchemaName = schemaName;
content.SchemaDisplayName = schemaDisplayName;
}
}
}
if (ShouldEnrich(context))
{
await EnrichReferencesAsync(context, results);
await EnrichAssetsAsync(context, results);
}
}
return results;
}
}
private async Task EnrichAssetsAsync(Context context, List<ContentEntity> contents)
{
var ids = new HashSet<Guid>();
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
AddAssetIds(ids, schema, group);
}
var assets = await GetAssetsAsync(context, ids);
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
ResolveAssets(schema, group, assets);
}
}
private async Task EnrichReferencesAsync(Context context, List<ContentEntity> contents)
{
var ids = new HashSet<Guid>();
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
AddReferenceIds(ids, schema, group);
}
var references = await GetReferencesAsync(context, ids);
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
await ResolveReferencesAsync(context, schema, group, references);
}
}
private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable<ContentEntity> contents, ILookup<Guid, IEnrichedContentEntity> references)
{
var formatted = new Dictionary<IContentEntity, JsonObject>();
foreach (var field in schema.SchemaDef.ResolvingReferences())
{
foreach (var content in contents)
{
if (content.ReferenceData == null)
{
content.ReferenceData = new NamedContentData();
}
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
try
{
if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{
foreach (var (partition, partitionValue) in fieldData)
{
var referencedContents =
field.GetReferencedIds(partitionValue, Ids.ContentOnly)
.Select(x => references[x])
.SelectMany(x => x)
.ToList();
if (referencedContents.Count == 1)
{
var reference = referencedContents[0];
var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString());
content.CacheDependencies.Add(referencedSchema.Id);
content.CacheDependencies.Add(referencedSchema.Version);
content.CacheDependencies.Add(reference.Id);
content.CacheDependencies.Add(reference.Version);
var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema));
fieldReference.AddJsonValue(partition, value);
}
else if (referencedContents.Count > 1)
{
var value = CreateFallback(context, referencedContents);
fieldReference.AddJsonValue(partition, value);
}
}
}
}
catch (DomainObjectNotFoundException)
{
continue;
}
}
}
}
var schemaCache = new Dictionary<Guid, Task<ISchemaEntity>>();
private void ResolveAssets(ISchemaEntity schema, IGrouping<Guid, ContentEntity> contents, ILookup<Guid, IEnrichedAssetEntity> assets)
{
foreach (var field in schema.SchemaDef.ResolvingAssets())
{
foreach (var content in contents)
{
if (content.ReferenceData == null)
Task<ISchemaEntity> GetSchema(Guid id)
{
content.ReferenceData = new NamedContentData();
return schemaCache.GetOrAdd(id, x => ContentQuery.GetSchemaOrThrowAsync(context, x.ToString()));
}
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null)
foreach (var step in steps)
{
foreach (var (partitionKey, partitionValue) in fieldData)
using (Profiler.TraceMethod(step.ToString()!))
{
var referencedImage =
field.GetReferencedIds(partitionValue, Ids.ContentOnly)
.Select(x => assets[x])
.SelectMany(x => x)
.FirstOrDefault(x => x.Type == AssetType.Image);
if (referencedImage != null)
{
var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString());
content.CacheDependencies.Add(referencedImage.Id);
content.CacheDependencies.Add(referencedImage.Version);
fieldReference.AddJsonValue(partitionKey, JsonValue.Create(url));
}
await step.EnrichAsync(context, results, GetSchema);
}
}
}
}
}
private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema)
{
return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig);
}
private static JsonObject CreateFallback(Context context, List<IEnrichedContentEntity> referencedContents)
{
var text = $"{referencedContents.Count} Reference(s)";
var value = JsonValue.Object();
foreach (var partitionKey in context.App.LanguagesConfig.AllKeys)
{
value.Add(partitionKey, text);
}
return value;
}
private void AddReferenceIds(HashSet<Guid> ids, ISchemaEntity schema, IEnumerable<ContentEntity> contents)
{
foreach (var content in contents)
{
ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly));
}
}
private void AddAssetIds(HashSet<Guid> ids, ISchemaEntity schema, IEnumerable<ContentEntity> contents)
{
foreach (var content in contents)
{
ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly));
}
}
private async Task<ILookup<Guid, IEnrichedContentEntity>> GetReferencesAsync(Context context, HashSet<Guid> ids)
{
if (ids.Count == 0)
{
return EmptyContents;
}
var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), ids.ToList());
return references.ToLookup(x => x.Id);
}
private async Task<ILookup<Guid, IEnrichedAssetEntity>> GetAssetsAsync(Context context, HashSet<Guid> ids)
{
if (ids.Count == 0)
{
return EmptyAssets;
}
var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), null, Q.Empty.WithIds(ids));
return assets.ToLookup(x => x.Id);
}
private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result, Context context)
{
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content, context.User);
}
private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context)
{
result.Nexts = await contentWorkflow.GetNextsAsync(content, context.User);
}
private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache)
{
result.StatusColor = await GetColorAsync(content, cache);
}
private async Task<string> GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache)
{
if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info))
{
info = await contentWorkflow.GetInfoAsync(content);
if (info == null)
{
info = new StatusInfo(content.Status, DefaultColor);
}
cache[(content.SchemaId.Id, content.Status)] = info;
return results;
}
return info.Color;
}
private static bool ShouldEnrichWithSchema(Context context)
{
return context.IsFrontendClient;
}
private static bool ShouldEnrichWithStatuses(Context context)
{
return context.IsFrontendClient || context.IsResolveFlow();
}
private static bool ShouldEnrich(Context context)
{
return context.IsFrontendClient && !context.IsNoEnrichment();
}
}
}

68
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
@ -29,7 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static readonly Status[] StatusPublishedOnly = { Status.Published };
private static readonly IResultList<IEnrichedContentEntity> EmptyContents = ResultList.CreateFrom<IEnrichedContentEntity>(0);
private readonly IAppProvider appProvider;
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IContentEnricher contentEnricher;
private readonly IContentRepository contentRepository;
private readonly IContentLoader contentVersionLoader;
@ -38,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public ContentQueryService(
IAppProvider appProvider,
IAssetUrlGenerator assetUrlGenerator,
IContentEnricher contentEnricher,
IContentRepository contentRepository,
IContentLoader contentVersionLoader,
@ -46,7 +43,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
ContentQueryParser queryParser)
{
Guard.NotNull(appProvider);
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(contentEnricher);
Guard.NotNull(contentRepository);
Guard.NotNull(contentVersionLoader);
@ -54,7 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Guard.NotNull(scriptEngine);
this.appProvider = appProvider;
this.assetUrlGenerator = assetUrlGenerator;
this.contentEnricher = contentEnricher;
this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader;
@ -169,8 +164,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var results = new List<IEnrichedContentEntity>();
var converters = GenerateConverters(context).ToArray();
var script = schema.SchemaDef.Scripts.Query;
var scripting = !string.IsNullOrWhiteSpace(script);
@ -180,25 +173,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var result = SimpleMapper.Map(content, new ContentEntity());
if (result.Data != null)
if (result.Data != null && !context.IsFrontendClient && scripting)
{
if (!context.IsFrontendClient && scripting)
{
var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id };
result.Data = scriptEngine.Transform(ctx, script);
}
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
}
var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id };
if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient))
{
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
else
{
result.DataDraft = null!;
result.Data = scriptEngine.Transform(ctx, script);
}
results.Add(result);
@ -208,43 +187,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
}
private IEnumerable<FieldConverter> GenerateConverters(Context context)
{
if (!context.IsFrontendClient)
{
yield return FieldConverters.ExcludeHidden();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden());
}
yield return FieldConverters.ExcludeChangedTypes();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes());
yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig);
yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig);
if (!context.IsFrontendClient)
{
if (!context.IsNoResolveLanguages())
{
yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig);
}
var languages = context.Languages();
if (languages.Any())
{
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages);
}
var assetUrls = context.AssetUrls();
if (assetUrls.Any())
{
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator);
}
}
}
public async Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName)
{
ISchemaEntity? schema = null;
@ -287,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static Status[]? GetStatus(Context context)
{
if (context.IsFrontendClient || context.IsUnpublished())
if (context.IsFrontendClient || context.ShouldProvideUnpublished())
{
return null;
}
@ -338,7 +280,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static bool WithDraft(Context context)
{
return context.IsUnpublished() || context.IsFrontendClient;
return context.ShouldProvideUnpublished() || context.IsFrontendClient;
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs

@ -8,7 +8,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public interface IContentEnricher
{

21
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public delegate Task<ISchemaEntity> ProvideSchema(Guid id);
public interface IContentEnricherStep
{
Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas);
}
}

158
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs

@ -0,0 +1,158 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class ConvertData : IContentEnricherStep
{
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
public ConvertData(IAssetUrlGenerator assetUrlGenerator, IAssetRepository assetRepository, IContentRepository contentRepository)
{
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(assetRepository);
Guard.NotNull(contentRepository);
this.assetUrlGenerator = assetUrlGenerator;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
var resolveDataDraft = context.ShouldProvideUnpublished() || context.IsFrontendClient;
var referenceCleaner = await CleanReferencesAsync(context, contents, schemas);
var converters = GenerateConverters(context, referenceCleaner).ToArray();
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
foreach (var content in group)
{
if (content.Data != null)
{
content.Data = content.Data.ConvertName2Name(schema.SchemaDef, converters);
}
if (content.DataDraft != null && resolveDataDraft)
{
content.DataDraft = content.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
else
{
content.DataDraft = null!;
}
}
}
}
private async Task<ValueConverter?> CleanReferencesAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
if (context.ShouldCleanup())
{
var ids = new HashSet<Guid>();
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
foreach (var content in group)
{
content.Data?.AddReferencedIds(schema.SchemaDef, ids);
content.DataDraft?.AddReferencedIds(schema.SchemaDef, ids);
}
}
if (ids.Count > 0)
{
var taskForAssets = QueryAssetIdsAsync(context, ids);
var taskForContents = QueryContentIdsAsync(context, ids);
await Task.WhenAll(taskForAssets, taskForContents);
var foundIds = new HashSet<Guid>(taskForAssets.Result.Union(taskForContents.Result));
return ValueReferencesConverter.CleanReferences(foundIds);
}
}
return null;
}
private async Task<IEnumerable<Guid>> QueryContentIdsAsync(Context context, HashSet<Guid> ids)
{
var result = await contentRepository.QueryIdsAsync(context.App.Id, ids);
return result.Select(x => x.Id);
}
private async Task<IEnumerable<Guid>> QueryAssetIdsAsync(Context context, HashSet<Guid> ids)
{
var result = await assetRepository.QueryIdsAsync(context.App.Id, ids);
return result;
}
private IEnumerable<FieldConverter> GenerateConverters(Context context, ValueConverter? cleanReferences)
{
if (!context.IsFrontendClient)
{
yield return FieldConverters.ExcludeHidden();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden());
}
yield return FieldConverters.ExcludeChangedTypes();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes());
if (cleanReferences != null)
{
yield return FieldConverters.ForValues(cleanReferences);
yield return FieldConverters.ForNestedName2Name(cleanReferences);
}
yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig);
yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig);
if (!context.IsFrontendClient)
{
if (context.ShouldResolveLanguages())
{
yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig);
}
var languages = context.Languages();
if (languages.Any())
{
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages);
}
var assetUrls = context.AssetUrls();
if (assetUrls.Any())
{
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator);
}
}
}
}
}

36
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class EnrichForCaching : IContentEnricherStep
{
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
var app = context.App;
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
foreach (var content in group)
{
content.CacheDependencies ??= new HashSet<object?>();
content.CacheDependencies.Add(app.Id);
content.CacheDependencies.Add(app.Version);
content.CacheDependencies.Add(schema.Id);
content.CacheDependencies.Add(schema.Version);
}
}
}
}
}

44
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class EnrichWithSchema : IContentEnricherStep
{
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
var schemaName = schema.SchemaDef.Name;
var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged();
foreach (var content in group)
{
content.SchemaName = schemaName;
content.SchemaDisplayName = schemaDisplayName;
}
if (context.IsFrontendClient)
{
var referenceFields = schema.SchemaDef.ReferencesFields().ToArray();
foreach (var content in group)
{
content.ReferenceFields = referenceFields;
}
}
}
}
}
}

82
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs

@ -0,0 +1,82 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class EnrichWithWorkflows : IContentEnricherStep
{
private const string DefaultColor = StatusColors.Draft;
private readonly IContentWorkflow contentWorkflow;
public EnrichWithWorkflows(IContentWorkflow contentWorkflow)
{
Guard.NotNull(contentWorkflow);
this.contentWorkflow = contentWorkflow;
}
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
var cache = new Dictionary<(Guid, Status), StatusInfo>();
foreach (var content in contents)
{
await EnrichColorAsync(content, content, cache);
if (ShouldEnrichWithStatuses(context))
{
await EnrichNextsAsync(content, context);
await EnrichCanUpdateAsync(content, context);
}
}
}
private async Task EnrichNextsAsync(ContentEntity content, Context context)
{
content.Nexts = await contentWorkflow.GetNextsAsync(content, context.User);
}
private async Task EnrichCanUpdateAsync( ContentEntity content, Context context)
{
content.CanUpdate = await contentWorkflow.CanUpdateAsync(content, context.User);
}
private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache)
{
result.StatusColor = await GetColorAsync(content, cache);
}
private async Task<string> GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache)
{
if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info))
{
info = await contentWorkflow.GetInfoAsync(content);
if (info == null)
{
info = new StatusInfo(content.Status, DefaultColor);
}
cache[(content.SchemaId.Id, content.Status)] = info;
}
return info.Color;
}
private static bool ShouldEnrichWithStatuses(Context context)
{
return context.IsFrontendClient || context.ShouldResolveFlow();
}
}
}

131
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs

@ -0,0 +1,131 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class ResolveAssets : IContentEnricherStep
{
private static readonly ILookup<Guid, IEnrichedAssetEntity> EmptyAssets = Enumerable.Empty<IEnrichedAssetEntity>().ToLookup(x => x.Id);
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IAssetQueryService assetQuery;
public ResolveAssets(IAssetUrlGenerator assetUrlGenerator, IAssetQueryService assetQuery)
{
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(assetQuery);
this.assetUrlGenerator = assetUrlGenerator;
this.assetQuery = assetQuery;
}
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
if (ShouldEnrich(context))
{
var ids = new HashSet<Guid>();
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
AddAssetIds(ids, schema, group);
}
var assets = await GetAssetsAsync(context, ids);
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
ResolveAssetsUrls(schema, group, assets);
}
}
}
private void ResolveAssetsUrls(ISchemaEntity schema, IGrouping<Guid, ContentEntity> contents, ILookup<Guid, IEnrichedAssetEntity> assets)
{
var temp = new HashSet<Guid>();
foreach (var field in schema.SchemaDef.ResolvingAssets())
{
foreach (var content in contents)
{
if (content.ReferenceData == null)
{
content.ReferenceData = new NamedContentData();
}
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{
foreach (var (partitionKey, partitionValue) in fieldData)
{
var referencedImage =
field.GetReferencedIds(partitionValue)
.Select(x => assets[x])
.SelectMany(x => x)
.FirstOrDefault(x => x.Type == AssetType.Image);
if (referencedImage != null)
{
var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString());
content.CacheDependencies ??= new HashSet<object?>();
content.CacheDependencies.Add(referencedImage.Id);
content.CacheDependencies.Add(referencedImage.Version);
fieldReference.AddJsonValue(partitionKey, JsonValue.Create(url));
}
}
}
}
}
}
private async Task<ILookup<Guid, IEnrichedAssetEntity>> GetAssetsAsync(Context context, HashSet<Guid> ids)
{
if (ids.Count == 0)
{
return EmptyAssets;
}
var assets = await assetQuery.QueryAsync(context.Clone().WithoutAssetEnrichment(true), null, Q.Empty.WithIds(ids));
return assets.ToLookup(x => x.Id);
}
private void AddAssetIds(HashSet<Guid> ids, ISchemaEntity schema, IEnumerable<ContentEntity> contents)
{
foreach (var content in contents)
{
content.DataDraft.AddReferencedIds(schema.SchemaDef.ResolvingAssets(), ids);
}
}
private static bool ShouldEnrich(Context context)
{
return context.IsFrontendClient && context.ShouldEnrichContent();
}
}
}

167
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs

@ -0,0 +1,167 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class ResolveReferences : IContentEnricherStep
{
private static readonly ILookup<Guid, IEnrichedContentEntity> EmptyContents = Enumerable.Empty<IEnrichedContentEntity>().ToLookup(x => x.Id);
private readonly Lazy<IContentQueryService> contentQuery;
private IContentQueryService ContentQuery
{
get { return contentQuery.Value; }
}
public ResolveReferences(Lazy<IContentQueryService> contentQuery)
{
Guard.NotNull(contentQuery);
this.contentQuery = contentQuery;
}
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
if (ShouldEnrich(context))
{
var ids = new HashSet<Guid>();
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
AddReferenceIds(ids, schema, group);
}
var references = await GetReferencesAsync(context, ids);
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var schema = await schemas(group.Key);
await ResolveReferencesAsync(context, schema, group, references, schemas);
}
}
}
private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable<ContentEntity> contents, ILookup<Guid, IEnrichedContentEntity> references, ProvideSchema schemas)
{
var formatted = new Dictionary<IContentEntity, JsonObject>();
foreach (var field in schema.SchemaDef.ResolvingReferences())
{
foreach (var content in contents)
{
if (content.ReferenceData == null)
{
content.ReferenceData = new NamedContentData();
}
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
try
{
if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{
foreach (var (partition, partitionValue) in fieldData)
{
var referencedContents =
field.GetReferencedIds(partitionValue)
.Select(x => references[x])
.SelectMany(x => x)
.ToList();
if (referencedContents.Count == 1)
{
var reference = referencedContents[0];
var referencedSchema = await schemas(reference.SchemaId.Id);
content.CacheDependencies ??= new HashSet<object?>();
content.CacheDependencies.Add(referencedSchema.Id);
content.CacheDependencies.Add(referencedSchema.Version);
content.CacheDependencies.Add(reference.Id);
content.CacheDependencies.Add(reference.Version);
var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema));
fieldReference.AddJsonValue(partition, value);
}
else if (referencedContents.Count > 1)
{
var value = CreateFallback(context, referencedContents);
fieldReference.AddJsonValue(partition, value);
}
}
}
}
catch (DomainObjectNotFoundException)
{
continue;
}
}
}
}
private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema)
{
return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig);
}
private static JsonObject CreateFallback(Context context, List<IEnrichedContentEntity> referencedContents)
{
var text = $"{referencedContents.Count} Reference(s)";
var value = JsonValue.Object();
foreach (var partitionKey in context.App.LanguagesConfig.AllKeys)
{
value.Add(partitionKey, text);
}
return value;
}
private void AddReferenceIds(HashSet<Guid> ids, ISchemaEntity schema, IEnumerable<ContentEntity> contents)
{
foreach (var content in contents)
{
content.DataDraft.AddReferencedIds(schema.SchemaDef.ResolvingReferences(), ids);
}
}
private async Task<ILookup<Guid, IEnrichedContentEntity>> GetReferencesAsync(Context context, HashSet<Guid> ids)
{
if (ids.Count == 0)
{
return EmptyContents;
}
var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), ids.ToList());
return references.ToLookup(x => x.Id);
}
private static bool ShouldEnrich(Context context)
{
return context.IsFrontendClient && context.ShouldEnrichContent();
}
}
}

5
backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs

@ -25,6 +25,11 @@ namespace Squidex.Infrastructure.Json.Objects
{
}
public JsonArray(JsonArray source)
: base(source)
{
}
internal JsonArray(params object?[] values)
: base(ToList(values))
{

40
backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs

@ -68,6 +68,8 @@ namespace Squidex.Infrastructure.Json.Objects
return Create(i);
case long l:
return Create(l);
case Guid g:
return Create(g);
case Instant i:
return Create(i);
}
@ -75,21 +77,24 @@ namespace Squidex.Infrastructure.Json.Objects
throw new ArgumentException("Invalid json type");
}
public static IJsonValue Create(bool value)
public static IJsonValue Create(Guid value)
{
return value ? True : False;
return Create(value.ToString());
}
public static IJsonValue Create(double value)
public static IJsonValue Create(Guid? value)
{
Guard.ValidNumber(value);
if (value == 0)
if (value == null)
{
return Zero;
return Null;
}
return new JsonNumber(value);
return Create(value.Value);
}
public static IJsonValue Create(Instant value)
{
return Create(value.ToString());
}
public static IJsonValue Create(Instant? value)
@ -99,7 +104,19 @@ namespace Squidex.Infrastructure.Json.Objects
return Null;
}
return Create(value.Value.ToString());
return Create(value.Value);
}
public static IJsonValue Create(double value)
{
Guard.ValidNumber(value);
if (value == 0)
{
return Zero;
}
return new JsonNumber(value);
}
public static IJsonValue Create(double? value)
@ -112,6 +129,11 @@ namespace Squidex.Infrastructure.Json.Objects
return Create(value.Value);
}
public static IJsonValue Create(bool value)
{
return value ? True : False;
}
public static IJsonValue Create(bool? value)
{
if (value == null)

2
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -110,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{
var response = SimpleMapper.Map(content, new ContentDto());
if (context.IsFlatten())
if (context.ShouldFlatten())
{
response.Data = content.Data?.ToFlatten();
response.DataDraft = content.DataDraft?.ToFlatten();

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

@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Infrastructure.EventSourcing;
@ -39,6 +40,24 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>();
services.AddSingletonAs<ConvertData>()
.As<IContentEnricherStep>();
services.AddSingletonAs<EnrichForCaching>()
.As<IContentEnricherStep>();
services.AddSingletonAs<EnrichWithSchema>()
.As<IContentEnricherStep>();
services.AddSingletonAs<EnrichWithWorkflows>()
.As<IContentEnricherStep>();
services.AddSingletonAs<ResolveAssets>()
.As<IContentEnricherStep>();
services.AddSingletonAs<ResolveReferences>()
.As<IContentEnricherStep>();
services.AddSingletonAs<ContentEnricher>()
.As<IContentEnricher>();

86
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs

@ -8,7 +8,6 @@
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
@ -149,90 +148,5 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
Assert.True(lhs.Equals((object)rhs));
Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode());
}
[Fact]
public void Should_extract_strings()
{
var input =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("en", "hello"))
.AddField("field2",
new ContentFieldData()
.AddValue("iv", "world"));
var result = input.ToFullText();
Assert.Equal("hello world", result);
}
[Fact]
public void Should_extract_strings_from_arrays()
{
var input =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("en", JsonValue.Array("hello", "loved")))
.AddField("field2",
new ContentFieldData()
.AddValue("iv", "world"));
var result = input.ToFullText();
Assert.Equal("hello loved world", result);
}
[Fact]
public void Should_extract_strings_from_objects()
{
var input =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("en", JsonValue.Array(JsonValue.Object().Add("p1", "hello"))))
.AddField("field2",
new ContentFieldData()
.AddValue("iv", "world"));
var result = input.ToFullText();
Assert.Equal("hello world", result);
}
[Fact]
public void Should_skip_long_strings()
{
var input =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("en", "hello"))
.AddField("field2",
new ContentFieldData()
.AddValue("iv", "you"));
var result = input.ToFullText(maxFieldLength: 3);
Assert.Equal("you", result);
}
[Fact]
public void Should_trim_long_results()
{
var input =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("en", "hello"))
.AddField("field2",
new ContentFieldData()
.AddValue("iv", "you"));
var result = input.ToFullText(maxTotalLength: 7);
Assert.Equal("hello y", result);
}
}
}

36
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input =
new ContentFieldData()
.AddValue("iv", JsonValue.Object());
.AddJsonValue(JsonValue.Object());
var actual = FieldConverters.ForValues((f, i) => Value.Unset)(input, field);
@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input =
new ContentFieldData()
.AddValue("iv", JsonValue.Object());
.AddJsonValue(JsonValue.Object());
var actual = FieldConverters.ForValues(ValueConverters.EncodeJson(TestUtils.DefaultSerializer))(input, field);
@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input =
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var expected =
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("1", 100)));
@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input =
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)
@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var expected =
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)));
@ -479,11 +479,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array("1", "2"));
var expected =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("url/to/1", "url/to/2"));
.AddJsonValue(JsonValue.Array("url/to/1", "url/to/2"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "assets" }), assetUrlGenerator)(source, field);
@ -499,13 +499,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("1", "2"))));
var expected =
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("url/to/1", "url/to/2"))));
@ -521,11 +521,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array("1", "2"));
var expected =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("url/to/1", "url/to/2"));
.AddJsonValue(JsonValue.Array("url/to/1", "url/to/2"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), assetUrlGenerator)(source, field);
@ -541,13 +541,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("1", "2"))));
var expected =
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("url/to/1", "url/to/2"))));
@ -563,11 +563,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array("1", "2"));
var expected =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array("1", "2"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "other" }), assetUrlGenerator)(source, field);
@ -581,11 +581,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array("1", "2"));
var expected =
new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array("1", "2"));
var result = FieldConverters.ResolveAssetUrls(null, assetUrlGenerator)(source, field);

14
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs → backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs

@ -8,7 +8,7 @@
using NodaTime;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.EnrichContent;
using Squidex.Domain.Apps.Core.DefaultValues;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -16,15 +16,15 @@ using Xunit;
#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions
namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
{
public class ContentEnrichmentTests
public class DefaultValuesTests
{
private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly Schema schema;
public ContentEnrichmentTests()
public DefaultValuesTests()
{
schema =
new Schema("my-schema")
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
new ContentFieldData()
.AddValue("iv", 456));
data.Enrich(schema, languagesConfig.ToResolver());
data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
Assert.Equal(456, ((JsonScalar<double>)data["my-number"]!["iv"]).Value);
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
new ContentFieldData()
.AddValue("iv", 456));
data.Enrich(schema, languagesConfig.ToResolver());
data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
Assert.Equal("en-string", data["my-string"]!["de"].ToString());
Assert.Equal("en-string", data["my-string"]!["en"].ToString());
@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
Fields.DateTime(1, "1", Partitioning.Invariant,
new DateTimeFieldProperties { DefaultValue = FutureDays(15) });
Assert.Equal(JsonValue.Create(FutureDays(15).ToString()), DefaultValueFactory.CreateDefaultValue(field, now));
Assert.Equal(JsonValue.Create(FutureDays(15)), DefaultValueFactory.CreateDefaultValue(field, now));
}
[Fact]

289
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
@ -21,39 +22,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
{
public class ReferenceExtractionTests
{
private readonly Guid schemaId = Guid.NewGuid();
private readonly Schema schema;
public ReferenceExtractionTests()
{
schema =
new Schema("my-schema")
.AddNumber(1, "field1", Partitioning.Language)
.AddNumber(2, "field2", Partitioning.Invariant)
.AddNumber(3, "field3", Partitioning.Invariant)
.AddAssets(5, "assets1", Partitioning.Invariant)
.AddAssets(6, "assets2", Partitioning.Invariant)
.AddArray(7, "array", Partitioning.Invariant, a => a
.AddAssets(71, "assets71"))
.AddJson(4, "json", Partitioning.Language)
.UpdateField(3, f => f.Hide());
}
[Fact]
public void Should_get_ids_from_id_data()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var input =
new IdContentData()
.AddField(5,
new ContentFieldData()
.AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString())));
var ids = input.GetReferencedIds(schema).ToArray();
Assert.Equal(new[] { id1, id2 }, ids);
.AddReferences(1, "references", Partitioning.Invariant)
.AddAssets(2, "assets", Partitioning.Invariant)
.AddArray(3, "array", Partitioning.Invariant, a => a
.AddAssets(31, "nested"));
}
[Fact]
@ -64,11 +42,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var input =
new NamedContentData()
.AddField("assets1",
.AddField("assets",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString())));
.AddJsonValue(JsonValue.Array(id1.ToString(), id2.ToString())));
var ids = input.GetReferencedIds(schema).ToArray();
var ids = new HashSet<Guid>();
input.AddReferencedIds(schema, ids);
Assert.Equal(new[] { id1, id2 }, ids);
}
@ -79,53 +59,44 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var input =
new IdContentData()
.AddField(5,
var source =
new NamedContentData()
.AddField("references",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString())));
var converter = FieldConverters.ForValues(ValueReferencesConverter.CleanReferences(new[] { id2 }));
var actual = input.ConvertId2Id(schema, converter);
var cleanedValue = (JsonArray)actual[5]!["iv"];
Assert.Equal(1, cleanedValue.Count);
Assert.Equal(id1.ToString(), cleanedValue[0].ToString());
}
[Fact]
public void Should_return_ids_from_assets_field()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray();
Assert.Equal(new[] { id1, id2 }, result);
}
[Fact]
public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_null()
{
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
.AddJsonValue(JsonValue.Array(id1, id2)))
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1)))
.AddField("array",
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id1, id2)))));
var result = sut.GetReferencedIds(null).ToArray();
var expected =
new NamedContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id2)))
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array()))
.AddField("array",
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id2)))));
Assert.Empty(result);
}
var cleaner = ValueReferencesConverter.CleanReferences(new HashSet<Guid> { id2 });
[Fact]
public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_other_type()
{
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
var converter = FieldConverters.ForValues(cleaner);
var converterNested = FieldConverters.ForNestedName2Name(cleaner);
var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray();
var actual = source.ConvertName2Name(schema, converter, converterNested);
Assert.Empty(result);
Assert.Equal(expected, actual);
}
[Fact]
@ -138,171 +109,127 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
Assert.Empty(result);
}
[Fact]
public void Should_return_null_from_assets_field_when_removing_references_from_null_array()
{
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
var result = sut.CleanReferences(JsonValue.Null, null);
Assert.Equal(JsonValue.Null, result);
}
[Fact]
public void Should_remove_deleted_references_from_assets_field()
[Theory]
[MemberData(nameof(ReferencingNestedFields))]
public void Should_return_ids_from_nested_field(NestedField field)
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2));
Assert.Equal(CreateValue(id1), result);
}
[Fact]
public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
var token = CreateValue(id1, id2);
var result = sut.CleanReferences(token, HashSet.Of(Guid.NewGuid()));
Assert.Same(token, result);
}
[Fact]
public void Should_return_ids_from_nested_references_field()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut =
Fields.Array(1, "my-array", Partitioning.Invariant,
Fields.References(1, "my-refs",
new ReferencesFieldProperties { SchemaId = schemaId }));
var arrayField = Fields.Array(1, "my-array", Partitioning.Invariant, field);
var value =
JsonValue.Array(
JsonValue.Object()
.Add("my-refs", CreateValue(id1, id2)));
.Add(field.Name, CreateValue(id1, id2)));
var result = sut.GetReferencedIds(value).ToArray();
var result = arrayField.GetReferencedIds(value).ToArray();
Assert.Equal(new[] { id1, id2, schemaId }, result);
Assert.Equal(new[] { id1, id2 }, result);
}
[Fact]
public void Should_return_ids_from_references_field()
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_item_is_invalid(IField field)
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var result = field.GetReferencedIds(JsonValue.Array("invalid")).ToArray();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId });
Assert.Empty(result);
}
var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray();
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_is_invalid(IField field)
{
var result = field.GetReferencedIds(JsonValue.Create("invalid")).ToArray();
Assert.Equal(new[] { id1, id2, schemaId }, result);
Assert.Empty(result);
}
[Fact]
public void Should_return_ids_from_references_field_without_schema_id()
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_is_empty(IField field)
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var result = field.GetReferencedIds(JsonValue.Array()).ToArray();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId });
Assert.Empty(result);
}
var result = sut.GetReferencedIds(CreateValue(id1, id2), Ids.ContentOnly).ToArray();
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_is_json_null(IField field)
{
var result = field.GetReferencedIds(null).ToArray();
Assert.Equal(new[] { id1, id2 }, result);
Assert.Empty(result);
}
[Fact]
public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null()
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_is_null(IField field)
{
var sut = Fields.References(1, "my-refs", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId });
var result = field.GetReferencedIds(null).ToArray();
var result = sut.GetReferencedIds(JsonValue.Null).ToArray();
Assert.Equal(new[] { schemaId }, result);
Assert.Empty(result);
}
[Fact]
public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type()
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_ids_from_field(IField field)
{
var sut = Fields.References(1, "my-refs", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId });
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray();
var value = CreateValue(id1, id2);
var result = field.GetReferencedIds(value);
Assert.Equal(new[] { schemaId }, result);
Assert.Equal(new HashSet<Guid> { id1, id2 }, result);
}
[Fact]
public void Should_return_null_from_references_field_when_removing_references_from_null_array()
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_return_same_value_from_field_when_value_is_json_null(IField field)
{
var sut = Fields.References(1, "my-refs", Partitioning.Invariant);
var result = sut.CleanReferences(JsonValue.Null, null);
var result = ValueReferencesConverter.CleanReferences(RandomIds())(JsonValue.Null, field);
Assert.Equal(JsonValue.Null, result);
}
[Fact]
public void Should_remove_deleted_references_from_references_field()
[Theory]
[MemberData(nameof(ReferencingFields))]
public void Should_remove_deleted_ids_from_field(IField field)
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId });
var value = CreateValue(id1, id2);
var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2));
var result = ValueReferencesConverter.CleanReferences(HashSet.Of(id1))(value, field);
Assert.Equal(CreateValue(id1), result);
}
[Fact]
public void Should_remove_all_references_from_references_field_when_schema_is_removed()
public static IEnumerable<object[]> ReferencingNestedFields()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId });
var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(schemaId));
Assert.Equal(CreateValue(), result);
yield return new object[] { Fields.References(1, "my-refs") };
yield return new object[] { Fields.Assets(1, "my-assets") };
}
[Fact]
public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove()
public static IEnumerable<object[]> ReferencingFields()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant);
var value = CreateValue(id1, id2);
var result = sut.CleanReferences(value, HashSet.Of(Guid.NewGuid()));
yield return new object[] { Fields.References(1, "my-refs", Partitioning.Invariant) };
yield return new object[] { Fields.Assets(1, "my-assets", Partitioning.Invariant) };
}
Assert.Same(value, result);
private static HashSet<Guid> RandomIds()
{
return HashSet.Of(Guid.NewGuid());
}
private static IJsonValue CreateValue(params Guid[] ids)
private static IJsonValue CreateValue(params object[] ids)
{
return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray());
return JsonValue.Array(ids);
}
}
}

11
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -282,7 +282,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Array()))
.AddJsonValue(JsonValue.Array()))
};
var result = sut.Format(script, @event);
@ -301,7 +301,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = sut.Format(script, @event);
@ -339,8 +339,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
"Berlin")))
.AddJsonValue(JsonValue.Array("Berlin")))
};
var result = sut.Format(script, @event);
@ -359,7 +358,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = sut.Format(script, @event);
@ -378,7 +377,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = sut.Format(script, @event);

12
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs

@ -155,13 +155,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(1.0, 2.0)));
.AddJsonValue(JsonValue.Array(1.0, 2.0)));
var expected =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(1.0, 4.0, 5.0)));
.AddJsonValue(JsonValue.Array(1.0, 4.0, 5.0)));
var result = ExecuteScript(original, @"data.number.iv = [data.number.iv[0], data.number.iv[1] + 2, 5]");
@ -175,13 +175,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("lat", 1.0)));
.AddJsonValue(JsonValue.Object().Add("lat", 1.0)));
var expected =
new NamedContentData()
.AddField("number",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("lat", 1.0).Add("lon", 4.0)));
.AddJsonValue(JsonValue.Object().Add("lat", 1.0).Add("lon", 4.0)));
var result = ExecuteScript(original, @"data.number.iv = { lat: data.number.iv.lat, lon: data.number.iv.lat + 3 }");
@ -265,7 +265,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new NamedContentData()
.AddField("obj",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("readonly", 1)));
.AddJsonValue(JsonValue.Object().Add("readonly", 1)));
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, "data.obj.iv.invalid = 1"));
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, "data.obj.iv.readonly = 2"));
@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new NamedContentData()
.AddField("obj",
new ContentFieldData()
.AddValue("iv", JsonValue.Array()));
.AddJsonValue(JsonValue.Array()));
ExecuteScript(original, "data.obj.iv[0] = 1");
}

15
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs

@ -12,6 +12,7 @@ using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
@ -44,8 +45,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
var oldData = GenerateData("o_raw");
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId),
A<HashSet<string>>.That.IsSameSequenceAs("n_raw2_1", "n_raw2_2", "n_raw4"),
A<HashSet<string>>.That.IsSameSequenceAs("o_raw2_1", "o_raw2_2", "o_raw4")))
A<HashSet<string>>.That.Is("n_raw2_1", "n_raw2_2", "n_raw4"),
A<HashSet<string>>.That.Is("o_raw2_1", "o_raw2_2", "o_raw4")))
.Returns(new Dictionary<string, string>
{
["n_raw2_2"] = "id2_2",
@ -65,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
var newData = GenerateData("name");
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId),
A<HashSet<string>>.That.IsSameSequenceAs("name2_1", "name2_2", "name4"),
A<HashSet<string>>.That.Is("name2_1", "name2_2", "name4"),
A<HashSet<string>>.That.IsEmpty()))
.Returns(new Dictionary<string, string>
{
@ -86,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
var newData = GenerateData("id");
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId),
A<HashSet<string>>.That.IsSameSequenceAs("id2_1", "id2_2", "id4"),
A<HashSet<string>>.That.Is("id2_1", "id2_2", "id4"),
A<HashSet<string>>.That.IsEmpty()))
.Returns(new Dictionary<string, string>
{
@ -114,16 +115,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
return new NamedContentData()
.AddField("tags1",
new ContentFieldData()
.AddValue("iv", JsonValue.Array($"{prefix}1")))
.AddJsonValue(JsonValue.Array($"{prefix}1")))
.AddField("tags2",
new ContentFieldData()
.AddValue("iv", JsonValue.Array($"{prefix}2_1", $"{prefix}2_2")))
.AddJsonValue(JsonValue.Array($"{prefix}2_1", $"{prefix}2_2")))
.AddField("string",
new ContentFieldData()
.AddValue("iv", $"{prefix}stringValue"))
.AddField("array",
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("nestedTags1", JsonValue.Array($"{prefix}3"))

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs

@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected array of objects." });
}
[Fact]

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected array of guid strings." });
}
[Fact]

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected boolean." });
}
private static IJsonValue CreateValue(bool? v)

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -348,7 +348,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new NamedContentData()
.AddField("my-field",
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object(),
JsonValue.Object().Add("my-nested", 1),
@ -387,7 +387,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new NamedContentData()
.AddField("my-field",
new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object())));

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "The value string does not match the required number from the format string \"uuuu\". Value being parsed: '^Invalid'. (^ indicates error position.)" });
}
[Fact]
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create(123), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected string." });
}
private static Instant FutureDays(int days)
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static IJsonValue CreateValue(Instant v)
{
return JsonValue.Create(v.ToString());
return JsonValue.Create(v);
}
private static RootField<DateTimeFieldProperties> Field(DateTimeFieldProperties properties)

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(geolocation, errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Latitude must be between -90 and 90." });
}
[Fact]
@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(geolocation, errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Longitude must be between -180 and 180." });
}
[Fact]
@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(geolocation, errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Geolocation can only have latitude and longitude property." });
}
[Fact]

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs

@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected number." });
}
[Fact]

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs

@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context());
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected array of guid strings." });
}
[Fact]

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs

@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Not a valid value." });
new[] { "Invalid json type, expected array of strings." });
}
[Fact]

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs

@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData()
.AddField("my-array", new ContentFieldData()
.AddValue("iv",
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("my-ui", null))));

31
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
namespace Squidex.Domain.Apps.Core.TestHelpers
{
public static class AExtensions
{
public static T[] Is<T>(this INegatableArgumentConstraintManager<T[]> that, params T[]? values)
{
return values == null ? that.IsNull() : that.IsSameSequenceAs(values);
}
public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, IEnumerable<T>? values)
{
return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Count());
}
public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, params T[]? values)
{
return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>.Ignored, requestContext))
.ReturnsLazily(() => SimpleMapper.Map(asset.Snapshot, new AssetEntity()));
A.CallTo(() => assetQuery.QueryByHashAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), AppId, A<string>.Ignored))
A.CallTo(() => assetQuery.QueryByHashAsync(A<Context>.That.Matches(x => x.ShouldEnrichAsset()), AppId, A<string>.Ignored))
.Returns(new List<IEnrichedAssetEntity>());
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
@ -301,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
FileSize = fileSize
};
A.CallTo(() => assetQuery.QueryByHashAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), A<Guid>.Ignored, A<string>.Ignored))
A.CallTo(() => assetQuery.QueryByHashAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), A<Guid>.Ignored, A<string>.Ignored))
.Returns(new List<IEnrichedAssetEntity> { duplicate });
}

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs

@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AppId = appId
};
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Has("id1", "id2")))
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Is("id1", "id2")))
.Returns(new Dictionary<string, string>
{
["id1"] = "name1",
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AppId = appId
};
var result = await sut.EnrichAsync(source, requestContext.Clone().WithNoAssetEnrichment());
var result = await sut.EnrichAsync(source, requestContext.Clone().WithoutAssetEnrichment());
Assert.Null(result.TagNames);
}
@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AppId = appId
};
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Has("id1", "id2", "id3")))
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Is("id1", "id2", "id3")))
.Returns(new Dictionary<string, string>
{
["id1"] = "name1",

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var ids = HashSet.Of(found1.Id, found2.Id);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<HashSet<Guid>>.That.IsSameSequenceAs(ids)))
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<HashSet<Guid>>.That.Is(ids)))
.Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))

1
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs

@ -9,6 +9,7 @@ using System;
using System.Threading.Tasks;
using FakeItEasy;
using Orleans;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Commands;

176
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs

@ -6,11 +6,11 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
@ -20,179 +20,89 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class ContentEnricherTests
{
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly ISchemaEntity schema;
private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ContentEnricher sut;
public ContentEnricherTests()
private sealed class ResolveSchema : IContentEnricherStep
{
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
public ISchemaEntity Schema { get; private set; }
schema = Mocks.Schema(appId, schemaId);
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A<Context>.Ignored, schemaId.Id.ToString()))
.Returns(schema);
sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy<IContentQueryService>(() => contentQuery), contentWorkflow);
}
[Fact]
public async Task Should_add_app_version_and_schema_as_dependency()
{
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source, requestContext);
Assert.Contains(requestContext.App.Version, result.CacheDependencies);
Assert.Contains(schema.Id, result.CacheDependencies);
Assert.Contains(schema.Version, result.CacheDependencies);
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
Schema = await schemas(group.Key);
}
}
}
[Fact]
public async Task Should_enrich_with_reference_fields()
public ContentEnricherTests()
{
var ctx = new Context(Mocks.FrontendUser(), requestContext.App);
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
var result = await sut.EnrichAsync(source, ctx);
schema = Mocks.Schema(appId, schemaId);
Assert.NotNull(result.ReferenceFields);
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()))
.Returns(schema);
}
[Fact]
public async Task Should_not_enrich_with_reference_fields_when_not_frontend()
public async Task Should_not_invoke_steps()
{
var source = PublishedContent();
var source = new IContentEntity[0];
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var step1 = A.Fake<IContentEnricherStep>();
var step2 = A.Fake<IContentEnricherStep>();
var result = await sut.EnrichAsync(source, requestContext);
var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
Assert.Null(result.ReferenceFields);
}
[Fact]
public async Task Should_enrich_with_schema_names()
{
var ctx = new Context(Mocks.FrontendUser(), requestContext.App);
await sut.EnrichAsync(source, requestContext);
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source, ctx);
A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>.Ignored, A<ProvideSchema>.Ignored))
.MustNotHaveHappened();
Assert.Equal("my-schema", result.SchemaName);
Assert.Equal("my-schema", result.SchemaDisplayName);
A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>.Ignored, A<ProvideSchema>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_enrich_with_schema_names_when_not_frontend()
public async Task Should_invoke_steps()
{
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source, requestContext);
Assert.Null(result.SchemaName);
Assert.Null(result.SchemaDisplayName);
}
var step1 = A.Fake<IContentEnricherStep>();
var step2 = A.Fake<IContentEnricherStep>();
[Fact]
public async Task Should_enrich_content_with_status_color()
{
var source = PublishedContent();
var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
await sut.EnrichAsync(source, requestContext);
var result = await sut.EnrichAsync(source, requestContext);
A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>.Ignored, A<ProvideSchema>.Ignored))
.MustHaveHappened();
Assert.Equal(StatusColors.Published, result.StatusColor);
A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>.Ignored, A<ProvideSchema>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Should_enrich_content_with_default_color_if_not_found()
public async Task Should_provide_and_cache_schema()
{
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(Task.FromResult<StatusInfo>(null!));
var result = await sut.EnrichAsync(source, requestContext);
Assert.Equal(StatusColors.Draft, result.StatusColor);
}
[Fact]
public async Task Should_enrich_content_with_can_update()
{
requestContext.WithResolveFlow(true);
var source = new ContentEntity { SchemaId = schemaId };
A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User))
.Returns(true);
var result = await sut.EnrichAsync(source, requestContext);
Assert.True(result.CanUpdate);
}
[Fact]
public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context()
{
requestContext.WithResolveFlow(false);
var source = new ContentEntity { SchemaId = schemaId };
var result = await sut.EnrichAsync(source, requestContext);
Assert.False(result.CanUpdate);
A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_enrich_multiple_contents_and_cache_color()
{
var source1 = PublishedContent();
var source2 = PublishedContent();
var source = new IContentEntity[]
{
source1,
source2
};
var step1 = new ResolveSchema();
var step2 = new ResolveSchema();
A.CallTo(() => contentWorkflow.GetInfoAsync(source1))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
var result = await sut.EnrichAsync(source, requestContext);
await sut.EnrichAsync(source, requestContext);
Assert.Equal(StatusColors.Published, result[0].StatusColor);
Assert.Equal(StatusColors.Published, result[1].StatusColor);
Assert.Same(schema, step1.Schema);
Assert.Same(schema, step1.Schema);
A.CallTo(() => contentWorkflow.GetInfoAsync(A<IContentEntity>.Ignored))
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()))
.MustHaveHappenedOnceExactly();
}

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -12,7 +12,6 @@ using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
@ -35,7 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAssetUrlGenerator urlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>();
@ -79,7 +77,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
sut = new ContentQueryService(
appProvider,
urlGenerator,
contentEnricher,
contentRepository,
contentVersionLoader,

146
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs

@ -0,0 +1,146 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class ConvertDataTests
{
private readonly ISchemaEntity schema;
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly ConvertData sut;
public ConvertDataTests()
{
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
var schemaDef =
new Schema("my-schema")
.AddReferences(1, "references", Partitioning.Invariant)
.AddAssets(2, "assets", Partitioning.Invariant)
.AddArray(3, "array", Partitioning.Invariant, a => a
.AddAssets(31, "nested"));
schema = Mocks.Schema(appId, schemaId, schemaDef);
schemaProvider = x => Task.FromResult(schema);
sut = new ConvertData(assetUrlGenerator, assetRepository, contentRepository);
}
[Fact]
public async Task Should_convert_data_only()
{
var source = PublishedContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider);
Assert.NotNull(source.Data);
Assert.Null(source.DataDraft);
}
[Fact]
public async Task Should_convert_data_and_data_draft_when_frontend_user()
{
var source = PublishedContent();
var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId));
await sut.EnrichAsync(ctx, Enumerable.Repeat(source, 1), schemaProvider);
Assert.NotNull(source.Data);
Assert.NotNull(source.DataDraft);
}
[Fact]
public async Task Should_cleanup_references()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var source =
new NamedContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2)))
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1)))
.AddField("array",
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id1, id2)))));
var expected =
new NamedContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id2)))
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array()))
.AddField("array",
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("nested", JsonValue.Array(id2)))));
var content = PublishedContent();
content.Data = source;
content.DataDraft = source;
A.CallTo(() => assetRepository.QueryIdsAsync(appId.Id, A<HashSet<Guid>>.That.Is(id1, id2)))
.Returns(new List<Guid> { id2 });
A.CallTo(() => contentRepository.QueryIdsAsync(appId.Id, A<HashSet<Guid>>.That.Is(id1, id2)))
.Returns(new List<(Guid, Guid)> { (id2, id2) });
var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId));
await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider);
Assert.Equal(expected, content.Data);
Assert.Equal(expected, content.DataDraft);
}
private ContentEntity PublishedContent()
{
return new ContentEntity
{
Status = Status.Published,
Data = new NamedContentData(),
DataDraft = new NamedContentData(),
SchemaId = schemaId
};
}
}
}

57
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class EnrichForCachingTests
{
private readonly ISchemaEntity schema;
private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly EnrichForCaching sut;
public EnrichForCachingTests()
{
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
schema = Mocks.Schema(appId, schemaId);
schemaProvider = x => Task.FromResult(schema);
sut = new EnrichForCaching();
}
[Fact]
public async Task Should_add_app_version_and_schema_as_dependency()
{
var source = PublishedContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider);
Assert.Contains(requestContext.App.Version, source.CacheDependencies);
Assert.Contains(schema.Id, source.CacheDependencies);
Assert.Contains(schema.Version, source.CacheDependencies);
}
private ContentEntity PublishedContent()
{
return new ContentEntity { Status = Status.Published, SchemaId = schemaId };
}
}
}

79
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs

@ -0,0 +1,79 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class EnrichWithSchemaTests
{
private readonly ISchemaEntity schema;
private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly EnrichWithSchema sut;
public EnrichWithSchemaTests()
{
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
schema = Mocks.Schema(appId, schemaId);
schemaProvider = x => Task.FromResult(schema);
sut = new EnrichWithSchema();
}
[Fact]
public async Task Should_enrich_with_reference_fields()
{
var ctx = new Context(Mocks.FrontendUser(), requestContext.App);
var source = PublishedContent();
await sut.EnrichAsync(ctx, Enumerable.Repeat(source, 1), schemaProvider);
Assert.NotNull(source.ReferenceFields);
}
[Fact]
public async Task Should_not_enrich_with_reference_fields_when_not_frontend()
{
var source = PublishedContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider);
Assert.Null(source.ReferenceFields);
}
[Fact]
public async Task Should_enrich_with_schema_names()
{
var ctx = new Context(Mocks.FrontendUser(), requestContext.App);
var source = PublishedContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider);
Assert.Equal("my-schema", source.SchemaName);
Assert.Equal("my-schema", source.SchemaDisplayName);
}
private ContentEntity PublishedContent()
{
return new ContentEntity { Status = Status.Published, SchemaId = schemaId };
}
}
}

109
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs

@ -0,0 +1,109 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class EnrichWithWorkflowsTests
{
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly ISchemaEntity schema;
private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly EnrichWithWorkflows sut;
public EnrichWithWorkflowsTests()
{
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
schema = Mocks.Schema(appId, schemaId);
schemaProvider = x => Task.FromResult(schema);
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A<Context>.Ignored, schemaId.Id.ToString()))
.Returns(schema);
sut = new EnrichWithWorkflows(contentWorkflow);
}
[Fact]
public async Task Should_enrich_content_with_status_color()
{
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
await sut.EnrichAsync(requestContext, new[] { source }, schemaProvider);
Assert.Equal(StatusColors.Published, source.StatusColor);
}
[Fact]
public async Task Should_enrich_content_with_default_color_if_not_found()
{
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(Task.FromResult<StatusInfo>(null!));
var ctx = requestContext.WithResolveFlow(true);
await sut.EnrichAsync(ctx, new[] { source }, schemaProvider);
Assert.Equal(StatusColors.Draft, source.StatusColor);
}
[Fact]
public async Task Should_enrich_content_with_can_update()
{
var source = new ContentEntity { SchemaId = schemaId };
A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User))
.Returns(true);
var ctx = requestContext.WithResolveFlow(true);
await sut.EnrichAsync(ctx, new[] { source }, schemaProvider);
Assert.True(source.CanUpdate);
}
[Fact]
public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context()
{
requestContext.WithResolveFlow(false);
var source = new ContentEntity { SchemaId = schemaId };
var ctx = requestContext.WithResolveFlow(false);
await sut.EnrichAsync(ctx, new[] { source }, schemaProvider);
Assert.False(source.CanUpdate);
A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User))
.MustNotHaveHappened();
}
private ContentEntity PublishedContent()
{
return new ContentEntity { Status = Status.Published, SchemaId = schemaId };
}
}
}

92
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs → backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs

@ -15,6 +15,8 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -22,18 +24,17 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class ContentEnricherAssetsTests
public class ResolveAssetsTests
{
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly Context requestContext;
private readonly ContentEnricher sut;
private readonly ResolveAssets sut;
public ContentEnricherAssetsTests()
public ResolveAssetsTests()
{
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE));
@ -56,17 +57,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => assetUrlGenerator.GenerateUrl(A<string>.Ignored))
.ReturnsLazily(new Func<string, string>(id => $"url/to/{id}"));
void SetupSchema(NamedId<Guid> id, Schema def)
schemaProvider = x =>
{
var schemaEntity = Mocks.Schema(appId, id, def);
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, id.Id.ToString()))
.Returns(schemaEntity);
}
SetupSchema(schemaId, schemaDef);
if (x == schemaId.Id)
{
return Task.FromResult(Mocks.Schema(appId, schemaId, schemaDef));
}
else
{
throw new DomainObjectNotFoundException(x.ToString(), typeof(ISchemaEntity));
}
};
sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy<IContentQueryService>(() => contentQuery), contentWorkflow);
sut = new ResolveAssets(assetUrlGenerator, assetQuery);
}
[Fact]
@ -78,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown);
var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown);
var source = new IContentEntity[]
var source = new[]
{
CreateContent(
new[] { document1.Id, image1.Id },
@ -88,17 +91,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new[] { document2.Id, image2.Id })
};
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), null, A<Q>.That.Matches(x => x.Ids.Count == 4)))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.Matches(x => x.Ids.Count == 4)))
.Returns(ResultList.CreateFrom(4, image1, image2, document1, document2));
var enriched = await sut.EnrichAsync(source, requestContext);
await sut.EnrichAsync(requestContext, source, schemaProvider);
var enriched1 = enriched.ElementAt(0);
var enriched1 = source[0];
Assert.Contains(image1.Id, enriched1.CacheDependencies);
Assert.Contains(image1.Version, enriched1.CacheDependencies);
var enriched2 = enriched.ElementAt(1);
var enriched2 = source[1];
Assert.Contains(image2.Id, enriched2.CacheDependencies);
Assert.Contains(image2.Version, enriched2.CacheDependencies);
@ -113,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown);
var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown);
var source = new IContentEntity[]
var source = new[]
{
CreateContent(
new[] { document1.Id, image1.Id },
@ -123,20 +126,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new[] { document2.Id, image2.Id })
};
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), null, A<Q>.That.Matches(x => x.Ids.Count == 4)))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), null, A<Q>.That.Matches(x => x.Ids.Count == 4)))
.Returns(ResultList.CreateFrom(4, image1, image2, document1, document2));
var enriched = await sut.EnrichAsync(source, requestContext);
await sut.EnrichAsync(requestContext, source, schemaProvider);
Assert.Equal(
new NamedContentData()
.AddField("asset1",
new ContentFieldData()
.AddValue("iv",
$"url/to/{image1.Id}"))
.AddValue("iv", $"url/to/{image1.Id}"))
.AddField("asset2",
new ContentFieldData()),
enriched.ElementAt(0).ReferenceData);
source[0].ReferenceData);
Assert.Equal(
new NamedContentData()
@ -144,22 +146,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new ContentFieldData())
.AddField("asset2",
new ContentFieldData()
.AddValue("en",
$"url/to/{image2.Id}")),
enriched.ElementAt(1).ReferenceData);
.AddValue("en", $"url/to/{image2.Id}")),
source[1].ReferenceData);
}
[Fact]
public async Task Should_not_enrich_references_if_not_api_user()
{
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0])
};
var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId)));
var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId));
await sut.EnrichAsync(ctx, source, schemaProvider);
Assert.Null(enriched.ElementAt(0).ReferenceData);
Assert.Null(source[0].ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
@ -168,14 +171,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact]
public async Task Should_not_enrich_references_if_disabled()
{
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0])
};
var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId)));
var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).WithoutContentEnrichment(true);
Assert.Null(enriched.ElementAt(0).ReferenceData);
await sut.EnrichAsync(ctx, source, schemaProvider);
Assert.Null(source[0].ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
@ -184,31 +189,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact]
public async Task Should_not_invoke_query_service_if_no_assets_found()
{
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new Guid[0], new Guid[0])
};
var enriched = await sut.EnrichAsync(source, requestContext);
Assert.NotNull(enriched.ElementAt(0).ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_invoke_query_service_if_nothing_to_enrich()
{
var source = new IContentEntity[0];
await sut.EnrichAsync(requestContext, source, schemaProvider);
await sut.EnrichAsync(source, requestContext);
Assert.NotNull(source[0].ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
}
private IEnrichedContentEntity CreateContent(Guid[] assets1, Guid[] assets2)
private ContentEntity CreateContent(Guid[] assets1, Guid[] assets2)
{
return new ContentEntity
{

107
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs → backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs

@ -12,9 +12,9 @@ using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -22,20 +22,18 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class ContentEnricherReferencesTests
public class ResolveReferencesTests
{
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> refSchemaId1 = NamedId.Of(Guid.NewGuid(), "my-ref1");
private readonly NamedId<Guid> refSchemaId2 = NamedId.Of(Guid.NewGuid(), "my-ref2");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly Context requestContext;
private readonly ContentEnricher sut;
private readonly ResolveReferences sut;
public ContentEnricherReferencesTests()
public ResolveReferencesTests()
{
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE));
@ -65,19 +63,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
})
.ConfigureFieldsInLists("ref1", "ref2");
void SetupSchema(NamedId<Guid> id, Schema def)
schemaProvider = x =>
{
var schemaEntity = Mocks.Schema(appId, id, def);
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, id.Id.ToString()))
.Returns(schemaEntity);
}
SetupSchema(schemaId, schemaDef);
SetupSchema(refSchemaId1, refSchemaDef);
SetupSchema(refSchemaId2, refSchemaDef);
if (x == schemaId.Id)
{
return Task.FromResult(Mocks.Schema(appId, schemaId, schemaDef));
}
else if (x == refSchemaId1.Id)
{
return Task.FromResult(Mocks.Schema(appId, refSchemaId1, refSchemaDef));
}
else if (x == refSchemaId2.Id)
{
return Task.FromResult(Mocks.Schema(appId, refSchemaId2, refSchemaDef));
}
else
{
throw new DomainObjectNotFoundException(x.ToString(), typeof(ISchemaEntity));
}
};
sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy<IContentQueryService>(() => contentQuery), contentWorkflow);
sut = new ResolveReferences(new Lazy<IContentQueryService>(() => contentQuery));
}
[Fact]
@ -88,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var ref2_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2);
var ref2_2 = CreateRefContent(Guid.NewGuid(), 4, "ref2_2", 29, refSchemaId2);
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }),
CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id })
@ -97,9 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<IReadOnlyList<Guid>>.That.Matches(x => x.Count == 4)))
.Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2));
var enriched = await sut.EnrichAsync(source, requestContext);
await sut.EnrichAsync(requestContext, source, schemaProvider);
var enriched1 = enriched.ElementAt(0);
var enriched1 = source[0];
Assert.Contains(refSchemaId1.Id, enriched1.CacheDependencies);
Assert.Contains(refSchemaId2.Id, enriched1.CacheDependencies);
@ -110,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Contains(ref2_1.Id, enriched1.CacheDependencies);
Assert.Contains(ref2_1.Version, enriched1.CacheDependencies);
var enriched2 = enriched.ElementAt(1);
var enriched2 = source[1];
Assert.Contains(refSchemaId1.Id, enriched2.CacheDependencies);
Assert.Contains(refSchemaId2.Id, enriched2.CacheDependencies);
@ -130,16 +136,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var ref2_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2);
var ref2_2 = CreateRefContent(Guid.NewGuid(), 3, "ref2_2", 29, refSchemaId2);
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }),
CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id })
};
A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoEnrichment()), A<IReadOnlyList<Guid>>.That.Matches(x => x.Count == 4)))
A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichContent()), A<IReadOnlyList<Guid>>.That.Matches(x => x.Count == 4)))
.Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2));
var enriched = await sut.EnrichAsync(source, requestContext);
await sut.EnrichAsync(requestContext, source, schemaProvider);
Assert.Equal(
new NamedContentData()
@ -155,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object()
.Add("en", "ref2_1, 23")
.Add("de", "ref2_1, 23"))),
enriched.ElementAt(0).ReferenceData);
source[0].ReferenceData);
Assert.Equal(
new NamedContentData()
@ -171,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object()
.Add("en", "ref2_2, 29")
.Add("de", "ref2_2, 29"))),
enriched.ElementAt(1).ReferenceData);
source[1].ReferenceData);
}
[Fact]
@ -182,16 +188,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var ref2_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2);
var ref2_2 = CreateRefContent(Guid.NewGuid(), 4, "ref2_2", 29, refSchemaId2);
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id, ref2_2.Id }),
CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id })
};
A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoEnrichment()), A<IReadOnlyList<Guid>>.That.Matches(x => x.Count == 4)))
A.CallTo(() => contentQuery.QueryAsync(A<Context>.That.Matches(x => !x.ShouldEnrichContent()), A<IReadOnlyList<Guid>>.That.Matches(x => x.Count == 4)))
.Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2));
var enriched = await sut.EnrichAsync(source, requestContext);
await sut.EnrichAsync(requestContext, source, schemaProvider);
Assert.Equal(
new NamedContentData()
@ -207,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object()
.Add("en", "2 Reference(s)")
.Add("de", "2 Reference(s)"))),
enriched.ElementAt(0).ReferenceData);
source[0].ReferenceData);
Assert.Equal(
new NamedContentData()
@ -223,20 +229,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object()
.Add("en", "2 Reference(s)")
.Add("de", "2 Reference(s)"))),
enriched.ElementAt(1).ReferenceData);
source[1].ReferenceData);
}
[Fact]
public async Task Should_not_enrich_references_if_not_api_user()
{
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0])
};
var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId)));
var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId));
await sut.EnrichAsync(ctx, source, schemaProvider);
Assert.Null(enriched.ElementAt(0).ReferenceData);
Assert.Null(source[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened();
@ -245,14 +253,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact]
public async Task Should_not_enrich_references_if_disabled()
{
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0])
};
var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId)));
var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).WithoutContentEnrichment(true);
Assert.Null(enriched.ElementAt(0).ReferenceData);
await sut.EnrichAsync(ctx, source, schemaProvider);
Assert.Null(source[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened();
@ -261,31 +271,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact]
public async Task Should_not_invoke_query_service_if_no_references_found()
{
var source = new IContentEntity[]
var source = new[]
{
CreateContent(new Guid[0], new Guid[0])
};
var enriched = await sut.EnrichAsync(source, requestContext);
Assert.NotNull(enriched.ElementAt(0).ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_invoke_query_service_if_nothing_to_enrich()
{
var source = new IContentEntity[0];
await sut.EnrichAsync(requestContext, source, schemaProvider);
await sut.EnrichAsync(source, requestContext);
Assert.NotNull(source[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened();
}
private IEnrichedContentEntity CreateContent(Guid[] ref1, Guid[] ref2)
private ContentEntity CreateContent(Guid[] ref1, Guid[] ref2)
{
return new ContentEntity
{

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs

@ -24,12 +24,12 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
return values == null ? that.IsNull() : that.IsSameSequenceAs(values);
}
public static IEnumerable<T> Has<T>(this INegatableArgumentConstraintManager<IEnumerable<T>> that, params T[]? values)
public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, IEnumerable<T>? values)
{
return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length);
return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Count());
}
public static HashSet<T> Has<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, params T[]? values)
public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, params T[]? values)
{
return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length);
}

2
backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs

@ -351,7 +351,7 @@ namespace Squidex.Infrastructure.Json.Objects
[Fact]
public void Should_throw_exception_when_creation_value_from_invalid_type()
{
Assert.Throws<ArgumentException>(() => JsonValue.Create(Guid.Empty));
Assert.Throws<ArgumentException>(() => JsonValue.Create(default(TimeSpan)));
}
[Fact]

83
backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs

@ -0,0 +1,83 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using TestSuite.Model;
using Xunit;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
namespace TestSuite.ApiTests
{
public class ContentCleanupTests : IClassFixture<ClientFixture>
{
public ClientFixture _ { get; }
public ContentCleanupTests(ClientFixture fixture)
{
_ = fixture;
}
[Fact]
public async Task Should_cleanup_old_data_from_update_response()
{
var schemaName = $"schema-{DateTime.UtcNow.Ticks}";
// STEP 1: Create a schema.
var schema = await _.Schemas.PostSchemaAsync(_.AppName, new CreateSchemaDto
{
Name = schemaName,
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
{
Name = "number",
Properties = new NumberFieldPropertiesDto
{
IsRequired = true
}
},
new UpsertSchemaFieldDto
{
Name = "string",
Properties = new StringFieldPropertiesDto
{
IsRequired = false
}
}
},
IsPublished = true
});
var contents = _.ClientManager.GetClient<TestEntity, TestEntityData>(schemaName);
// STEP 2: Create a content for this schema.
var data = new TestEntityData { Number = 12, String = "hello" };
var content_1 = await contents.CreateAsync(data);
Assert.Equal(data.String, content_1.Data.String);
// STEP 3: Delete a field from schema.
await _.Schemas.DeleteFieldAsync(_.AppName, schema.Name, schema.Fields.ElementAt(1).FieldId);
// STEP 4: Make any update.
var content_2 = await contents.ChangeStatusAsync(content_1.Id, "Published");
// Should not return deleted field.
Assert.Null(content_2.Data.String);
}
}
}

4
backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs

@ -36,9 +36,7 @@ namespace TestSuite.Fixtures
{
try
{
var schemas = ClientManager.CreateSchemasClient();
await schemas.PostSchemaAsync(AppName, new CreateSchemaDto
await Schemas.PostSchemaAsync(AppName, new CreateSchemaDto
{
Name = SchemaName,
Fields = new List<UpsertSchemaFieldDto>

Loading…
Cancel
Save