From 59951d28d72f5ecf5a7481b7f2d9f06917b16a38 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 4 May 2020 16:30:40 +0200 Subject: [PATCH] Started to fix the conversion flow. (#519) * Started to fix the conversion flow. * Finalized (hopefully) * Improved tests and fixed a bug with assets resolver. --- backend/Squidex.ruleset | 1 + .../Contents/ContentData.cs | 5 + .../Contents/ContentFieldData.cs | 22 ++ .../Contents/NamedContentData.cs | 17 + .../Schemas/Fields.cs | 2 +- .../ConvertContent/ContentConverter.cs | 151 ++++++--- .../ConvertContent/FieldConverters.cs | 308 +++--------------- .../ConvertContent/FieldIdentifier.cs | 53 +++ .../ConvertContent/Value.cs | 16 - .../ConvertContent/ValueConverters.cs | 142 ++++++-- .../ExtractReferenceIds/ReferencesCleaner.cs | 7 +- .../ValueReferencesConverter.cs | 6 +- .../Contents/Operations/DataConverter.cs | 16 +- .../Contents/ContentCommandMiddleware.cs | 2 +- .../Contents/Queries/ContentEnricher.cs | 16 +- .../Contents/Queries/IContentEnricher.cs | 2 +- .../Contents/Queries/Steps/ConvertData.cs | 19 +- .../Json/Objects/IJsonValue.cs | 2 + .../Json/Objects/JsonArray.cs | 10 + .../Json/Objects/JsonNull.cs | 5 + .../Json/Objects/JsonObject.cs | 10 + .../Json/Objects/JsonScalar.cs | 5 + .../Model/Contents/ContentDataTests.cs | 19 ++ .../Model/Contents/ContentFieldDataTests.cs | 20 ++ .../ConvertContent/ContentConversionTests.cs | 74 +++-- .../ConvertContent/FieldConvertersTests.cs | 273 +--------------- .../ConvertContent/ValueConvertersTests.cs | 90 ++++- .../ReferenceExtractionTests.cs | 10 +- .../Contents/ContentCommandMiddlewareTests.cs | 6 +- .../Contents/Queries/ContentEnricherTests.cs | 33 +- .../Json/Objects/JsonObjectTests.cs | 86 +++++ 31 files changed, 755 insertions(+), 673 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldIdentifier.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs diff --git a/backend/Squidex.ruleset b/backend/Squidex.ruleset index 5aae5da01..1f62259db 100644 --- a/backend/Squidex.ruleset +++ b/backend/Squidex.ruleset @@ -86,6 +86,7 @@ + diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs index 680cac80e..4f7d0cf70 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -25,6 +25,11 @@ namespace Squidex.Domain.Apps.Core.Contents { } + protected ContentData(ContentData source, IEqualityComparer comparer) + : base(source, comparer) + { + } + protected ContentData(int capacity, IEqualityComparer comparer) : base(capacity, comparer) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs index d00c21552..35859d863 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ b/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) { return AddJsonValue(JsonValue.Create(value)); @@ -52,6 +62,18 @@ namespace Squidex.Domain.Apps.Core.Contents 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) { return Equals(obj as ContentFieldData); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs index aea3e31e7..6e55de3dc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs +++ b/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) : base(capacity, StringComparer.Ordinal) { @@ -46,6 +51,18 @@ namespace Squidex.Domain.Apps.Core.Contents 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) { return base.Equals(other); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs index 9af19f67f..aa7ced7d9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ b/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 RootField 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); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs index ebfd7ec2e..4575de073 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -5,35 +5,45 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; +using System.Linq; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ConvertContent { public static class ContentConverter { - private static readonly Func KeyNameResolver = f => f.Name; - private static readonly Func KeyIdResolver = f => f.Id; - public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters) { Guard.NotNull(schema); 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) - { - Guard.NotNull(schema); + ContentFieldData? newData = data; - 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) @@ -42,60 +52,117 @@ namespace Squidex.Domain.Apps.Core.ConvertContent 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); 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( - TDict1 source, - TDict2 target, - IReadOnlyDictionary fields, - Func targetKey, params FieldConverter[] converters) - where TDict1 : IDictionary - where TDict2 : IDictionary - where TKey1 : notnull - where TKey2 : notnull + private static ContentFieldData? ConvertData(FieldConverter[] converters, IRootField field, ContentFieldData data) { - 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()) { - 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; } } -} +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs index 9fe0a8061..d0a42eda3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -23,162 +23,51 @@ namespace Squidex.Domain.Apps.Core.ConvertContent 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; - 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) + public static readonly FieldConverter ExcludeHidden = (data, field) => { - if (key != null && long.TryParse(key, out var id)) - { - return array.FieldsById.GetOrDefault(id); - } - - return null; - } + return field.IsForApi() ? data : 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); - } - - 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) { - if (value.Type == JsonValueType.Null) - { - continue; - } - - try - { - var (_, error) = JsonValueConverter.ConvertValue(field, value); - - if (error != null) - { - return null; - } - } - catch - { - return null; - } + continue; } - return data; - }; - } - - public static FieldConverter ResolveAssetUrls(IReadOnlyCollection? fields, IUrlGenerator urlGenerator) - { - if (fields?.Any() != true) - { - return (data, field) => data; - } - - bool ShouldHandle(IField field, IField? parent = null) - { - if (field is IField) + try { - if (fields.Contains("*")) - { - return true; - } + var (_, error) = JsonValueConverter.ConvertValue(field, value); - if (parent == null) - { - return fields.Contains(field.Name); - } - else + if (error != null) { - return fields.Contains($"{parent.Name}.{field.Name}"); + return null; } } - - return false; - } - - void Resolve(IJsonValue value) - { - if (value is JsonArray array) + catch { - for (var i = 0; i < array.Count; i++) - { - var id = array[i].ToString(); - - array[i] = JsonValue.Create(urlGenerator.AssetContent(Guid.Parse(id))); - } + return null; } } - return (data, field) => - { - 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; - }; - } + return data; + }; public static FieldConverter ResolveInvariant(LanguagesConfig languages) { var codeForInvariant = InvariantPartitioning.Key; - var codeForMasterLanguage = languages.Master; 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)) - { - result[codeForInvariant] = value; - } - else if (data.TryGetValue(codeForMasterLanguage, out value)) + if (data.TryGetValue(languages.Master, out var value)) { result[codeForInvariant] = value; } @@ -202,21 +91,20 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { if (field.Partitioning.Equals(Partitioning.Language)) { - var result = new ContentFieldData(); - - foreach (var languageCode in languages.AllKeys) + if (data.TryGetValue(codeForInvariant, out var value)) { - if (data.TryGetValue(languageCode, out var value)) - { - result[languageCode] = value; - } - else if (languages.IsMaster(languageCode) && data.TryGetValue(codeForInvariant, out value)) + var result = new ContentFieldData { - 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; @@ -231,11 +119,11 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { foreach (var languageCode in languages.AllKeys) { - if (!data.TryGetValue(languageCode, out var value)) + if (!data.ContainsKey(languageCode)) { foreach (var fallback in languages.GetPriorities(languageCode)) { - if (data.TryGetValue(fallback, out value)) + if (data.TryGetValue(fallback, out var value)) { data[languageCode] = value; break; @@ -253,7 +141,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { if (languages?.Any() != true) { - return (data, field) => data; + return Noop; } var languageSet = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -275,145 +163,45 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { if (field.Partitioning.Equals(Partitioning.Language)) { - var result = new ContentFieldData(); - - foreach (var languageCode in languageSet) + foreach (var (key, _) in data.ToList()) { - if (data.TryGetValue(languageCode, out var value)) + if (!languageSet.Contains(key)) { - result[languageCode] = value; + data.Remove(key); } } - - return result; } return data; }; } - public static FieldConverter ForNestedName2Name(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) + public static FieldConverter ForValues(params ValueConverter[] converters) { 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)) - { - continue; - } + newValue = converters[i](newValue!, field, null); - var newArray = JsonValue.Array(); - - foreach (var item in array.OfType()) + if (newValue == null) { - var newItem = JsonValue.Object(); - - 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); + break; } - - result.Add(partitionKey, newArray); } - return result; - } - - 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) + if (newValue == null) { - var newValue = value; - - var isUnset = false; - - if (converters != null) - { - foreach (var converter in converters) - { - newValue = converter(newValue, field); - - if (ReferenceEquals(newValue, Value.Unset)) - { - isUnset = true; - break; - } - } - } - - if (!isUnset) - { - result.Add(key, newValue); - } + data.Remove(key); + } + else if (!ReferenceEquals(newValue, value)) + { + data[key] = newValue; } - - return result; } return data; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldIdentifier.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldIdentifier.cs new file mode 100644 index 000000000..b94619db9 --- /dev/null +++ b/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(); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs deleted file mode 100644 index a83740e60..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs +++ /dev/null @@ -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"); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs index b7973b677..2109a0a97 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; @@ -14,13 +16,44 @@ using Squidex.Infrastructure.Json.Objects; 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 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) { - return (value, field) => + return (value, field, parent) => { if (field is IField && value is JsonScalar s) { @@ -35,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent public static ValueConverter EncodeJson(IJsonSerializer jsonSerializer) { - return (value, field) => + return (value, field, parent) => { if (value.Type != JsonValueType.Null && field is IField) { @@ -48,32 +81,103 @@ namespace Squidex.Domain.Apps.Core.ConvertContent }; } - public static ValueConverter ExcludeHidden() + public static ValueConverter ResolveAssetUrls(IReadOnlyCollection? fields, IUrlGenerator urlGenerator) { - return (value, field) => !field.IsForApi() ? Value.Unset : value; - } + if (fields?.Any() != true) + { + return Noop; + } - public static ValueConverter ExcludeChangedTypes() - { - return (value, field) => + Func shouldHandle; + + if (fields.Contains("*")) { - if (value.Type == JsonValueType.Null) - { - return value; - } + shouldHandle = (field, parent) => true; + } + 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()) + { + 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; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs index 376bcbde8..3cefb32ac 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ b/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) { - var result = new JsonArray(array); + var result = array; for (var i = 0; i < result.Count; i++) { if (!IsValidReference(result[i])) { + if (ReferenceEquals(result, array)) + { + result = new JsonArray(array); + } + result.RemoveAt(i); i--; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs index eeee1a1a5..b0dec3778 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs @@ -18,16 +18,16 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds { if (validIds == null) { - return (value, field) => value; + return ValueConverters.Noop; } var cleaner = new ReferencesCleaner(validIds); - return (value, field) => + return (value, field, parent) => { if (value.Type == JsonValueType.Null) { - return value!; + return value; } cleaner.SetValue(value); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/DataConverter.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/DataConverter.cs index 96dc61e2a..e4cb65582 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/DataConverter.cs +++ b/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) { + var decoder = ValueConverters.DecodeJson(serializer); + decodeJsonConverters = new[] { - FieldConverters.ForValues( - ValueConverters.DecodeJson(serializer)), - FieldConverters.ForNestedId2Name( - ValueConverters.DecodeJson(serializer)) + FieldConverters.ForValues(decoder, ValueConverters.ForNested(decoder)) }; + var encoder = ValueConverters.EncodeJson(serializer); + encodeJsonConverters = new[] { - FieldConverters.ForValues( - ValueConverters.EncodeJson(serializer)), - FieldConverters.ForNestedName2Id( - ValueConverters.EncodeJson(serializer)) + FieldConverters.ForValues(encoder, ValueConverters.ForNested(encoder)) }; } @@ -43,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations public IdContentData ToMongoModel(NamedContentData result, Schema schema) { - return result.ConvertName2Id(schema, encodeJsonConverters); + return result.ConvertName2IdCloned(schema, encodeJsonConverters); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index 1a9f2bca8..736b3ebcc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/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)) { - var enriched = await contentEnricher.EnrichAsync(content, contextProvider.Context); + var enriched = await contentEnricher.EnrichAsync(content, true, contextProvider.Context); context.Complete(enriched); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 860abb2bb..5b09a992b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -36,20 +36,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries this.contentQuery = contentQuery; } - public async Task EnrichAsync(IContentEntity content, Context context) + public async Task EnrichAsync(IContentEntity content, bool cloneData, Context context) { 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]; } - public async Task> EnrichAsync(IEnumerable contents, Context context) + public Task> EnrichAsync(IEnumerable contents, Context context) { Guard.NotNull(contents); Guard.NotNull(context); + return EnrichInternalAsync(contents, false, context); + } + + private async Task> EnrichInternalAsync(IEnumerable contents, bool cloneData, Context context) + { using (Profiler.TraceMethod()) { var results = new List(); @@ -65,6 +70,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var result = SimpleMapper.Map(content, new ContentEntity()); + if (cloneData) + { + result.Data = result.Data.Clone(); + } + results.Add(result); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs index 7a2a5a741..63ab1198b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs @@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public interface IContentEnricher { - Task EnrichAsync(IContentEntity content, Context context); + Task EnrichAsync(IContentEntity content, bool cloneData, Context context); Task> EnrichAsync(IEnumerable contents, Context context); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index 9abdd5fcc..5a9192fc7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/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) { - yield return FieldConverters.ExcludeHidden(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); + yield return FieldConverters.ExcludeHidden; + yield return FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeHidden)); } - yield return FieldConverters.ExcludeChangedTypes(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); + yield return FieldConverters.ExcludeChangedTypes; + yield return FieldConverters.ForValues(ValueConverters.ForNested(ValueConverters.ExcludeChangedTypes)); if (cleanReferences != null) { yield return FieldConverters.ForValues(cleanReferences); - yield return FieldConverters.ForNestedName2Name(cleanReferences); + yield return FieldConverters.ForValues(ValueConverters.ForNested(cleanReferences)); } 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); } - 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)); } } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs index e2180b7f5..deb5fdb4d 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs +++ b/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); + IJsonValue Clone(); + string ToJsonString(); string ToString(); diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs index f91d3a048..33cc19b1c 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs @@ -26,6 +26,11 @@ namespace Squidex.Infrastructure.Json.Objects } public JsonArray(JsonArray source) + : base(source.ToList()) + { + } + + private JsonArray(List source) : base(source) { } @@ -90,6 +95,11 @@ namespace Squidex.Infrastructure.Json.Objects return hashCode; } + public IJsonValue Clone() + { + return new JsonArray(this.Select(x => x.Clone()).ToList()); + } + public string ToJsonString() { return ToString(); diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs index 18ea75059..04b4f18c2 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs @@ -43,6 +43,11 @@ namespace Squidex.Infrastructure.Json.Objects return 0; } + public IJsonValue Clone() + { + return this; + } + public string ToJsonString() { return ToString(); diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs index 8f523809f..bc0d045c8 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -61,6 +61,11 @@ namespace Squidex.Infrastructure.Json.Objects inner = new Dictionary(obj.inner); } + private JsonObject(Dictionary source) + { + inner = source; + } + public JsonObject Add(string key, object? value) { return Add(key, JsonValue.Create(value)); @@ -123,6 +128,11 @@ namespace Squidex.Infrastructure.Json.Objects return inner.DictionaryHashCode(); } + public IJsonValue Clone() + { + return new JsonObject(this.ToDictionary(x => x.Key, x => x.Value.Clone())); + } + public string ToJsonString() { return ToString(); diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs index e2c76751e..0044ab666 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonScalar.cs +++ b/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); } + public IJsonValue Clone() + { + return this; + } + public override int GetHashCode() { return Value.GetHashCode(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs index fe4b91dc5..d7b0295d5 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs +++ b/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.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]); + } + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs index 010007544..185789b0b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs @@ -9,6 +9,7 @@ using System; using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Json.Objects; using Xunit; 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())); } + + [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]); + } + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index 7081b61bc..d253f2a8d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs +++ b/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.ConvertContent; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json.Objects; using Xunit; namespace Squidex.Domain.Apps.Core.Operations.ConvertContent @@ -25,7 +26,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddNumber(3, "field3", Partitioning.Invariant) .AddAssets(5, "assets1", 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) + .HideField(2) + .HideField(71, 7) .UpdateField(3, f => f.Hide()); } @@ -40,17 +46,34 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddField("field2", new ContentFieldData() .AddValue("iv", 1)) + .AddField("array", + new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("nested1", 100) + .Add("nested2", 200) + .Add("invalid", 300)))) .AddField("invalid", new ContentFieldData() .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 = new IdContentData() .AddField(1, 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); } @@ -81,32 +104,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent 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] public void Should_convert_id_to_name() { @@ -118,17 +115,34 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddField(2, new ContentFieldData() .AddValue("iv", 1)) + .AddField(7, + new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("71", 100) + .Add("72", 200) + .Add("799", 300)))) .AddField(99, new ContentFieldData() .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 = new NamedContentData() .AddField("field1", 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); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs index ed3a9603d..179393282 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs @@ -5,10 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; using System.Linq; -using FakeItEasy; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; @@ -21,31 +18,22 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { public class FieldConvertersTests { - private readonly IUrlGenerator urlGenerato = A.Fake(); - private readonly Guid id1 = Guid.NewGuid(); - private readonly Guid id2 = Guid.NewGuid(); private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); - public FieldConvertersTests() - { - A.CallTo(() => urlGenerato.AssetContent(A._)) - .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument(0)}"); - } - [Fact] public void Should_filter_for_value_conversion() { var field = Fields.String(1, "string", Partitioning.Invariant); - var input = + var source = new ContentFieldData() .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(); - Assert.Equal(expected, actual); + Assert.Equal(expected, result); } [Fact] @@ -53,133 +41,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { var field = Fields.Json(1, "json", Partitioning.Invariant); - var input = + var source = new ContentFieldData() .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 = new ContentFieldData() .AddValue("iv", "e30="); - Assert.Equal(expected, actual); - } - - [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); + Assert.Equal(expected, result); } [Fact] @@ -192,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddValue("en", null) .AddValue("de", 1); - var result = FieldConverters.ExcludeChangedTypes()(source, field); + var result = FieldConverters.ExcludeChangedTypes(source, field); Assert.Same(source, result); } @@ -207,7 +79,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent .AddValue("en", "EN") .AddValue("de", 0); - var result = FieldConverters.ExcludeChangedTypes()(source, field); + var result = FieldConverters.ExcludeChangedTypes(source, field); Assert.Null(result); } @@ -219,19 +91,19 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent var source = new ContentFieldData(); - var result = FieldConverters.ExcludeHidden()(source, field); + var result = FieldConverters.ExcludeHidden(source, field); Assert.Same(source, result); } [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 source = new ContentFieldData(); - var result = FieldConverters.ExcludeHidden()(source, field.Hide()); + var result = FieldConverters.ExcludeHidden(source, field.Hide()); Assert.Null(result); } @@ -293,8 +165,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent var source = new ContentFieldData() - .AddValue("iv", "A") - .AddValue("it", "B"); + .AddValue("iv", "A"); var expected = new ContentFieldData() @@ -474,125 +345,5 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent 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(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(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(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(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(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); - } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs index f1e3a82f6..01762b092 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs @@ -5,8 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using FakeItEasy; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -14,10 +17,19 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { public class ValueConvertersTests { + private readonly IUrlGenerator urlGenerator = A.Fake(); + private readonly Guid id1 = Guid.NewGuid(); + private readonly Guid id2 = Guid.NewGuid(); private readonly RootField stringField = Fields.String(1, "1", Partitioning.Invariant); private readonly RootField jsonField = Fields.Json(1, "1", Partitioning.Invariant); private readonly RootField numberField = Fields.Number(1, "1", Partitioning.Invariant); + public ValueConvertersTests() + { + A.CallTo(() => urlGenerator.AssetContent(A._)) + .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument(0)}"); + } + [Fact] public void Should_encode_json_value() { @@ -79,23 +91,89 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent } [Fact] - public void Should_return_unset_if_field_hidden() + public void Should_return_null_if_field_hidden() { 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] - 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 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); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs index fb45e05fa..16662a41d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ b/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))))); var cleaner = ValueReferencesConverter.CleanReferences(new HashSet { id2 }); + var cleanNested = ValueConverters.ForNested(cleaner); - var converter = FieldConverters.ForValues(cleaner); - var converterNested = FieldConverters.ForNestedName2Name(cleaner); + var converter = FieldConverters.ForValues(cleaner, cleanNested); - var actual = source.ConvertName2Name(schema, converter, converterNested); + var actual = source.ConvertName2Name(schema, converter); Assert.Equal(expected, actual); } @@ -210,7 +210,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds [MemberData(nameof(ReferencingFields))] 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); } @@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds 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); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs index da02198f3..d6ef0b12e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents await sut.HandleAsync(context); - A.CallTo(() => contentEnricher.EnrichAsync(A._, requestContext)) + A.CallTo(() => contentEnricher.EnrichAsync(A._, A._, requestContext)) .MustNotHaveHappened(); } @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Same(result, context.Result()); - A.CallTo(() => contentEnricher.EnrichAsync(A._, requestContext)) + A.CallTo(() => contentEnricher.EnrichAsync(A._, A._, requestContext)) .MustNotHaveHappened(); } @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var enriched = new ContentEntity(); - A.CallTo(() => contentEnricher.EnrichAsync(result, requestContext)) + A.CallTo(() => contentEnricher.EnrichAsync(result, true, requestContext)) .Returns(enriched); await sut.HandleAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs index e50722d01..c40297db7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; @@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy(() => contentQuery)); - await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(source, false, requestContext); A.CallTo(() => step1.EnrichAsync(requestContext)) .MustHaveHappened(); @@ -108,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy(() => contentQuery)); - await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(source, false, requestContext); Assert.Same(schema, step1.Schema); Assert.Same(schema, step1.Schema); @@ -117,9 +118,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries .MustHaveHappenedOnceExactly(); } - private ContentEntity CreateContent() + [Fact] + public async Task Should_clone_data_when_requested() + { + var source = CreateContent(new NamedContentData()); + + var sut = new ContentEnricher(Enumerable.Empty(), new Lazy(() => 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(), new Lazy(() => 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! }; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs index 04867c285..58d4184b3 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs @@ -182,6 +182,22 @@ namespace Squidex.Infrastructure.Json.Objects 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] public void Should_create_object() { @@ -236,6 +252,76 @@ namespace Squidex.Infrastructure.Json.Objects 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] public void Should_create_arrays_in_different_ways() {