Browse Source

Started to fix the conversion flow. (#519)

* Started to fix the conversion flow.

* Finalized (hopefully)

* Improved tests and fixed a bug with assets resolver.
pull/520/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
59951d28d7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/Squidex.ruleset
  2. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
  3. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  4. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  6. 151
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs
  7. 308
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  8. 53
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldIdentifier.cs
  9. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs
  10. 142
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs
  11. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  12. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs
  13. 16
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/DataConverter.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  15. 16
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  16. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs
  17. 19
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  18. 2
      backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs
  19. 10
      backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs
  20. 5
      backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs
  21. 10
      backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs
  22. 5
      backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs
  23. 19
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs
  24. 20
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs
  25. 74
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs
  26. 273
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs
  27. 90
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs
  28. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs
  29. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs
  30. 33
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs
  31. 86
      backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs

1
backend/Squidex.ruleset

@ -86,6 +86,7 @@
<Rule Id="AD0001" Action="None" /> <Rule Id="AD0001" Action="None" />
</Rules> </Rules>
<Rules AnalyzerId="Roslyn.Core" RuleNamespace="Microsoft.CodeAnalysis.Diagnostics"> <Rules AnalyzerId="Roslyn.Core" RuleNamespace="Microsoft.CodeAnalysis.Diagnostics">
<Rule Id="IDE0070" Action="None" />
<Rule Id="IDE0032" Action="None" /> <Rule Id="IDE0032" Action="None" />
<Rule Id="IDE0042" Action="None" /> <Rule Id="IDE0042" Action="None" />
</Rules> </Rules>

5
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs

@ -25,6 +25,11 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
} }
protected ContentData(ContentData<T> source, IEqualityComparer<T> comparer)
: base(source, comparer)
{
}
protected ContentData(int capacity, IEqualityComparer<T> comparer) protected ContentData(int capacity, IEqualityComparer<T> comparer)
: base(capacity, comparer) : base(capacity, comparer)
{ {

22
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs

@ -19,6 +19,16 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
} }
public ContentFieldData(ContentFieldData source)
: base(source, StringComparer.OrdinalIgnoreCase)
{
}
public ContentFieldData(int capacity)
: base(capacity, StringComparer.OrdinalIgnoreCase)
{
}
public ContentFieldData AddValue(object? value) public ContentFieldData AddValue(object? value)
{ {
return AddJsonValue(JsonValue.Create(value)); return AddJsonValue(JsonValue.Create(value));
@ -52,6 +62,18 @@ namespace Squidex.Domain.Apps.Core.Contents
return this; return this;
} }
public ContentFieldData Clone()
{
var clone = new ContentFieldData(Count);
foreach (var (key, value) in this)
{
clone[key] = value?.Clone()!;
}
return clone;
}
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
return Equals(obj as ContentFieldData); return Equals(obj as ContentFieldData);

17
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs

@ -17,6 +17,11 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
} }
public NamedContentData(NamedContentData source)
: base(source, StringComparer.Ordinal)
{
}
public NamedContentData(int capacity) public NamedContentData(int capacity)
: base(capacity, StringComparer.Ordinal) : base(capacity, StringComparer.Ordinal)
{ {
@ -46,6 +51,18 @@ namespace Squidex.Domain.Apps.Core.Contents
return this; return this;
} }
public NamedContentData Clone()
{
var clone = new NamedContentData(Count);
foreach (var (key, value) in this)
{
clone[key] = value?.Clone()!;
}
return clone;
}
public bool Equals(NamedContentData other) public bool Equals(NamedContentData other)
{ {
return base.Equals(other); return base.Equals(other);

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public static class Fields public static class Fields
{ {
public static RootField<ArrayFieldProperties> Array(long id, string name, Partitioning partitioning, params NestedField[] fields) public static ArrayField Array(long id, string name, Partitioning partitioning, params NestedField[] fields)
{ {
return new ArrayField(id, name, partitioning, fields); return new ArrayField(id, name, partitioning, fields);
} }

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

@ -5,35 +5,45 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System.Linq;
using System.Collections.Generic;
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
{ {
public static class ContentConverter public static class ContentConverter
{ {
private static readonly Func<IRootField, string> KeyNameResolver = f => f.Name;
private static readonly Func<IRootField, long> KeyIdResolver = f => f.Id;
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);
var result = new NamedContentData(content.Count); var result = new NamedContentData(content.Count);
return ConvertInternal(content, result, schema.FieldsById, KeyNameResolver, converters); foreach (var (fieldId, data) in content)
} {
if (data == null || !schema.FieldsById.TryGetValue(fieldId, out var field))
{
continue;
}
public static IdContentData ConvertId2Id(this IdContentData content, Schema schema, params FieldConverter[] converters) ContentFieldData? newData = data;
{
Guard.NotNull(schema);
var result = new IdContentData(content.Count); ConvertArray(newData, field, FieldIdentifier.ById, FieldIdentifier.ByName);
if (newData != null)
{
newData = ConvertData(converters, field, newData);
}
if (newData != null)
{
result.Add(field.Name, newData);
}
}
return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters); return result;
} }
public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters) public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters)
@ -42,60 +52,117 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
var result = new NamedContentData(content.Count); var result = new NamedContentData(content.Count);
return ConvertInternal(content, result, schema.FieldsByName, KeyNameResolver, converters); foreach (var (fieldName, data) in content)
{
if (data == null || !schema.FieldsByName.TryGetValue(fieldName, out var field))
{
continue;
}
ContentFieldData? newData = data;
if (newData != null)
{
newData = ConvertData(converters, field, newData);
}
if (newData != null)
{
result.Add(field.Name, newData);
}
}
return result;
} }
public static IdContentData ConvertName2Id(this NamedContentData content, Schema schema, params FieldConverter[] converters) public static IdContentData ConvertName2IdCloned(this NamedContentData content, Schema schema, params FieldConverter[] converters)
{ {
Guard.NotNull(schema); Guard.NotNull(schema);
var result = new IdContentData(content.Count); var result = new IdContentData(content.Count);
return ConvertInternal(content, result, schema.FieldsByName, KeyIdResolver, converters); foreach (var (fieldName, data) in content)
{
if (data == null || !schema.FieldsByName.TryGetValue(fieldName, out var field))
{
continue;
}
ContentFieldData? newData = data.Clone();
if (newData != null)
{
newData = ConvertData(converters, field, newData);
}
if (newData != null)
{
ConvertArray(newData, field, FieldIdentifier.ByName, FieldIdentifier.ById);
}
if (newData != null)
{
result.Add(field.Id, newData);
}
}
return result;
} }
private static TDict2 ConvertInternal<TKey1, TKey2, TDict1, TDict2>( private static ContentFieldData? ConvertData(FieldConverter[] converters, IRootField field, ContentFieldData data)
TDict1 source,
TDict2 target,
IReadOnlyDictionary<TKey1, RootField> fields,
Func<IRootField, TKey2> targetKey, params FieldConverter[] converters)
where TDict1 : IDictionary<TKey1, ContentFieldData?>
where TDict2 : IDictionary<TKey2, ContentFieldData?>
where TKey1 : notnull
where TKey2 : notnull
{ {
foreach (var (fieldName, value) in source) if (converters != null)
{ {
if (!fields.TryGetValue(fieldName, out var field)) for (var i = 0; i < converters.Length; i++)
{ {
continue; data = converters[i](data!, field)!;
if (data == null)
{
break;
}
} }
}
var newValue = value; return data;
}
if (newValue != null) private static void ConvertArray(ContentFieldData data, IRootField? field, FieldIdentifier sourceIdentifier, FieldIdentifier targetIdentifier)
{
if (field is IArrayField arrayField)
{
foreach (var (key, value) in data)
{ {
if (converters != null) if (value is JsonArray array)
{ {
foreach (var converter in converters) foreach (var nested in array.OfType<JsonObject>())
{ {
newValue = converter(newValue, field); var properties = nested.ToList();
if (newValue == null) nested.Clear();
foreach (var (nestedKey, nestedValue) in properties)
{ {
break; if (nestedValue == null)
{
continue;
}
var nestedField = sourceIdentifier.GetField(arrayField, nestedKey);
if (nestedField == null)
{
continue;
}
var targetKey = targetIdentifier.GetStringKey(nestedField);
nested[targetKey] = nestedValue;
} }
} }
} }
} }
if (newValue != null)
{
target.Add(targetKey(field), newValue);
}
} }
return target;
} }
} }
} }

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

@ -23,162 +23,51 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
public static class FieldConverters public static class FieldConverters
{ {
private delegate string FieldKeyResolver(IField field); public static readonly FieldConverter Noop = (data, field) => data;
private static readonly FieldKeyResolver KeyNameResolver = f => f.Name; public static readonly FieldConverter ExcludeHidden = (data, field) =>
private static readonly FieldKeyResolver KeyIdResolver = f => f.Id.ToString();
private delegate IField? FieldResolver(IArrayField field, string key);
private static IField? FieldByIdResolver(IArrayField array, string key)
{ {
if (key != null && long.TryParse(key, out var id)) return field.IsForApi() ? data : null;
{ };
return array.FieldsById.GetOrDefault(id);
}
return null;
}
private static IField? FieldByNameResolver(IArrayField array, string key) public static readonly FieldConverter ExcludeChangedTypes = (data, field) =>
{ {
if (key != null) foreach (var value in data.Values)
{ {
return array.FieldsByName.GetOrDefault(key); if (value.Type == JsonValueType.Null)
}
return null;
}
public static FieldConverter ExcludeHidden()
{
return (data, field) => !field.IsForApi() ? null : data;
}
public static FieldConverter ExcludeChangedTypes()
{
return (data, field) =>
{
foreach (var value in data.Values)
{ {
if (value.Type == JsonValueType.Null) continue;
{
continue;
}
try
{
var (_, error) = JsonValueConverter.ConvertValue(field, value);
if (error != null)
{
return null;
}
}
catch
{
return null;
}
} }
return data; try
};
}
public static FieldConverter ResolveAssetUrls(IReadOnlyCollection<string>? fields, IUrlGenerator urlGenerator)
{
if (fields?.Any() != true)
{
return (data, field) => data;
}
bool ShouldHandle(IField field, IField? parent = null)
{
if (field is IField<AssetsFieldProperties>)
{ {
if (fields.Contains("*")) var (_, error) = JsonValueConverter.ConvertValue(field, value);
{
return true;
}
if (parent == null) if (error != null)
{
return fields.Contains(field.Name);
}
else
{ {
return fields.Contains($"{parent.Name}.{field.Name}"); return null;
} }
} }
catch
return false;
}
void Resolve(IJsonValue value)
{
if (value is JsonArray array)
{ {
for (var i = 0; i < array.Count; i++) return null;
{
var id = array[i].ToString();
array[i] = JsonValue.Create(urlGenerator.AssetContent(Guid.Parse(id)));
}
} }
} }
return (data, field) => return data;
{ };
if (ShouldHandle(field))
{
foreach (var partition in data)
{
Resolve(partition.Value);
}
}
else if (field is IArrayField arrayField)
{
foreach (var partition in data)
{
if (partition.Value is JsonArray array)
{
for (var i = 0; i < array.Count; i++)
{
if (array[i] is JsonObject arrayItem)
{
foreach (var (key, value) in arrayItem)
{
if (arrayField.FieldsByName.TryGetValue(key, out var nestedField) && ShouldHandle(nestedField, field))
{
Resolve(value);
}
}
}
}
}
}
}
return data;
};
}
public static FieldConverter ResolveInvariant(LanguagesConfig languages) public static FieldConverter ResolveInvariant(LanguagesConfig languages)
{ {
var codeForInvariant = InvariantPartitioning.Key; var codeForInvariant = InvariantPartitioning.Key;
var codeForMasterLanguage = languages.Master;
return (data, field) => return (data, field) =>
{ {
if (field.Partitioning.Equals(Partitioning.Invariant)) if (field.Partitioning.Equals(Partitioning.Invariant) && !data.ContainsKey(codeForInvariant))
{ {
var result = new ContentFieldData(); var result = new ContentFieldData(1);
if (data.TryGetValue(codeForInvariant, out var value)) if (data.TryGetValue(languages.Master, out var value))
{
result[codeForInvariant] = value;
}
else if (data.TryGetValue(codeForMasterLanguage, out value))
{ {
result[codeForInvariant] = value; result[codeForInvariant] = value;
} }
@ -202,21 +91,20 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
if (field.Partitioning.Equals(Partitioning.Language)) if (field.Partitioning.Equals(Partitioning.Language))
{ {
var result = new ContentFieldData(); if (data.TryGetValue(codeForInvariant, out var value))
foreach (var languageCode in languages.AllKeys)
{ {
if (data.TryGetValue(languageCode, out var value)) var result = new ContentFieldData
{
result[languageCode] = value;
}
else if (languages.IsMaster(languageCode) && data.TryGetValue(codeForInvariant, out value))
{ {
result[languageCode] = value; [languages.Master] = value
} };
return result;
} }
return result; foreach (var key in data.Keys.Where(x => !languages.AllKeys.Contains(x)).ToList())
{
data.Remove(key);
}
} }
return data; return data;
@ -231,11 +119,11 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
foreach (var languageCode in languages.AllKeys) foreach (var languageCode in languages.AllKeys)
{ {
if (!data.TryGetValue(languageCode, out var value)) if (!data.ContainsKey(languageCode))
{ {
foreach (var fallback in languages.GetPriorities(languageCode)) foreach (var fallback in languages.GetPriorities(languageCode))
{ {
if (data.TryGetValue(fallback, out value)) if (data.TryGetValue(fallback, out var value))
{ {
data[languageCode] = value; data[languageCode] = value;
break; break;
@ -253,7 +141,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
if (languages?.Any() != true) if (languages?.Any() != true)
{ {
return (data, field) => data; return Noop;
} }
var languageSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var languageSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@ -275,145 +163,45 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
if (field.Partitioning.Equals(Partitioning.Language)) if (field.Partitioning.Equals(Partitioning.Language))
{ {
var result = new ContentFieldData(); foreach (var (key, _) in data.ToList())
foreach (var languageCode in languageSet)
{ {
if (data.TryGetValue(languageCode, out var value)) if (!languageSet.Contains(key))
{ {
result[languageCode] = value; data.Remove(key);
} }
} }
return result;
} }
return data; return data;
}; };
} }
public static FieldConverter ForNestedName2Name(params ValueConverter[] converters) public static FieldConverter ForValues(params ValueConverter[] converters)
{
return ForNested(FieldByNameResolver, KeyNameResolver, converters);
}
public static FieldConverter ForNestedName2Id(params ValueConverter[] converters)
{
return ForNested(FieldByNameResolver, KeyIdResolver, converters);
}
public static FieldConverter ForNestedId2Name(params ValueConverter[] converters)
{
return ForNested(FieldByIdResolver, KeyNameResolver, converters);
}
public static FieldConverter ForNestedId2Id(params ValueConverter[] converters)
{
return ForNested(FieldByIdResolver, KeyIdResolver, converters);
}
private static FieldConverter ForNested(FieldResolver fieldResolver, FieldKeyResolver keyResolver, params ValueConverter[] converters)
{ {
return (data, field) => return (data, field) =>
{ {
if (field is IArrayField arrayField) foreach (var (key, value) in data.ToList())
{ {
var result = new ContentFieldData(); IJsonValue? newValue = value;
foreach (var (partitionKey, partitionValue) in data) for (var i = 0; i < converters.Length; i++)
{ {
if (!(partitionValue is JsonArray array)) newValue = converters[i](newValue!, field, null);
{
continue;
}
var newArray = JsonValue.Array(); if (newValue == null)
foreach (var item in array.OfType<JsonObject>())
{ {
var newItem = JsonValue.Object(); break;
foreach (var (key, value) in item)
{
var nestedField = fieldResolver(arrayField, key);
if (nestedField == null)
{
continue;
}
var newValue = value;
var isUnset = false;
if (converters != null)
{
foreach (var converter in converters)
{
newValue = converter(newValue, nestedField);
if (ReferenceEquals(newValue, Value.Unset))
{
isUnset = true;
break;
}
}
}
if (!isUnset)
{
newItem.Add(keyResolver(nestedField), newValue);
}
}
newArray.Add(newItem);
} }
result.Add(partitionKey, newArray);
} }
return result; if (newValue == null)
}
return data;
};
}
public static FieldConverter ForValues(params ValueConverter[] converters)
{
return (data, field) =>
{
if (!(field is IArrayField))
{
var result = new ContentFieldData();
foreach (var (key, value) in data)
{ {
var newValue = value; data.Remove(key);
}
var isUnset = false; else if (!ReferenceEquals(newValue, value))
{
if (converters != null) data[key] = newValue;
{
foreach (var converter in converters)
{
newValue = converter(newValue, field);
if (ReferenceEquals(newValue, Value.Unset))
{
isUnset = true;
break;
}
}
}
if (!isUnset)
{
result.Add(key, newValue);
}
} }
return result;
} }
return data; return data;

53
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldIdentifier.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public abstract class FieldIdentifier
{
public static readonly FieldIdentifier ByName = new FieldByName();
public static readonly FieldIdentifier ById = new FieldById();
public abstract IField? GetField(IArrayField arrayField, string key);
public abstract string GetStringKey(IField field);
private sealed class FieldByName : FieldIdentifier
{
public override IField? GetField(IArrayField arrayField, string key)
{
return arrayField.FieldsByName.GetValueOrDefault(key);
}
public override string GetStringKey(IField field)
{
return field.Name;
}
}
private sealed class FieldById : FieldIdentifier
{
public override IField? GetField(IArrayField arrayField, string key)
{
if (long.TryParse(key, out var id))
{
return arrayField.FieldsById.GetValueOrDefault(id);
}
return null;
}
public override string GetStringKey(IField field)
{
return field.Id.ToString();
}
}
}
}

16
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public static class Value
{
public static readonly IJsonValue Unset = JsonValue.Create("UNSET");
}
}

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

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
@ -14,13 +16,44 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
public delegate IJsonValue ValueConverter(IJsonValue value, IField field); public delegate IJsonValue? ValueConverter(IJsonValue value, IField field, IArrayField? parent = null);
public static class ValueConverters public static class ValueConverters
{ {
public static readonly ValueConverter Noop = (value, field, parent) => value;
public static readonly ValueConverter ExcludeHidden = (value, field, parent) =>
{
return field.IsForApi() ? value : null;
};
public static readonly ValueConverter ExcludeChangedTypes = (value, field, parent) =>
{
if (value.Type == JsonValueType.Null)
{
return value;
}
try
{
var (_, error) = JsonValueConverter.ConvertValue(field, value);
if (error != null)
{
return null;
}
}
catch
{
return null;
}
return value;
};
public static ValueConverter DecodeJson(IJsonSerializer jsonSerializer) public static ValueConverter DecodeJson(IJsonSerializer jsonSerializer)
{ {
return (value, field) => return (value, field, parent) =>
{ {
if (field is IField<JsonFieldProperties> && value is JsonScalar<string> s) if (field is IField<JsonFieldProperties> && value is JsonScalar<string> s)
{ {
@ -35,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
public static ValueConverter EncodeJson(IJsonSerializer jsonSerializer) public static ValueConverter EncodeJson(IJsonSerializer jsonSerializer)
{ {
return (value, field) => return (value, field, parent) =>
{ {
if (value.Type != JsonValueType.Null && field is IField<JsonFieldProperties>) if (value.Type != JsonValueType.Null && field is IField<JsonFieldProperties>)
{ {
@ -48,32 +81,103 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
}; };
} }
public static ValueConverter ExcludeHidden() public static ValueConverter ResolveAssetUrls(IReadOnlyCollection<string>? fields, IUrlGenerator urlGenerator)
{ {
return (value, field) => !field.IsForApi() ? Value.Unset : value; if (fields?.Any() != true)
} {
return Noop;
}
public static ValueConverter ExcludeChangedTypes() Func<IField, IField?, bool> shouldHandle;
{
return (value, field) => if (fields.Contains("*"))
{ {
if (value.Type == JsonValueType.Null) shouldHandle = (field, parent) => true;
{ }
return value; else
} {
var paths = fields.Select(x => x.Split('.')).ToList();
try shouldHandle = (field, parent) =>
{ {
var (_, error) = JsonValueConverter.ConvertValue(field, value); for (var i = 0; i < paths.Count; i++)
{
var path = paths[i];
if (parent != null)
{
return path.Length == 2 && path[0] == parent.Name && path[1] == field.Name;
}
else
{
return path.Length == 1 && path[0] == field.Name;
}
}
if (error != null) return false;
};
}
return (value, field, parent) =>
{
if (value is JsonArray array && shouldHandle(field, parent))
{
for (var i = 0; i < array.Count; i++)
{ {
return Value.Unset; var id = array[i].ToString();
array[i] = JsonValue.Create(urlGenerator.AssetContent(Guid.Parse(id)));
} }
} }
catch
return value;
};
}
public static ValueConverter ForNested(params ValueConverter[] converters)
{
if (converters?.Any() != true)
{
return Noop;
}
return (value, field, parent) =>
{
if (value is JsonArray array && field is IArrayField arrayField)
{ {
return Value.Unset; foreach (var nested in array.OfType<JsonObject>())
{
foreach (var (fieldName, nestedValue) in nested.ToList())
{
IJsonValue? newValue = nestedValue;
if (arrayField.FieldsByName.TryGetValue(fieldName, out var nestedField))
{
for (var i = 0; i < converters.Length; i++)
{
newValue = converters[i](newValue!, nestedField, arrayField);
if (newValue == null)
{
break;
}
}
}
else
{
newValue = null;
}
if (newValue == null)
{
nested.Remove(fieldName);
}
else if (!ReferenceEquals(nestedValue, newValue))
{
nested[fieldName] = newValue;
}
}
}
} }
return value; return value;

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

@ -89,12 +89,17 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
if (value is JsonArray array) if (value is JsonArray array)
{ {
var result = new JsonArray(array); var result = array;
for (var i = 0; i < result.Count; i++) for (var i = 0; i < result.Count; i++)
{ {
if (!IsValidReference(result[i])) if (!IsValidReference(result[i]))
{ {
if (ReferenceEquals(result, array))
{
result = new JsonArray(array);
}
result.RemoveAt(i); result.RemoveAt(i);
i--; i--;
} }

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

@ -18,16 +18,16 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
if (validIds == null) if (validIds == null)
{ {
return (value, field) => value; return ValueConverters.Noop;
} }
var cleaner = new ReferencesCleaner(validIds); var cleaner = new ReferencesCleaner(validIds);
return (value, field) => return (value, field, parent) =>
{ {
if (value.Type == JsonValueType.Null) if (value.Type == JsonValueType.Null)
{ {
return value!; return value;
} }
cleaner.SetValue(value); cleaner.SetValue(value);

16
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/DataConverter.cs

@ -19,20 +19,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
public DataConverter(IJsonSerializer serializer) public DataConverter(IJsonSerializer serializer)
{ {
var decoder = ValueConverters.DecodeJson(serializer);
decodeJsonConverters = new[] decodeJsonConverters = new[]
{ {
FieldConverters.ForValues( FieldConverters.ForValues(decoder, ValueConverters.ForNested(decoder))
ValueConverters.DecodeJson(serializer)),
FieldConverters.ForNestedId2Name(
ValueConverters.DecodeJson(serializer))
}; };
var encoder = ValueConverters.EncodeJson(serializer);
encodeJsonConverters = new[] encodeJsonConverters = new[]
{ {
FieldConverters.ForValues( FieldConverters.ForValues(encoder, ValueConverters.ForNested(encoder))
ValueConverters.EncodeJson(serializer)),
FieldConverters.ForNestedName2Id(
ValueConverters.EncodeJson(serializer))
}; };
} }
@ -43,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
public IdContentData ToMongoModel(NamedContentData result, Schema schema) public IdContentData ToMongoModel(NamedContentData result, Schema schema)
{ {
return result.ConvertName2Id(schema, encodeJsonConverters); return result.ConvertName2IdCloned(schema, encodeJsonConverters);
} }
} }
} }

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

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (context.PlainResult is IContentEntity content && NotEnriched(context)) if (context.PlainResult is IContentEntity content && NotEnriched(context))
{ {
var enriched = await contentEnricher.EnrichAsync(content, contextProvider.Context); var enriched = await contentEnricher.EnrichAsync(content, true, contextProvider.Context);
context.Complete(enriched); context.Complete(enriched);
} }

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

@ -36,20 +36,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
} }
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, Context context) public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, bool cloneData, Context context)
{ {
Guard.NotNull(content); Guard.NotNull(content);
var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), context); var enriched = await EnrichInternalAsync(Enumerable.Repeat(content, 1), cloneData, context);
return enriched[0]; return enriched[0];
} }
public async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context) public Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context)
{ {
Guard.NotNull(contents); Guard.NotNull(contents);
Guard.NotNull(context); Guard.NotNull(context);
return EnrichInternalAsync(contents, false, context);
}
private async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichInternalAsync(IEnumerable<IContentEntity> contents, bool cloneData, Context context)
{
using (Profiler.TraceMethod<ContentEnricher>()) using (Profiler.TraceMethod<ContentEnricher>())
{ {
var results = new List<ContentEntity>(); var results = new List<ContentEntity>();
@ -65,6 +70,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var result = SimpleMapper.Map(content, new ContentEntity()); var result = SimpleMapper.Map(content, new ContentEntity());
if (cloneData)
{
result.Data = result.Data.Clone();
}
results.Add(result); results.Add(result);
} }

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

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public interface IContentEnricher public interface IContentEnricher
{ {
Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, Context context); Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, bool cloneData, Context context);
Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context); Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context);
} }

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

@ -104,17 +104,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
if (!context.IsFrontendClient) if (!context.IsFrontendClient)
{ {
yield return FieldConverters.ExcludeHidden(); yield return FieldConverters.ExcludeHidden;
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); yield return FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeHidden));
} }
yield return FieldConverters.ExcludeChangedTypes(); yield return FieldConverters.ExcludeChangedTypes;
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); yield return FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeChangedTypes));
if (cleanReferences != null) if (cleanReferences != null)
{ {
yield return FieldConverters.ForValues(cleanReferences); yield return FieldConverters.ForValues(cleanReferences);
yield return FieldConverters.ForNestedName2Name(cleanReferences); yield return FieldConverters.ForValues(ValueConverters.ForNested(cleanReferences));
} }
yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig);
@ -134,11 +134,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages);
} }
var assetUrls = context.AssetUrls(); var assetUrls = context.AssetUrls().ToList();
if (assetUrls.Any()) if (assetUrls.Count > 0)
{ {
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), urlGenerator); var resolveAssetUrls = ValueConverters.ResolveAssetUrls(assetUrls, urlGenerator);
yield return FieldConverters.ForValues(resolveAssetUrls);
yield return FieldConverters.ForValues(ValueConverters.ForNested(resolveAssetUrls));
} }
} }
} }

2
backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs

@ -16,6 +16,8 @@ namespace Squidex.Infrastructure.Json.Objects
bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result); bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result);
IJsonValue Clone();
string ToJsonString(); string ToJsonString();
string ToString(); string ToString();

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

@ -26,6 +26,11 @@ namespace Squidex.Infrastructure.Json.Objects
} }
public JsonArray(JsonArray source) public JsonArray(JsonArray source)
: base(source.ToList())
{
}
private JsonArray(List<IJsonValue> source)
: base(source) : base(source)
{ {
} }
@ -90,6 +95,11 @@ namespace Squidex.Infrastructure.Json.Objects
return hashCode; return hashCode;
} }
public IJsonValue Clone()
{
return new JsonArray(this.Select(x => x.Clone()).ToList());
}
public string ToJsonString() public string ToJsonString()
{ {
return ToString(); return ToString();

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

@ -43,6 +43,11 @@ namespace Squidex.Infrastructure.Json.Objects
return 0; return 0;
} }
public IJsonValue Clone()
{
return this;
}
public string ToJsonString() public string ToJsonString()
{ {
return ToString(); return ToString();

10
backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs

@ -61,6 +61,11 @@ namespace Squidex.Infrastructure.Json.Objects
inner = new Dictionary<string, IJsonValue>(obj.inner); inner = new Dictionary<string, IJsonValue>(obj.inner);
} }
private JsonObject(Dictionary<string, IJsonValue> source)
{
inner = source;
}
public JsonObject Add(string key, object? value) public JsonObject Add(string key, object? value)
{ {
return Add(key, JsonValue.Create(value)); return Add(key, JsonValue.Create(value));
@ -123,6 +128,11 @@ namespace Squidex.Infrastructure.Json.Objects
return inner.DictionaryHashCode(); return inner.DictionaryHashCode();
} }
public IJsonValue Clone()
{
return new JsonObject(this.ToDictionary(x => x.Key, x => x.Value.Clone()));
}
public string ToJsonString() public string ToJsonString()
{ {
return ToString(); return ToString();

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

@ -36,6 +36,11 @@ namespace Squidex.Infrastructure.Json.Objects
return other != null && other.Type == Type && Equals(other.Value, Value); return other != null && other.Type == Type && Equals(other.Value, Value);
} }
public IJsonValue Clone()
{
return this;
}
public override int GetHashCode() public override int GetHashCode()
{ {
return Value.GetHashCode(); return Value.GetHashCode();

19
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs

@ -231,5 +231,24 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
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_clone_named_value_and_also_children()
{
var source = new NamedContentData
{
["field1"] = new ContentFieldData(),
["field2"] = new ContentFieldData()
};
var clone = source.Clone();
Assert.NotSame(source, clone);
foreach (var (key, value) in clone)
{
Assert.NotSame(value, source[key]);
}
}
} }
} }

20
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs

@ -9,6 +9,7 @@ using System;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Contents namespace Squidex.Domain.Apps.Core.Model.Contents
@ -62,5 +63,24 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
Assert.Null(string.IsInterned(serialized.Keys.First())); Assert.Null(string.IsInterned(serialized.Keys.First()));
} }
[Fact]
public void Should_clone_value_and_also_children()
{
var source = new ContentFieldData
{
["en"] = JsonValue.Array(),
["de"] = JsonValue.Array()
};
var clone = source.Clone();
Assert.NotSame(source, clone);
foreach (var (key, value) in clone)
{
Assert.NotSame(value, source[key]);
}
}
} }
} }

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

@ -8,6 +8,7 @@
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
@ -25,7 +26,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddNumber(3, "field3", Partitioning.Invariant) .AddNumber(3, "field3", Partitioning.Invariant)
.AddAssets(5, "assets1", Partitioning.Invariant) .AddAssets(5, "assets1", Partitioning.Invariant)
.AddAssets(6, "assets2", Partitioning.Invariant) .AddAssets(6, "assets2", Partitioning.Invariant)
.AddArray(7, "array", Partitioning.Invariant, h => h
.AddNumber(71, "nested1")
.AddNumber(72, "nested2"))
.AddJson(4, "json", Partitioning.Language) .AddJson(4, "json", Partitioning.Language)
.HideField(2)
.HideField(71, 7)
.UpdateField(3, f => f.Hide()); .UpdateField(3, f => f.Hide());
} }
@ -40,17 +46,34 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddField("field2", .AddField("field2",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 1)) .AddValue("iv", 1))
.AddField("array",
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("nested1", 100)
.Add("nested2", 200)
.Add("invalid", 300))))
.AddField("invalid", .AddField("invalid",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 2)); .AddValue("iv", 2));
var actual = input.ConvertName2Id(schema, (data, field) => field.Name == "field2" ? null : data); var hideRoot = FieldConverters.ExcludeHidden;
var hideNested = FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeHidden));
var actual = input.ConvertName2IdCloned(schema, hideRoot, hideNested);
var expected = var expected =
new IdContentData() new IdContentData()
.AddField(1, .AddField(1,
new ContentFieldData() new ContentFieldData()
.AddValue("en", "EN")); .AddValue("en", "EN"))
.AddField(7,
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("72", 200))));
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -81,32 +104,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
[Fact]
public void Should_convert_id_to_id()
{
var input =
new IdContentData()
.AddField(1,
new ContentFieldData()
.AddValue("en", "EN"))
.AddField(2,
new ContentFieldData()
.AddValue("iv", 1))
.AddField(99,
new ContentFieldData()
.AddValue("iv", 2));
var actual = input.ConvertId2Id(schema, (data, field) => field.Name == "field2" ? null : data);
var expected =
new IdContentData()
.AddField(1,
new ContentFieldData()
.AddValue("en", "EN"));
Assert.Equal(expected, actual);
}
[Fact] [Fact]
public void Should_convert_id_to_name() public void Should_convert_id_to_name()
{ {
@ -118,17 +115,34 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddField(2, .AddField(2,
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 1)) .AddValue("iv", 1))
.AddField(7,
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("71", 100)
.Add("72", 200)
.Add("799", 300))))
.AddField(99, .AddField(99,
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 2)); .AddValue("iv", 2));
var actual = input.ConvertId2Name(schema, (data, field) => field.Name == "field2" ? null : data); var hideRoot = FieldConverters.ExcludeHidden;
var hideNested = FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeHidden));
var actual = input.ConvertId2Name(schema, hideRoot, hideNested);
var expected = var expected =
new NamedContentData() new NamedContentData()
.AddField("field1", .AddField("field1",
new ContentFieldData() new ContentFieldData()
.AddValue("en", "EN")); .AddValue("en", "EN"))
.AddField("array",
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("nested2", 200))));
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }

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

@ -5,10 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using FakeItEasy;
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.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
@ -21,31 +18,22 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
public class FieldConvertersTests public class FieldConvertersTests
{ {
private readonly IUrlGenerator urlGenerato = A.Fake<IUrlGenerator>();
private readonly Guid id1 = Guid.NewGuid();
private readonly Guid id2 = Guid.NewGuid();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
public FieldConvertersTests()
{
A.CallTo(() => urlGenerato.AssetContent(A<Guid>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<Guid>(0)}");
}
[Fact] [Fact]
public void Should_filter_for_value_conversion() public void Should_filter_for_value_conversion()
{ {
var field = Fields.String(1, "string", Partitioning.Invariant); var field = Fields.String(1, "string", Partitioning.Invariant);
var input = var source =
new ContentFieldData() new ContentFieldData()
.AddJsonValue(JsonValue.Object()); .AddJsonValue(JsonValue.Object());
var actual = FieldConverters.ForValues((f, i) => Value.Unset)(input, field); var result = FieldConverters.ForValues((value, field, parent) => null)(source, field);
var expected = new ContentFieldData(); var expected = new ContentFieldData();
Assert.Equal(expected, actual); Assert.Equal(expected, result);
} }
[Fact] [Fact]
@ -53,133 +41,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
var field = Fields.Json(1, "json", Partitioning.Invariant); var field = Fields.Json(1, "json", Partitioning.Invariant);
var input = var source =
new ContentFieldData() new ContentFieldData()
.AddJsonValue(JsonValue.Object()); .AddJsonValue(JsonValue.Object());
var actual = FieldConverters.ForValues(ValueConverters.EncodeJson(TestUtils.DefaultSerializer))(input, field); var result = FieldConverters.ForValues(ValueConverters.EncodeJson(TestUtils.DefaultSerializer))(source, field);
var expected = var expected =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", "e30="); .AddValue("iv", "e30=");
Assert.Equal(expected, actual); Assert.Equal(expected, result);
}
[Fact]
public void Should_convert_name_to_id()
{
var field =
Fields.Array(1, "1", Partitioning.Invariant,
Fields.Number(1, "field1"),
Fields.Number(2, "field2").Hide());
var input =
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)
.Add("field2", 200)
.Add("invalid", 300)));
var actual = FieldConverters.ForNestedName2Id(ValueConverters.ExcludeHidden())(input, field);
var expected =
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("1", 100)));
Assert.Equal(expected, actual);
}
[Fact]
public void Should_convert_name_to_name()
{
var field =
Fields.Array(1, "1", Partitioning.Invariant,
Fields.Number(1, "field1"),
Fields.Number(2, "field2").Hide());
var input =
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)
.Add("field2", 200)
.Add("invalid", 300)));
var actual = FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden())(input, field);
var expected =
new ContentFieldData()
.AddJsonValue(
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)));
Assert.Equal(expected, actual);
}
[Fact]
public void Should_convert_id_to_id()
{
var field =
Fields.Array(1, "1", Partitioning.Invariant,
Fields.Number(1, "field1"),
Fields.Number(2, "field2").Hide());
var input =
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("1", 100)
.Add("2", 200)
.Add("99", 300)));
var actual = FieldConverters.ForNestedId2Id(ValueConverters.ExcludeHidden())(input, field);
var expected =
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("1", 100)));
Assert.Equal(expected, actual);
}
[Fact]
public void Should_convert_id_to_name()
{
var field =
Fields.Array(1, "1", Partitioning.Invariant,
Fields.Number(1, "field1"),
Fields.Number(2, "field2").Hide());
var input =
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("1", 100)
.Add("2", 200)
.Add("99", 300)));
var actual = FieldConverters.ForNestedId2Name(ValueConverters.ExcludeHidden())(input, field);
var expected =
new ContentFieldData()
.AddValue("iv",
JsonValue.Array(
JsonValue.Object()
.Add("field1", 100)));
Assert.Equal(expected, actual);
} }
[Fact] [Fact]
@ -192,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddValue("en", null) .AddValue("en", null)
.AddValue("de", 1); .AddValue("de", 1);
var result = FieldConverters.ExcludeChangedTypes()(source, field); var result = FieldConverters.ExcludeChangedTypes(source, field);
Assert.Same(source, result); Assert.Same(source, result);
} }
@ -207,7 +79,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddValue("en", "EN") .AddValue("en", "EN")
.AddValue("de", 0); .AddValue("de", 0);
var result = FieldConverters.ExcludeChangedTypes()(source, field); var result = FieldConverters.ExcludeChangedTypes(source, field);
Assert.Null(result); Assert.Null(result);
} }
@ -219,19 +91,19 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = new ContentFieldData(); var source = new ContentFieldData();
var result = FieldConverters.ExcludeHidden()(source, field); var result = FieldConverters.ExcludeHidden(source, field);
Assert.Same(source, result); Assert.Same(source, result);
} }
[Fact] [Fact]
public void Should_return_null_values_if_field_hidden() public void Should_return_null_if_field_hidden()
{ {
var field = Fields.String(1, "string", Partitioning.Language); var field = Fields.String(1, "string", Partitioning.Language);
var source = new ContentFieldData(); var source = new ContentFieldData();
var result = FieldConverters.ExcludeHidden()(source, field.Hide()); var result = FieldConverters.ExcludeHidden(source, field.Hide());
Assert.Null(result); Assert.Null(result);
} }
@ -293,8 +165,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = var source =
new ContentFieldData() new ContentFieldData()
.AddValue("iv", "A") .AddValue("iv", "A");
.AddValue("it", "B");
var expected = var expected =
new ContentFieldData() new ContentFieldData()
@ -474,125 +345,5 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
Assert.Same(source, result); Assert.Same(source, result);
} }
[Fact]
public void Should_convert_asset_ids_to_urls()
{
var field = Fields.Assets(1, "assets", Partitioning.Invariant);
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "assets" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
[Fact]
public void Should_convert_nested_asset_ids_to_urls()
{
var field =
Fields.Array(1, "array", Partitioning.Invariant,
Fields.Assets(1, "assets"));
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array(id1, id2))));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"))));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "array.assets" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
[Fact]
public void Should_convert_asset_ids_to_urls_for_wildcard_fields()
{
var field = Fields.Assets(1, "assets", Partitioning.Invariant);
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
[Fact]
public void Should_convert_nested_asset_ids_to_urls_for_wildcard_fields()
{
var field =
Fields.Array(1, "array", Partitioning.Invariant,
Fields.Assets(1, "assets"));
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array(id1, id2))));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"))));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
[Fact]
public void Should_not_convert_asset_ids_to_urls_when_field_does_not_match()
{
var field = Fields.Assets(1, "assets", Partitioning.Invariant);
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "other" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
[Fact]
public void Should_not_convert_asset_ids_to_urls_when_fields_is_null()
{
var field = Fields.Assets(1, "assets", Partitioning.Invariant);
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(id1, id2));
var result = FieldConverters.ResolveAssetUrls(null, urlGenerato)(source, field);
Assert.Equal(expected, result);
}
} }
} }

90
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs

@ -5,8 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using FakeItEasy;
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;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
@ -14,10 +17,19 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
public class ValueConvertersTests public class ValueConvertersTests
{ {
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly Guid id1 = Guid.NewGuid();
private readonly Guid id2 = Guid.NewGuid();
private readonly RootField<StringFieldProperties> stringField = Fields.String(1, "1", Partitioning.Invariant); private readonly RootField<StringFieldProperties> stringField = Fields.String(1, "1", Partitioning.Invariant);
private readonly RootField<JsonFieldProperties> jsonField = Fields.Json(1, "1", Partitioning.Invariant); private readonly RootField<JsonFieldProperties> jsonField = Fields.Json(1, "1", Partitioning.Invariant);
private readonly RootField<NumberFieldProperties> numberField = Fields.Number(1, "1", Partitioning.Invariant); private readonly RootField<NumberFieldProperties> numberField = Fields.Number(1, "1", Partitioning.Invariant);
public ValueConvertersTests()
{
A.CallTo(() => urlGenerator.AssetContent(A<Guid>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<Guid>(0)}");
}
[Fact] [Fact]
public void Should_encode_json_value() public void Should_encode_json_value()
{ {
@ -79,23 +91,89 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
} }
[Fact] [Fact]
public void Should_return_unset_if_field_hidden() public void Should_return_null_if_field_hidden()
{ {
var source = JsonValue.Create(123); var source = JsonValue.Create(123);
var result = ValueConverters.ExcludeHidden()(source, stringField.Hide()); var result = ValueConverters.ExcludeHidden(source, stringField.Hide());
Assert.Same(Value.Unset, result); Assert.Null(result);
} }
[Fact] [Fact]
public void Should_return_unset_if_field_has_wrong_type() public void Should_return_null_if_field_has_wrong_type()
{ {
var source = JsonValue.Create("invalid"); var source = JsonValue.Create("invalid");
var result = ValueConverters.ExcludeChangedTypes()(source, numberField); var result = ValueConverters.ExcludeChangedTypes(source, numberField);
Assert.Null(result);
}
[Theory]
[InlineData("assets")]
[InlineData("*")]
public void Should_convert_asset_ids_to_urls(string path)
{
var field = Fields.Assets(1, "assets", Partitioning.Invariant);
var source = JsonValue.Array(id1, id2);
var expected = JsonValue.Array($"url/to/{id1}", $"url/to/{id2}");
var result = ValueConverters.ResolveAssetUrls(HashSet.Of(path), urlGenerator)(source, field);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("other")]
[InlineData("**")]
public void Should_not_convert_asset_ids_when_field_name_does_not_match(string path)
{
var field = Fields.Assets(1, "assets", Partitioning.Invariant);
var source = JsonValue.Array(id1, id2);
var expected = source;
var result = ValueConverters.ResolveAssetUrls(HashSet.Of(path), urlGenerator)(source, field);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("parent.assets")]
[InlineData("*")]
public void Should_convert_nested_asset_ids_to_urls(string path)
{
var field = Fields.Array(1, "parent", Partitioning.Invariant, Fields.Assets(11, "assets"));
var source = JsonValue.Array(id1, id2);
var expected = JsonValue.Array($"url/to/{id1}", $"url/to/{id2}");
var result = ValueConverters.ResolveAssetUrls(HashSet.Of(path), urlGenerator)(source, field.Fields[0], field);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("assets")]
[InlineData("parent")]
[InlineData("parent.other")]
[InlineData("other.assets")]
public void Should_not_convert_nested_asset_ids_when_field_name_does_not_match(string path)
{
var field = Fields.Array(1, "parent", Partitioning.Invariant, Fields.Assets(11, "assets"));
var source = JsonValue.Array(id1, id2);
var expected = source;
var result = ValueConverters.ResolveAssetUrls(HashSet.Of(path), urlGenerator)(source, field.Fields[0], field);
Assert.Same(Value.Unset, result); Assert.Equal(expected, result);
} }
} }
} }

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

@ -109,11 +109,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
.Add("nested", JsonValue.Array(id2))))); .Add("nested", JsonValue.Array(id2)))));
var cleaner = ValueReferencesConverter.CleanReferences(new HashSet<Guid> { id2 }); var cleaner = ValueReferencesConverter.CleanReferences(new HashSet<Guid> { id2 });
var cleanNested = ValueConverters.ForNested(cleaner);
var converter = FieldConverters.ForValues(cleaner); var converter = FieldConverters.ForValues(cleaner, cleanNested);
var converterNested = FieldConverters.ForNestedName2Name(cleaner);
var actual = source.ConvertName2Name(schema, converter, converterNested); var actual = source.ConvertName2Name(schema, converter);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
@ -210,7 +210,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
[MemberData(nameof(ReferencingFields))] [MemberData(nameof(ReferencingFields))]
public void Should_return_same_value_from_field_when_value_is_json_null(IField field) public void Should_return_same_value_from_field_when_value_is_json_null(IField field)
{ {
var result = ValueReferencesConverter.CleanReferences(RandomIds())(JsonValue.Null, field); var result = ValueReferencesConverter.CleanReferences(RandomIds())(JsonValue.Null, field, null);
Assert.Equal(JsonValue.Null, result); Assert.Equal(JsonValue.Null, result);
} }
@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var value = CreateValue(id1, id2); var value = CreateValue(id1, id2);
var result = ValueReferencesConverter.CleanReferences(HashSet.Of(id1))(value, field); var result = ValueReferencesConverter.CleanReferences(HashSet.Of(id1))(value, field, null);
Assert.Equal(CreateValue(id1), result); Assert.Equal(CreateValue(id1), result);
} }

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

@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await sut.HandleAsync(context); await sut.HandleAsync(context);
A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, requestContext)) A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, A<bool>._, requestContext))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Same(result, context.Result<IEnrichedContentEntity>()); Assert.Same(result, context.Result<IEnrichedContentEntity>());
A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, requestContext)) A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, A<bool>._, requestContext))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var enriched = new ContentEntity(); var enriched = new ContentEntity();
A.CallTo(() => contentEnricher.EnrichAsync(result, requestContext)) A.CallTo(() => contentEnricher.EnrichAsync(result, true, requestContext))
.Returns(enriched); .Returns(enriched);
await sut.HandleAsync(context); await sut.HandleAsync(context);

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

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq; 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.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;
@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery)); var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(source, false, requestContext);
A.CallTo(() => step1.EnrichAsync(requestContext)) A.CallTo(() => step1.EnrichAsync(requestContext))
.MustHaveHappened(); .MustHaveHappened();
@ -108,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery)); var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy<IContentQueryService>(() => contentQuery));
await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(source, false, requestContext);
Assert.Same(schema, step1.Schema); Assert.Same(schema, step1.Schema);
Assert.Same(schema, step1.Schema); Assert.Same(schema, step1.Schema);
@ -117,9 +118,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
.MustHaveHappenedOnceExactly(); .MustHaveHappenedOnceExactly();
} }
private ContentEntity CreateContent() [Fact]
public async Task Should_clone_data_when_requested()
{
var source = CreateContent(new NamedContentData());
var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), new Lazy<IContentQueryService>(() => contentQuery));
var result = await sut.EnrichAsync(source, true, requestContext);
Assert.NotSame(source.Data, result.Data);
}
[Fact]
public async Task Should_not_clone_data_when_not_requested()
{
var source = CreateContent(new NamedContentData());
var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), new Lazy<IContentQueryService>(() => contentQuery));
var result = await sut.EnrichAsync(source, false, requestContext);
Assert.Same(source.Data, result.Data);
}
private ContentEntity CreateContent(NamedContentData? data = null)
{ {
return new ContentEntity { SchemaId = schemaId }; return new ContentEntity { SchemaId = schemaId, Data = data! };
} }
} }
} }

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

@ -182,6 +182,22 @@ namespace Squidex.Infrastructure.Json.Objects
Assert.Equal("[1, \"2\"]", json.ToString()); Assert.Equal("[1, \"2\"]", json.ToString());
} }
[Fact]
public void Should_create_array_from_source()
{
var json = JsonValue.Array(1, "2");
var copy = new JsonArray(json);
Assert.Equal("[1, \"2\"]", copy.ToJsonString());
Assert.Equal("[1, \"2\"]", copy.ToString());
copy.Clear();
Assert.Empty(copy);
Assert.NotEmpty(json);
}
[Fact] [Fact]
public void Should_create_object() public void Should_create_object()
{ {
@ -236,6 +252,76 @@ namespace Squidex.Infrastructure.Json.Objects
Assert.Equal("null", json.ToString()); Assert.Equal("null", json.ToString());
} }
[Fact]
public void Should_clone_number_and_return_same()
{
var source = JsonValue.Create(1);
var clone = source.Clone();
Assert.Same(source, clone);
}
[Fact]
public void Should_clone_string_and_return_same()
{
var source = JsonValue.Create("test");
var clone = source.Clone();
Assert.Same(source, clone);
}
[Fact]
public void Should_clone_boolean_and_return_same()
{
var source = JsonValue.Create(true);
var clone = source.Clone();
Assert.Same(source, clone);
}
[Fact]
public void Should_clone_null_and_return_same()
{
var source = JsonValue.Null;
var clone = source.Clone();
Assert.Same(source, clone);
}
[Fact]
public void Should_clone_array_and_also_children()
{
var source = JsonValue.Array(JsonValue.Array(), JsonValue.Array());
var clone = (JsonArray)source.Clone();
Assert.NotSame(source, clone);
for (var i = 0; i < source.Count; i++)
{
Assert.NotSame(clone[i], source[i]);
}
}
[Fact]
public void Should_clone_object_and_also_children()
{
var source = JsonValue.Object().Add("1", JsonValue.Array()).Add("2", JsonValue.Array());
var clone = (JsonObject)source.Clone();
Assert.NotSame(source, clone);
foreach (var (key, value) in clone)
{
Assert.NotSame(value, source[key]);
}
}
[Fact] [Fact]
public void Should_create_arrays_in_different_ways() public void Should_create_arrays_in_different_ways()
{ {

Loading…
Cancel
Save