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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent 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, string> KeyNameResolver = f => f.Name;
private static readonly Func<IRootField, long> KeyIdResolver = f => f.Id; 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) public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters)
{ {
Guard.NotNull(schema); 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 try
{ {
JsonValueConverter.ConvertValue(field, value); var (_, error) = JsonValueConverter.ConvertValue(field, value);
if (error != null)
{
return null;
}
} }
catch catch
{ {

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

@ -64,7 +64,12 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
try try
{ {
JsonValueConverter.ConvertValue(field, value); var (_, error) = JsonValueConverter.ConvertValue(field, value);
if (error != null)
{
return Value.Unset;
}
} }
catch 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.Contents;
using Squidex.Domain.Apps.Core.Schemas; 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); 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;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.EnrichContent namespace Squidex.Domain.Apps.Core.DefaultValues
{ {
public sealed class DefaultValueFactory : IFieldVisitor<IJsonValue> public sealed class DefaultValueFactory : IFieldVisitor<IJsonValue>
{ {
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
{ {
if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now)
{ {
return JsonValue.Create(now.ToString()); return JsonValue.Create(now);
} }
if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) 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($"{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;
using Squidex.Infrastructure.Json.Objects; 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 Schema schema;
private readonly PartitionResolver partitionResolver; private readonly PartitionResolver partitionResolver;
public ContentEnricher(Schema schema, PartitionResolver partitionResolver) public DefaultValueGenerator(Schema schema, PartitionResolver partitionResolver)
{ {
Guard.NotNull(schema); Guard.NotNull(schema);
Guard.NotNull(partitionResolver); 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 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); Guard.NotNull(schema);
foreach (var field in schema.Fields) var extractor = new ReferencesExtractor(new HashSet<Guid>());
{
var ids = source.GetReferencedIds(field, strategy);
foreach (var id in ids) AddReferencedIds(source, schema.Fields, extractor);
{
yield return id; 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) var extractor = new ReferencesExtractor(result);
{
foreach (var partitionValue in fieldData)
{
var ids = field.GetReferencedIds(partitionValue.Value, strategy);
foreach (var id in ids) AddReferencedIds(source, schema.Fields, extractor);
{
yield return id;
}
}
}
} }
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) foreach (var field in fields)
{ {
var ids = source.GetReferencedIds(field, strategy); AddReferencedIds(source, field, extractor);
foreach (var id in ids)
{
yield return id;
}
} }
} }
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) if (source.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{ {
foreach (var partitionValue in fieldData) foreach (var partitionValue in fieldData)
{ {
var ids = field.GetReferencedIds(partitionValue.Value, strategy); extractor.SetValue(partitionValue.Value);
foreach (var id in ids) field.Accept(extractor);
{
yield return id;
}
} }
} }
} }
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 = ", ") public static JsonObject FormatReferences(this NamedContentData data, Schema schema, IFieldPartitioning partitioning, string separator = ", ")
{ {
Guard.NotNull(schema); Guard.NotNull(schema);

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

@ -8,25 +8,26 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
public sealed class ReferencesCleaner : IFieldVisitor<IJsonValue> public sealed class ReferencesCleaner : IFieldVisitor<IJsonValue>
{ {
private readonly IJsonValue value; private readonly HashSet<Guid> validIds;
private readonly ICollection<Guid>? oldReferences; 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) public IJsonValue Visit(IField<AssetsFieldProperties> field)
@ -36,31 +37,9 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
public IJsonValue Visit(IField<ReferencesFieldProperties> field) public IJsonValue Visit(IField<ReferencesFieldProperties> field)
{ {
if (oldReferences?.Contains(field.Properties.SingleId()) == true)
{
return JsonValue.Array();
}
return CleanIds(); 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) public IJsonValue Visit(IField<BooleanFieldProperties> field)
{ {
return value; return value;
@ -105,5 +84,31 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
return value; 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;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
public static class ReferencesExtensions public static class ReferencesExtensions
{ {
public static IEnumerable<Guid> GetReferencedIds(this IField field, IJsonValue? value, Ids strategy = Ids.All) public static void AddIds(this IJsonValue? value, HashSet<Guid> result)
{
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)
{ {
if (value is JsonArray array) if (value is JsonArray array)
{ {
var result = new HashSet<Guid>();
foreach (var id in array) foreach (var id in array)
{ {
if (id.Type == JsonValueType.String && Guid.TryParse(id.ToString(), out var guid)) 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); 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.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds 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 HashSet<Guid> result;
private readonly Ids strategy; private IJsonValue? value;
private ReferencesExtractor(IJsonValue? value, Ids strategy) public HashSet<Guid> Result
{ {
this.value = value; get { return result; }
this.strategy = strategy;
} }
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) if (value is JsonArray array)
{ {
foreach (var item in array.OfType<JsonObject>()) 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)) 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(); value.AddIds(result);
if (strategy == Ids.All && field.Properties.SchemaIds != null)
{
foreach (var schemaId in field.Properties.SchemaIds)
{
ids.Add(schemaId);
}
}
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 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) => return (value, field) =>
{ {
if (value.Type == JsonValueType.Null) 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. // 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, public string Error { get; }
ContentOnly
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 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; private readonly IJsonValue value;
@ -23,67 +23,67 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
this.value = value; 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)); return field.Accept(new JsonValueConverter(json));
} }
public object Visit(IArrayField field) public (object? Result, JsonError? Error) Visit(IArrayField field)
{ {
return ConvertToObjectList(); return ConvertToObjectList();
} }
public object Visit(IField<AssetsFieldProperties> field) public (object? Result, JsonError? Error) Visit(IField<AssetsFieldProperties> field)
{ {
return ConvertToGuidList(); return ConvertToGuidList();
} }
public object Visit(IField<ReferencesFieldProperties> field) public (object? Result, JsonError? Error) Visit(IField<ReferencesFieldProperties> field)
{ {
return ConvertToGuidList(); return ConvertToGuidList();
} }
public object Visit(IField<TagsFieldProperties> field) public (object? Result, JsonError? Error) Visit(IField<TagsFieldProperties> field)
{ {
return ConvertToStringList(); return ConvertToStringList();
} }
public object Visit(IField<BooleanFieldProperties> field) public (object? Result, JsonError? Error) Visit(IField<BooleanFieldProperties> field)
{ {
if (value is JsonScalar<bool> b) 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) if (value.Type == JsonValueType.String)
{ {
@ -91,16 +91,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (!parseResult.Success) 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) if (value is JsonObject geolocation)
{ {
@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) && if (!string.Equals(propertyName, "latitude", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(propertyName, "longitude", 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)) 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 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) 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)) 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 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) if (value is JsonArray array)
{ {
var result = new List<Guid>(); var result = new List<Guid>(array.Count);
foreach (var item in array) foreach (var item in array)
{ {
@ -166,21 +166,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
} }
else 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) if (value is JsonArray array)
{ {
var result = new List<string?>(); var result = new List<string?>(array.Count);
foreach (var item in array) foreach (var item in array)
{ {
@ -194,21 +194,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
} }
else 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) if (value is JsonArray array)
{ {
var result = new List<JsonObject>(); var result = new List<JsonObject>(array.Count);
foreach (var item in array) foreach (var item in array)
{ {
@ -218,14 +218,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
} }
else 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 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.IndexedAppId)
.Ascending(x => x.IsDeleted) .Ascending(x => x.IsDeleted)
.Ascending(x => x.ParentId) .Ascending(x => x.ParentId)
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)), .Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>( new CreateIndexModel<MongoAssetEntity>(
Index Index
.Ascending(x => x.IndexedAppId) .Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted) .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); }, 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")) 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; 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 namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
[BsonIgnoreExtraElements]
public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Guid> public sealed class MongoContentEntity : IContentEntity, IVersionedEntity<Guid>
{ {
private NamedContentData? data; private NamedContentData? data;
@ -42,12 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonRequired] [BsonRequired]
[BsonElement("rf")] [BsonElement("rf")]
[BsonRepresentation(BsonType.String)] [BsonRepresentation(BsonType.String)]
public List<Guid>? ReferencedIds { get; set; } public HashSet<Guid>? ReferencedIds { get; set; }
[BsonRequired]
[BsonElement("rd")]
[BsonRepresentation(BsonType.String)]
public List<Guid> ReferencedIdsDeleted { get; set; } = new List<Guid>();
[BsonRequired] [BsonRequired]
[BsonElement("ss")] [BsonElement("ss")]
@ -122,11 +118,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public void ParseData(Schema schema, IJsonSerializer serializer) public void ParseData(Schema schema, IJsonSerializer serializer)
{ {
data = DataByIds?.FromMongoModel(schema, ReferencedIdsDeleted, serializer); data = DataByIds?.FromMongoModel(schema, serializer);
if (DataDraftByIds != null) 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 IJsonSerializer serializer;
private readonly string typeAssetDeleted; private readonly string typeAssetDeleted;
private readonly string typeContentDeleted; private readonly string typeContentDeleted;
private readonly CleanupReferences cleanupReferences;
private readonly QueryContent queryContentAsync; private readonly QueryContent queryContentAsync;
private readonly QueryContentsByIds queryContentsById; private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery; private readonly QueryContentsByQuery queryContentsByQuery;
@ -59,7 +58,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
this.serializer = serializer; this.serializer = serializer;
cleanupReferences = new CleanupReferences();
queryContentAsync = new QueryContent(serializer); queryContentAsync = new QueryContent(serializer);
queryContentsById = new QueryContentsByIds(serializer, appProvider); queryContentsById = new QueryContentsByIds(serializer, appProvider);
queryContentsByQuery = new QueryContentsByQuery(serializer, indexer); 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) protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{ {
await cleanupReferences.PrepareAsync(collection, ct);
await queryContentAsync.PrepareAsync(collection, ct); await queryContentAsync.PrepareAsync(collection, ct);
await queryContentsById.PrepareAsync(collection, ct); await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.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;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -66,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id); 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; var idDraftData = idData;
if (!ReferenceEquals(value.Data, value.DataDraft)) if (!ReferenceEquals(value.Data, value.DataDraft))
@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
IsDeleted = value.IsDeleted, IsDeleted = value.IsDeleted,
IndexedAppId = value.AppId.Id, IndexedAppId = value.AppId.Id,
IndexedSchemaId = value.SchemaId.Id, IndexedSchemaId = value.SchemaId.Id,
ReferencedIds = idData.ToReferencedIds(schema.SchemaDef), ReferencedIds = value.Data.GetReferencedIds(schema.SchemaDef),
ScheduledAt = value.ScheduleJob?.DueTime, ScheduledAt = value.ScheduleJob?.DueTime,
Version = newVersion 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;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -20,20 +18,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
public static class Extensions public static class Extensions
{ {
public static List<Guid> ToReferencedIds(this IdContentData data, Schema schema) public static NamedContentData FromMongoModel(this IdContentData result, Schema schema, IJsonSerializer serializer)
{
return data.GetReferencedIds(schema).Distinct().ToList();
}
public static NamedContentData FromMongoModel(this IdContentData result, Schema schema, List<Guid> deletedIds, IJsonSerializer serializer)
{ {
return result.ConvertId2Name(schema, return result.ConvertId2Name(schema,
FieldConverters.ForValues( FieldConverters.ForValues(
ValueConverters.DecodeJson(serializer), ValueConverters.DecodeJson(serializer)),
ValueReferencesConverter.CleanReferences(deletedIds)),
FieldConverters.ForNestedId2Name( FieldConverters.ForNestedId2Name(
ValueConverters.DecodeJson(serializer), ValueConverters.DecodeJson(serializer)));
ValueReferencesConverter.CleanReferences(deletedIds)));
} }
public static IdContentData ToMongoModel(this NamedContentData result, Schema schema, IJsonSerializer 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 try
{ {
var ctx = contextProvider.Context.Clone().WithNoAssetEnrichment(); var ctx = contextProvider.Context.Clone().WithoutAssetEnrichment();
var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); 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"; 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) 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) 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<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity?> FindAssetAsync(Guid id); Task<IAssetEntity?> FindAssetAsync(Guid id);
Task<IAssetEntity?> FindAssetBySlugAsync(Guid appId, string slug); 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 System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; 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) 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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; 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.Schemas;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return context; 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; 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 HeaderResolveAssetUrls = "X-Resolve-Urls";
private const string HeaderNoResolveLanguages = "X-NoResolveLanguages"; private const string HeaderNoResolveLanguages = "X-NoResolveLanguages";
private const string HeaderNoEnrichment = "X-NoEnrichment"; private const string HeaderNoEnrichment = "X-NoEnrichment";
private const string HeaderNoCleanup = "X-NoCleanup";
private static readonly char[] Separators = { ',', ';' }; 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) return SetBoolean(context, HeaderNoCleanup, value);
{ }
context.Headers[HeaderNoEnrichment] = "1";
}
else
{
context.Headers.Remove(HeaderNoEnrichment);
}
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); return context.Headers.ContainsKey(HeaderUnpublished);
} }
public static Context WithUnpublished(this Context context, bool value = true) public static Context WithUnpublished(this Context context, bool value = true)
{ {
if (value) return SetBoolean(context, HeaderUnpublished, value);
{
context.Headers[HeaderUnpublished] = "1";
}
else
{
context.Headers.Remove(HeaderUnpublished);
}
return context;
} }
public static bool IsFlatten(this Context context) public static bool ShouldFlatten(this Context context)
{ {
return context.Headers.ContainsKey(HeaderFlatten); return context.Headers.ContainsKey(HeaderFlatten);
} }
public static Context WithFlatten(this Context context, bool value = true) public static Context WithFlatten(this Context context, bool value = true)
{ {
if (value) return SetBoolean(context, HeaderFlatten, value);
{
context.Headers[HeaderFlatten] = "1";
}
else
{
context.Headers.Remove(HeaderFlatten);
}
return context;
} }
public static bool IsResolveFlow(this Context context) public static bool ShouldResolveFlow(this Context context)
{ {
return context.Headers.ContainsKey(HeaderResolveFlow); return context.Headers.ContainsKey(HeaderResolveFlow);
} }
public static Context WithResolveFlow(this Context context, bool value = true) public static Context WithResolveFlow(this Context context, bool value = true)
{ {
if (value) return SetBoolean(context, HeaderResolveFlow, value);
{
context.Headers[HeaderResolveFlow] = "1";
}
else
{
context.Headers.Remove(HeaderResolveFlow);
}
return context;
} }
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) public static Context WithoutResolveLanguages(this Context context, bool value = true)
{ {
if (value) return SetBoolean(context, HeaderNoResolveLanguages, value);
{
context.Headers[HeaderNoResolveLanguages] = "1";
}
else
{
context.Headers.Remove(HeaderNoResolveLanguages);
}
return context;
} }
public static IEnumerable<string> AssetUrls(this Context context) public static IEnumerable<string> AssetUrls(this Context context)
@ -175,5 +141,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
return context; 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; 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.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -25,30 +18,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public sealed class ContentEnricher : IContentEnricher public sealed class ContentEnricher : IContentEnricher
{ {
private const string DefaultColor = StatusColors.Draft; private readonly IEnumerable<IContentEnricherStep> steps;
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 Lazy<IContentQueryService> contentQuery; private readonly Lazy<IContentQueryService> contentQuery;
private readonly IContentWorkflow contentWorkflow;
private IContentQueryService ContentQuery private IContentQueryService ContentQuery
{ {
get { return contentQuery.Value; } 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(steps);
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(contentQuery); Guard.NotNull(contentQuery);
Guard.NotNull(contentWorkflow);
this.assetQuery = assetQuery; this.steps = steps;
this.assetUrlGenerator = assetUrlGenerator;
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow;
} }
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, Context context) public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, Context context)
@ -73,308 +58,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (contents.Any()) if (contents.Any())
{ {
var cache = new Dictionary<(Guid, Status), StatusInfo>();
foreach (var content in contents) foreach (var content in contents)
{ {
var result = SimpleMapper.Map(content, new ContentEntity()); 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); results.Add(result);
} }
foreach (var group in results.GroupBy(x => x.SchemaId.Id)) var schemaCache = new Dictionary<Guid, Task<ISchemaEntity>>();
{
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;
}
}
}
}
private void ResolveAssets(ISchemaEntity schema, IGrouping<Guid, ContentEntity> contents, ILookup<Guid, IEnrichedAssetEntity> assets) Task<ISchemaEntity> GetSchema(Guid id)
{
foreach (var field in schema.SchemaDef.ResolvingAssets())
{
foreach (var content in contents)
{
if (content.ReferenceData == null)
{ {
content.ReferenceData = new NamedContentData(); return schemaCache.GetOrAdd(id, x => ContentQuery.GetSchemaOrThrowAsync(context, x.ToString()));
} }
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; foreach (var step in steps)
if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{ {
foreach (var (partitionKey, partitionValue) in fieldData) using (Profiler.TraceMethod(step.ToString()!))
{ {
var referencedImage = await step.EnrichAsync(context, results, GetSchema);
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));
}
} }
} }
} }
}
}
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); return results;
}
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 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; 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 Status[] StatusPublishedOnly = { Status.Published };
private static readonly IResultList<IEnrichedContentEntity> EmptyContents = ResultList.CreateFrom<IEnrichedContentEntity>(0); private static readonly IResultList<IEnrichedContentEntity> EmptyContents = ResultList.CreateFrom<IEnrichedContentEntity>(0);
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IContentEnricher contentEnricher; private readonly IContentEnricher contentEnricher;
private readonly IContentRepository contentRepository; private readonly IContentRepository contentRepository;
private readonly IContentLoader contentVersionLoader; private readonly IContentLoader contentVersionLoader;
@ -38,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public ContentQueryService( public ContentQueryService(
IAppProvider appProvider, IAppProvider appProvider,
IAssetUrlGenerator assetUrlGenerator,
IContentEnricher contentEnricher, IContentEnricher contentEnricher,
IContentRepository contentRepository, IContentRepository contentRepository,
IContentLoader contentVersionLoader, IContentLoader contentVersionLoader,
@ -46,7 +43,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
ContentQueryParser queryParser) ContentQueryParser queryParser)
{ {
Guard.NotNull(appProvider); Guard.NotNull(appProvider);
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(contentEnricher); Guard.NotNull(contentEnricher);
Guard.NotNull(contentRepository); Guard.NotNull(contentRepository);
Guard.NotNull(contentVersionLoader); Guard.NotNull(contentVersionLoader);
@ -54,7 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Guard.NotNull(scriptEngine); Guard.NotNull(scriptEngine);
this.appProvider = appProvider; this.appProvider = appProvider;
this.assetUrlGenerator = assetUrlGenerator;
this.contentEnricher = contentEnricher; this.contentEnricher = contentEnricher;
this.contentRepository = contentRepository; this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader; this.contentVersionLoader = contentVersionLoader;
@ -169,8 +164,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var results = new List<IEnrichedContentEntity>(); var results = new List<IEnrichedContentEntity>();
var converters = GenerateConverters(context).ToArray();
var script = schema.SchemaDef.Scripts.Query; var script = schema.SchemaDef.Scripts.Query;
var scripting = !string.IsNullOrWhiteSpace(script); var scripting = !string.IsNullOrWhiteSpace(script);
@ -180,25 +173,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var result = SimpleMapper.Map(content, new ContentEntity()); 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 };
{
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);
}
if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient)) result.Data = scriptEngine.Transform(ctx, script);
{
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
else
{
result.DataDraft = null!;
} }
results.Add(result); 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) public async Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName)
{ {
ISchemaEntity? schema = null; ISchemaEntity? schema = null;
@ -287,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static Status[]? GetStatus(Context context) private static Status[]? GetStatus(Context context)
{ {
if (context.IsFrontendClient || context.IsUnpublished()) if (context.IsFrontendClient || context.ShouldProvideUnpublished())
{ {
return null; return null;
} }
@ -338,7 +280,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static bool WithDraft(Context context) 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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public interface IContentEnricher 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) internal JsonArray(params object?[] values)
: base(ToList(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); return Create(i);
case long l: case long l:
return Create(l); return Create(l);
case Guid g:
return Create(g);
case Instant i: case Instant i:
return Create(i); return Create(i);
} }
@ -75,21 +77,24 @@ namespace Squidex.Infrastructure.Json.Objects
throw new ArgumentException("Invalid json type"); 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 == null)
if (value == 0)
{ {
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) public static IJsonValue Create(Instant? value)
@ -99,7 +104,19 @@ namespace Squidex.Infrastructure.Json.Objects
return Null; 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) public static IJsonValue Create(double? value)
@ -112,6 +129,11 @@ namespace Squidex.Infrastructure.Json.Objects
return Create(value.Value); return Create(value.Value);
} }
public static IJsonValue Create(bool value)
{
return value ? True : False;
}
public static IJsonValue Create(bool? value) public static IJsonValue Create(bool? value)
{ {
if (value == null) 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()); var response = SimpleMapper.Map(content, new ContentDto());
if (context.IsFlatten()) if (context.ShouldFlatten())
{ {
response.Data = content.Data?.ToFlatten(); response.Data = content.Data?.ToFlatten();
response.DataDraft = content.DataDraft?.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 Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -39,6 +40,24 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentQueryService>() services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>(); .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>() services.AddSingletonAs<ContentEnricher>()
.As<IContentEnricher>(); .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.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ConvertContent 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.True(lhs.Equals((object)rhs));
Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); 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 = var input =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Object()); .AddJsonValue(JsonValue.Object());
var actual = FieldConverters.ForValues((f, i) => Value.Unset)(input, field); var actual = FieldConverters.ForValues((f, i) => Value.Unset)(input, field);
@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input = var input =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Object()); .AddJsonValue(JsonValue.Object());
var actual = FieldConverters.ForValues(ValueConverters.EncodeJson(TestUtils.DefaultSerializer))(input, field); var actual = FieldConverters.ForValues(ValueConverters.EncodeJson(TestUtils.DefaultSerializer))(input, field);
@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input = var input =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("field1", 100) .Add("field1", 100)
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var expected = var expected =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("1", 100))); .Add("1", 100)));
@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var input = var input =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("field1", 100) .Add("field1", 100)
@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var expected = var expected =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("field1", 100))); .Add("field1", 100)));
@ -479,11 +479,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2")); .AddJsonValue(JsonValue.Array("1", "2"));
var expected = var expected =
new ContentFieldData() 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); 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 = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array( .AddJsonValue(JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("assets", JsonValue.Array("1", "2")))); .Add("assets", JsonValue.Array("1", "2"))));
var expected = var expected =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array( .AddJsonValue(JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("assets", JsonValue.Array("url/to/1", "url/to/2")))); .Add("assets", JsonValue.Array("url/to/1", "url/to/2"))));
@ -521,11 +521,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2")); .AddJsonValue(JsonValue.Array("1", "2"));
var expected = var expected =
new ContentFieldData() 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); var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), assetUrlGenerator)(source, field);
@ -541,13 +541,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array( .AddJsonValue(JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("assets", JsonValue.Array("1", "2")))); .Add("assets", JsonValue.Array("1", "2"))));
var expected = var expected =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array( .AddJsonValue(JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("assets", JsonValue.Array("url/to/1", "url/to/2")))); .Add("assets", JsonValue.Array("url/to/1", "url/to/2"))));
@ -563,11 +563,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2")); .AddJsonValue(JsonValue.Array("1", "2"));
var expected = var expected =
new ContentFieldData() 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); 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 = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2")); .AddJsonValue(JsonValue.Array("1", "2"));
var expected = var expected =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array("1", "2")); .AddJsonValue(JsonValue.Array("1", "2"));
var result = FieldConverters.ResolveAssetUrls(null, assetUrlGenerator)(source, field); 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 NodaTime;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; 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.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; 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 #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 Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly Schema schema; private readonly Schema schema;
public ContentEnrichmentTests() public DefaultValuesTests()
{ {
schema = schema =
new Schema("my-schema") new Schema("my-schema")
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 456)); .AddValue("iv", 456));
data.Enrich(schema, languagesConfig.ToResolver()); data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
Assert.Equal(456, ((JsonScalar<double>)data["my-number"]!["iv"]).Value); Assert.Equal(456, ((JsonScalar<double>)data["my-number"]!["iv"]).Value);
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 456)); .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"]!["de"].ToString());
Assert.Equal("en-string", data["my-string"]!["en"].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, Fields.DateTime(1, "1", Partitioning.Invariant,
new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); 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] [Fact]

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
@ -21,39 +22,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
{ {
public class ReferenceExtractionTests public class ReferenceExtractionTests
{ {
private readonly Guid schemaId = Guid.NewGuid();
private readonly Schema schema; private readonly Schema schema;
public ReferenceExtractionTests() public ReferenceExtractionTests()
{ {
schema = schema =
new Schema("my-schema") new Schema("my-schema")
.AddNumber(1, "field1", Partitioning.Language) .AddReferences(1, "references", Partitioning.Invariant)
.AddNumber(2, "field2", Partitioning.Invariant) .AddAssets(2, "assets", Partitioning.Invariant)
.AddNumber(3, "field3", Partitioning.Invariant) .AddArray(3, "array", Partitioning.Invariant, a => a
.AddAssets(5, "assets1", Partitioning.Invariant) .AddAssets(31, "nested"));
.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);
} }
[Fact] [Fact]
@ -64,11 +42,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var input = var input =
new NamedContentData() new NamedContentData()
.AddField("assets1", .AddField("assets",
new ContentFieldData() 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); Assert.Equal(new[] { id1, id2 }, ids);
} }
@ -79,53 +59,44 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var id1 = Guid.NewGuid(); var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid(); var id2 = Guid.NewGuid();
var input = var source =
new IdContentData() new NamedContentData()
.AddField(5, .AddField("references",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array(id1.ToString(), id2.ToString()))); .AddJsonValue(JsonValue.Array(id1, id2)))
.AddField("assets",
var converter = FieldConverters.ForValues(ValueReferencesConverter.CleanReferences(new[] { id2 })); new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1)))
var actual = input.ConvertId2Id(schema, converter); .AddField("array",
new ContentFieldData()
var cleanedValue = (JsonArray)actual[5]!["iv"]; .AddJsonValue(
JsonValue.Array(
Assert.Equal(1, cleanedValue.Count); JsonValue.Object()
Assert.Equal(id1.ToString(), cleanedValue[0].ToString()); .Add("nested", JsonValue.Array(id1, id2)))));
}
[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);
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] var converter = FieldConverters.ForValues(cleaner);
public void Should_return_empty_list_from_assets_field_for_referenced_ids_when_other_type() var converterNested = FieldConverters.ForNestedName2Name(cleaner);
{
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant);
var result = sut.GetReferencedIds(JsonValue.Create("invalid")).ToArray(); var actual = source.ConvertName2Name(schema, converter, converterNested);
Assert.Empty(result); Assert.Equal(expected, actual);
} }
[Fact] [Fact]
@ -138,171 +109,127 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
Assert.Empty(result); Assert.Empty(result);
} }
[Fact] [Theory]
public void Should_return_null_from_assets_field_when_removing_references_from_null_array() [MemberData(nameof(ReferencingNestedFields))]
{ public void Should_return_ids_from_nested_field(NestedField field)
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()
{ {
var id1 = Guid.NewGuid(); var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid(); var id2 = Guid.NewGuid();
var sut = Fields.Assets(1, "my-asset", Partitioning.Invariant); var arrayField = Fields.Array(1, "my-array", Partitioning.Invariant, field);
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 value = var value =
JsonValue.Array( JsonValue.Array(
JsonValue.Object() 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] [Theory]
public void Should_return_ids_from_references_field() [MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_item_is_invalid(IField field)
{ {
var id1 = Guid.NewGuid(); var result = field.GetReferencedIds(JsonValue.Array("invalid")).ToArray();
var id2 = Guid.NewGuid();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant, Assert.Empty(result);
new ReferencesFieldProperties { SchemaId = schemaId }); }
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] [Theory]
public void Should_return_ids_from_references_field_without_schema_id() [MemberData(nameof(ReferencingFields))]
public void Should_return_empty_list_from_field_when_value_is_empty(IField field)
{ {
var id1 = Guid.NewGuid(); var result = field.GetReferencedIds(JsonValue.Array()).ToArray();
var id2 = Guid.NewGuid();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant, Assert.Empty(result);
new ReferencesFieldProperties { SchemaId = schemaId }); }
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] [Theory]
public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() [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, var result = field.GetReferencedIds(null).ToArray();
new ReferencesFieldProperties { SchemaId = schemaId });
var result = sut.GetReferencedIds(JsonValue.Null).ToArray(); Assert.Empty(result);
Assert.Equal(new[] { schemaId }, result);
} }
[Fact] [Theory]
public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() [MemberData(nameof(ReferencingFields))]
public void Should_return_ids_from_field(IField field)
{ {
var sut = Fields.References(1, "my-refs", Partitioning.Invariant, var id1 = Guid.NewGuid();
new ReferencesFieldProperties { SchemaId = schemaId }); 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] [Theory]
public void Should_return_null_from_references_field_when_removing_references_from_null_array() [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 = ValueReferencesConverter.CleanReferences(RandomIds())(JsonValue.Null, field);
var result = sut.CleanReferences(JsonValue.Null, null);
Assert.Equal(JsonValue.Null, result); Assert.Equal(JsonValue.Null, result);
} }
[Fact] [Theory]
public void Should_remove_deleted_references_from_references_field() [MemberData(nameof(ReferencingFields))]
public void Should_remove_deleted_ids_from_field(IField field)
{ {
var id1 = Guid.NewGuid(); var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid(); var id2 = Guid.NewGuid();
var sut = Fields.References(1, "my-refs", Partitioning.Invariant, var value = CreateValue(id1, id2);
new ReferencesFieldProperties { SchemaId = schemaId });
var result = sut.CleanReferences(CreateValue(id1, id2), HashSet.Of(id2)); var result = ValueReferencesConverter.CleanReferences(HashSet.Of(id1))(value, field);
Assert.Equal(CreateValue(id1), result); Assert.Equal(CreateValue(id1), result);
} }
[Fact] public static IEnumerable<object[]> ReferencingNestedFields()
public void Should_remove_all_references_from_references_field_when_schema_is_removed()
{ {
var id1 = Guid.NewGuid(); yield return new object[] { Fields.References(1, "my-refs") };
var id2 = Guid.NewGuid(); yield return new object[] { Fields.Assets(1, "my-assets") };
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);
} }
[Fact] public static IEnumerable<object[]> ReferencingFields()
public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove()
{ {
var id1 = Guid.NewGuid(); yield return new object[] { Fields.References(1, "my-refs", Partitioning.Invariant) };
var id2 = Guid.NewGuid(); yield return new object[] { Fields.Assets(1, "my-assets", Partitioning.Invariant) };
}
var sut = Fields.References(1, "my-refs", Partitioning.Invariant);
var value = CreateValue(id1, id2);
var result = sut.CleanReferences(value, HashSet.Of(Guid.NewGuid()));
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() new NamedContentData()
.AddField("city", .AddField("city",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array())) .AddJsonValue(JsonValue.Array()))
}; };
var result = sut.Format(script, @event); var result = sut.Format(script, @event);
@ -301,7 +301,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData() new NamedContentData()
.AddField("city", .AddField("city",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) .AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
}; };
var result = sut.Format(script, @event); var result = sut.Format(script, @event);
@ -339,8 +339,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData() new NamedContentData()
.AddField("city", .AddField("city",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array( .AddJsonValue(JsonValue.Array("Berlin")))
"Berlin")))
}; };
var result = sut.Format(script, @event); var result = sut.Format(script, @event);
@ -359,7 +358,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData() new NamedContentData()
.AddField("city", .AddField("city",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) .AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
}; };
var result = sut.Format(script, @event); var result = sut.Format(script, @event);
@ -378,7 +377,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData() new NamedContentData()
.AddField("city", .AddField("city",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) .AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
}; };
var result = sut.Format(script, @event); 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() new NamedContentData()
.AddField("number", .AddField("number",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array(1.0, 2.0))); .AddJsonValue(JsonValue.Array(1.0, 2.0)));
var expected = var expected =
new NamedContentData() new NamedContentData()
.AddField("number", .AddField("number",
new ContentFieldData() 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]"); 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() new NamedContentData()
.AddField("number", .AddField("number",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("lat", 1.0))); .AddJsonValue(JsonValue.Object().Add("lat", 1.0)));
var expected = var expected =
new NamedContentData() new NamedContentData()
.AddField("number", .AddField("number",
new ContentFieldData() 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 }"); 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() new NamedContentData()
.AddField("obj", .AddField("obj",
new ContentFieldData() 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.invalid = 1"));
Assert.Throws<JavaScriptException>(() => ExecuteScript(original, "data.obj.iv.readonly = 2")); Assert.Throws<JavaScriptException>(() => ExecuteScript(original, "data.obj.iv.readonly = 2"));
@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new NamedContentData() new NamedContentData()
.AddField("obj", .AddField("obj",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array())); .AddJsonValue(JsonValue.Array()));
ExecuteScript(original, "data.obj.iv[0] = 1"); 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.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
@ -44,8 +45,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
var oldData = GenerateData("o_raw"); var oldData = GenerateData("o_raw");
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), 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.Is("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("o_raw2_1", "o_raw2_2", "o_raw4")))
.Returns(new Dictionary<string, string> .Returns(new Dictionary<string, string>
{ {
["n_raw2_2"] = "id2_2", ["n_raw2_2"] = "id2_2",
@ -65,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
var newData = GenerateData("name"); var newData = GenerateData("name");
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), 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())) A<HashSet<string>>.That.IsEmpty()))
.Returns(new Dictionary<string, string> .Returns(new Dictionary<string, string>
{ {
@ -86,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
var newData = GenerateData("id"); var newData = GenerateData("id");
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), 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())) A<HashSet<string>>.That.IsEmpty()))
.Returns(new Dictionary<string, string> .Returns(new Dictionary<string, string>
{ {
@ -114,16 +115,16 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags
return new NamedContentData() return new NamedContentData()
.AddField("tags1", .AddField("tags1",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array($"{prefix}1"))) .AddJsonValue(JsonValue.Array($"{prefix}1")))
.AddField("tags2", .AddField("tags2",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) .AddJsonValue(JsonValue.Array($"{prefix}2_1", $"{prefix}2_2")))
.AddField("string", .AddField("string",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", $"{prefix}stringValue")) .AddValue("iv", $"{prefix}stringValue"))
.AddField("array", .AddField("array",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("nestedTags1", JsonValue.Array($"{prefix}3")) .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); await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected array of objects." });
} }
[Fact] [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); await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected array of guid strings." });
} }
[Fact] [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); await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected boolean." });
} }
private static IJsonValue CreateValue(bool? v) 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() new NamedContentData()
.AddField("my-field", .AddField("my-field",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object(), JsonValue.Object(),
JsonValue.Object().Add("my-nested", 1), JsonValue.Object().Add("my-nested", 1),
@ -387,7 +387,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new NamedContentData() new NamedContentData()
.AddField("my-field", .AddField("my-field",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object()))); 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); await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo( 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] [Fact]
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(JsonValue.Create(123), errors); await sut.ValidateAsync(JsonValue.Create(123), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected string." });
} }
private static Instant FutureDays(int days) private static Instant FutureDays(int days)
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static IJsonValue CreateValue(Instant v) private static IJsonValue CreateValue(Instant v)
{ {
return JsonValue.Create(v.ToString()); return JsonValue.Create(v);
} }
private static RootField<DateTimeFieldProperties> Field(DateTimeFieldProperties properties) 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); await sut.ValidateAsync(geolocation, errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Latitude must be between -90 and 90." });
} }
[Fact] [Fact]
@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(geolocation, errors); await sut.ValidateAsync(geolocation, errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Longitude must be between -180 and 180." });
} }
[Fact] [Fact]
@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
await sut.ValidateAsync(geolocation, errors); await sut.ValidateAsync(geolocation, errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Geolocation can only have latitude and longitude property." });
} }
[Fact] [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); await sut.ValidateAsync(JsonValue.Create("Invalid"), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected number." });
} }
[Fact] [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()); await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context());
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected array of guid strings." });
} }
[Fact] [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); await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Not a valid value." }); new[] { "Invalid json type, expected array of strings." });
} }
[Fact] [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 = var data =
new NamedContentData() new NamedContentData()
.AddField("my-array", new ContentFieldData() .AddField("my-array", new ContentFieldData()
.AddValue("iv", .AddJsonValue(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("my-ui", null)))); .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)) A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>.Ignored, requestContext))
.ReturnsLazily(() => SimpleMapper.Map(asset.Snapshot, new AssetEntity())); .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>()); .Returns(new List<IEnrichedAssetEntity>());
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
@ -301,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
FileSize = fileSize 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 }); .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 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> .Returns(new Dictionary<string, string>
{ {
["id1"] = "name1", ["id1"] = "name1",
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AppId = appId AppId = appId
}; };
var result = await sut.EnrichAsync(source, requestContext.Clone().WithNoAssetEnrichment()); var result = await sut.EnrichAsync(source, requestContext.Clone().WithoutAssetEnrichment());
Assert.Null(result.TagNames); Assert.Null(result.TagNames);
} }
@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AppId = appId 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> .Returns(new Dictionary<string, string>
{ {
["id1"] = "name1", ["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); 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)); .Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext)) 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 System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;

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

@ -6,11 +6,11 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents; 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.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -20,179 +20,89 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public class ContentEnricherTests public class ContentEnricherTests
{ {
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); 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 ISchemaEntity schema;
private readonly Context requestContext; private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); 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); public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A<Context>.Ignored, schemaId.Id.ToString())) foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
.Returns(schema); {
Schema = await schemas(group.Key);
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);
} }
[Fact] public ContentEnricherTests()
public async Task Should_enrich_with_reference_fields()
{ {
var ctx = new Context(Mocks.FrontendUser(), requestContext.App); requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
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] [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)) var step1 = A.Fake<IContentEnricherStep>();
.Returns(new StatusInfo(Status.Published, StatusColors.Published)); 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); await sut.EnrichAsync(source, requestContext);
}
[Fact]
public async Task Should_enrich_with_schema_names()
{
var ctx = new Context(Mocks.FrontendUser(), requestContext.App);
var source = PublishedContent(); A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>.Ignored, A<ProvideSchema>.Ignored))
.MustNotHaveHappened();
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source, ctx);
Assert.Equal("my-schema", result.SchemaName); A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>.Ignored, A<ProvideSchema>.Ignored))
Assert.Equal("my-schema", result.SchemaDisplayName); .MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task Should_not_enrich_with_schema_names_when_not_frontend() public async Task Should_invoke_steps()
{ {
var source = PublishedContent(); var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source)) var step1 = A.Fake<IContentEnricherStep>();
.Returns(new StatusInfo(Status.Published, StatusColors.Published)); var step2 = A.Fake<IContentEnricherStep>();
var result = await sut.EnrichAsync(source, requestContext);
Assert.Null(result.SchemaName);
Assert.Null(result.SchemaDisplayName);
}
[Fact] var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
public async Task Should_enrich_content_with_status_color()
{
var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source)) await sut.EnrichAsync(source, requestContext);
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
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] [Fact]
public async Task Should_enrich_content_with_default_color_if_not_found() public async Task Should_provide_and_cache_schema()
{ {
var source = PublishedContent(); var source = PublishedContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(source)) var step1 = new ResolveSchema();
.Returns(Task.FromResult<StatusInfo>(null!)); var step2 = new ResolveSchema();
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
};
A.CallTo(() => contentWorkflow.GetInfoAsync(source1)) var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(source, requestContext);
Assert.Equal(StatusColors.Published, result[0].StatusColor); Assert.Same(schema, step1.Schema);
Assert.Equal(StatusColors.Published, result[1].StatusColor); Assert.Same(schema, step1.Schema);
A.CallTo(() => contentWorkflow.GetInfoAsync(A<IContentEntity>.Ignored)) A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString()))
.MustHaveHappenedOnceExactly(); .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 System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
@ -35,7 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAssetUrlGenerator urlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>(); private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>(); private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>();
@ -79,7 +77,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
sut = new ContentQueryService( sut = new ContentQueryService(
appProvider, appProvider,
urlGenerator,
contentEnricher, contentEnricher,
contentRepository, contentRepository,
contentVersionLoader, 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.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets; 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.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -22,18 +24,17 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries 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 IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>(); private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly Context requestContext; 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)); 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)) A.CallTo(() => assetUrlGenerator.GenerateUrl(A<string>.Ignored))
.ReturnsLazily(new Func<string, string>(id => $"url/to/{id}")); .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); if (x == schemaId.Id)
{
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, id.Id.ToString())) return Task.FromResult(Mocks.Schema(appId, schemaId, schemaDef));
.Returns(schemaEntity); }
} else
{
SetupSchema(schemaId, schemaDef); throw new DomainObjectNotFoundException(x.ToString(), typeof(ISchemaEntity));
}
};
sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy<IContentQueryService>(() => contentQuery), contentWorkflow); sut = new ResolveAssets(assetUrlGenerator, assetQuery);
} }
[Fact] [Fact]
@ -78,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown); var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown);
var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown); var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown);
var source = new IContentEntity[] var source = new[]
{ {
CreateContent( CreateContent(
new[] { document1.Id, image1.Id }, new[] { document1.Id, image1.Id },
@ -88,17 +91,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new[] { document2.Id, image2.Id }) 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)); .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.Id, enriched1.CacheDependencies);
Assert.Contains(image1.Version, 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.Id, enriched2.CacheDependencies);
Assert.Contains(image2.Version, 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 document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown);
var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown); var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown);
var source = new IContentEntity[] var source = new[]
{ {
CreateContent( CreateContent(
new[] { document1.Id, image1.Id }, new[] { document1.Id, image1.Id },
@ -123,20 +126,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new[] { document2.Id, image2.Id }) 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)); .Returns(ResultList.CreateFrom(4, image1, image2, document1, document2));
var enriched = await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(requestContext, source, schemaProvider);
Assert.Equal( Assert.Equal(
new NamedContentData() new NamedContentData()
.AddField("asset1", .AddField("asset1",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", .AddValue("iv", $"url/to/{image1.Id}"))
$"url/to/{image1.Id}"))
.AddField("asset2", .AddField("asset2",
new ContentFieldData()), new ContentFieldData()),
enriched.ElementAt(0).ReferenceData); source[0].ReferenceData);
Assert.Equal( Assert.Equal(
new NamedContentData() new NamedContentData()
@ -144,22 +146,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new ContentFieldData()) new ContentFieldData())
.AddField("asset2", .AddField("asset2",
new ContentFieldData() new ContentFieldData()
.AddValue("en", .AddValue("en", $"url/to/{image2.Id}")),
$"url/to/{image2.Id}")), source[1].ReferenceData);
enriched.ElementAt(1).ReferenceData);
} }
[Fact] [Fact]
public async Task Should_not_enrich_references_if_not_api_user() 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]) 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)) A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -168,14 +171,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_not_enrich_references_if_disabled() public async Task Should_not_enrich_references_if_disabled()
{ {
var source = new IContentEntity[] var source = new[]
{ {
CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0]) 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)) A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -184,31 +189,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_not_invoke_query_service_if_no_assets_found() 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]) CreateContent(new Guid[0], new Guid[0])
}; };
var enriched = await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(requestContext, source, schemaProvider);
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(source, requestContext); Assert.NotNull(source[0].ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored)) A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
private IEnrichedContentEntity CreateContent(Guid[] assets1, Guid[] assets2) private ContentEntity CreateContent(Guid[] assets1, Guid[] assets2)
{ {
return new ContentEntity 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 FakeItEasy;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas; 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.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -22,20 +22,18 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries 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 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> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> refSchemaId1 = NamedId.Of(Guid.NewGuid(), "my-ref1"); 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> refSchemaId2 = NamedId.Of(Guid.NewGuid(), "my-ref2");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ProvideSchema schemaProvider;
private readonly Context requestContext; 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)); requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE));
@ -65,19 +63,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}) })
.ConfigureFieldsInLists("ref1", "ref2"); .ConfigureFieldsInLists("ref1", "ref2");
void SetupSchema(NamedId<Guid> id, Schema def) schemaProvider = x =>
{ {
var schemaEntity = Mocks.Schema(appId, id, def); if (x == schemaId.Id)
{
A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, id.Id.ToString())) return Task.FromResult(Mocks.Schema(appId, schemaId, schemaDef));
.Returns(schemaEntity); }
} else if (x == refSchemaId1.Id)
{
SetupSchema(schemaId, schemaDef); return Task.FromResult(Mocks.Schema(appId, refSchemaId1, refSchemaDef));
SetupSchema(refSchemaId1, refSchemaDef); }
SetupSchema(refSchemaId2, 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] [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_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2);
var ref2_2 = CreateRefContent(Guid.NewGuid(), 4, "ref2_2", 29, 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_1.Id }, new[] { ref2_1.Id }),
CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.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))) 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)); .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(refSchemaId1.Id, enriched1.CacheDependencies);
Assert.Contains(refSchemaId2.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.Id, enriched1.CacheDependencies);
Assert.Contains(ref2_1.Version, 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(refSchemaId1.Id, enriched2.CacheDependencies);
Assert.Contains(refSchemaId2.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_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2);
var ref2_2 = CreateRefContent(Guid.NewGuid(), 3, "ref2_2", 29, 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_1.Id }, new[] { ref2_1.Id }),
CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.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)); .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( Assert.Equal(
new NamedContentData() new NamedContentData()
@ -155,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object() JsonValue.Object()
.Add("en", "ref2_1, 23") .Add("en", "ref2_1, 23")
.Add("de", "ref2_1, 23"))), .Add("de", "ref2_1, 23"))),
enriched.ElementAt(0).ReferenceData); source[0].ReferenceData);
Assert.Equal( Assert.Equal(
new NamedContentData() new NamedContentData()
@ -171,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object() JsonValue.Object()
.Add("en", "ref2_2, 29") .Add("en", "ref2_2, 29")
.Add("de", "ref2_2, 29"))), .Add("de", "ref2_2, 29"))),
enriched.ElementAt(1).ReferenceData); source[1].ReferenceData);
} }
[Fact] [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_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2);
var ref2_2 = CreateRefContent(Guid.NewGuid(), 4, "ref2_2", 29, 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_1.Id }, new[] { ref2_1.Id, ref2_2.Id }),
CreateContent(new[] { ref1_2.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)); .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( Assert.Equal(
new NamedContentData() new NamedContentData()
@ -207,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object() JsonValue.Object()
.Add("en", "2 Reference(s)") .Add("en", "2 Reference(s)")
.Add("de", "2 Reference(s)"))), .Add("de", "2 Reference(s)"))),
enriched.ElementAt(0).ReferenceData); source[0].ReferenceData);
Assert.Equal( Assert.Equal(
new NamedContentData() new NamedContentData()
@ -223,20 +229,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
JsonValue.Object() JsonValue.Object()
.Add("en", "2 Reference(s)") .Add("en", "2 Reference(s)")
.Add("de", "2 Reference(s)"))), .Add("de", "2 Reference(s)"))),
enriched.ElementAt(1).ReferenceData); source[1].ReferenceData);
} }
[Fact] [Fact]
public async Task Should_not_enrich_references_if_not_api_user() 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]) 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)) A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -245,14 +253,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_not_enrich_references_if_disabled() public async Task Should_not_enrich_references_if_disabled()
{ {
var source = new IContentEntity[] var source = new[]
{ {
CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0]) 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)) A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -261,31 +271,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_not_invoke_query_service_if_no_references_found() 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]) CreateContent(new Guid[0], new Guid[0])
}; };
var enriched = await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(requestContext, source, schemaProvider);
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(source, requestContext); Assert.NotNull(source[0].ReferenceData);
A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored)) A.CallTo(() => contentQuery.QueryAsync(A<Context>.Ignored, A<List<Guid>>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
private IEnrichedContentEntity CreateContent(Guid[] ref1, Guid[] ref2) private ContentEntity CreateContent(Guid[] ref1, Guid[] ref2)
{ {
return new ContentEntity 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); 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); 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] [Fact]
public void Should_throw_exception_when_creation_value_from_invalid_type() 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] [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 try
{ {
var schemas = ClientManager.CreateSchemasClient(); await Schemas.PostSchemaAsync(AppName, new CreateSchemaDto
await schemas.PostSchemaAsync(AppName, new CreateSchemaDto
{ {
Name = SchemaName, Name = SchemaName,
Fields = new List<UpsertSchemaFieldDto> Fields = new List<UpsertSchemaFieldDto>

Loading…
Cancel
Save