mirror of https://github.com/Squidex/squidex.git
Browse Source
* Improve consistency for content repositories.
* Temp
* Improved consistency for assets.
* Improve tests
* Fixes to app plan management
* Remove referrer
* Fixes cancellation token.
* Fix method naming.
* Remove test loop.
* Another test to improve tests
* Small improvement for tests.
* Perhaps the potential to get better error messages.
* Revert batch change.
* Test logging.
* Fix JSON
* Better polling subscription.
* Store recent events.
* Test output.
* Test
* Another test
* Fix asset deleter.
* Build fix.
* Revert "Store recent events."
This reverts commit 127491d61d.
pull/908/head
committed by
GitHub
72 changed files with 1825 additions and 881 deletions
@ -1,151 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
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.Tags |
|||
{ |
|||
public static class TagNormalizer |
|||
{ |
|||
public static async Task NormalizeAsync(this ITagService tagService, DomainId appId, DomainId schemaId, Schema schema, ContentData newData, ContentData? oldData) |
|||
{ |
|||
Guard.NotNull(tagService); |
|||
Guard.NotNull(schema); |
|||
Guard.NotNull(newData); |
|||
|
|||
var newValues = new HashSet<string>(); |
|||
var newArrays = new List<JsonValue>(); |
|||
|
|||
var oldValues = new HashSet<string>(); |
|||
var oldArrays = new List<JsonValue>(); |
|||
|
|||
GetValues(schema, newValues, newArrays, newData); |
|||
|
|||
if (oldData != null) |
|||
{ |
|||
GetValues(schema, oldValues, oldArrays, oldData); |
|||
} |
|||
|
|||
if (newValues.Count > 0) |
|||
{ |
|||
var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), newValues, oldValues); |
|||
|
|||
foreach (var source in newArrays) |
|||
{ |
|||
var array = source.AsArray; |
|||
|
|||
for (var i = 0; i < array.Count; i++) |
|||
{ |
|||
if (normalized.TryGetValue(array[i].ToString(), out var result)) |
|||
{ |
|||
array[i] = result; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static async Task DenormalizeAsync(this ITagService tagService, DomainId appId, DomainId schemaId, Schema schema, params ContentData[] datas) |
|||
{ |
|||
Guard.NotNull(tagService); |
|||
Guard.NotNull(schema); |
|||
|
|||
var tagsValues = new HashSet<string>(); |
|||
var tagsArrays = new List<JsonValue>(); |
|||
|
|||
GetValues(schema, tagsValues, tagsArrays, datas); |
|||
|
|||
if (tagsValues.Count > 0) |
|||
{ |
|||
var denormalized = await tagService.DenormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), tagsValues); |
|||
|
|||
foreach (var source in tagsArrays) |
|||
{ |
|||
var array = source.AsArray; |
|||
|
|||
for (var i = 0; i < array.Count; i++) |
|||
{ |
|||
if (denormalized.TryGetValue(array[i].ToString(), out var result)) |
|||
{ |
|||
array[i] = result; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void GetValues(Schema schema, HashSet<string> values, List<JsonValue> arrays, params ContentData[] datas) |
|||
{ |
|||
foreach (var field in schema.Fields) |
|||
{ |
|||
if (field is IField<TagsFieldProperties> tags && tags.Properties.Normalization == TagsFieldNormalization.Schema) |
|||
{ |
|||
foreach (var data in datas) |
|||
{ |
|||
if (data.TryGetValue(field.Name, out var fieldData) && fieldData != null) |
|||
{ |
|||
foreach (var partition in fieldData) |
|||
{ |
|||
ExtractTags(partition.Value, values, arrays); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else if (field is IArrayField arrayField) |
|||
{ |
|||
foreach (var nestedField in arrayField.Fields) |
|||
{ |
|||
if (nestedField is IField<TagsFieldProperties> nestedTags && nestedTags.Properties.Normalization == TagsFieldNormalization.Schema) |
|||
{ |
|||
foreach (var data in datas) |
|||
{ |
|||
if (data.TryGetValue(field.Name, out var fieldData) && fieldData != null) |
|||
{ |
|||
foreach (var partition in fieldData) |
|||
{ |
|||
if (partition.Value.Value is JsonArray a) |
|||
{ |
|||
foreach (var value in a) |
|||
{ |
|||
if (value.Value is JsonObject o) |
|||
{ |
|||
if (o.TryGetValue(nestedField.Name, out var nestedValue)) |
|||
{ |
|||
ExtractTags(nestedValue, values, arrays); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void ExtractTags(JsonValue value, ISet<string> values, ICollection<JsonValue> arrays) |
|||
{ |
|||
if (value.Value is JsonArray a) |
|||
{ |
|||
foreach (var item in a) |
|||
{ |
|||
if (item.Value is string s) |
|||
{ |
|||
values.Add(s); |
|||
} |
|||
} |
|||
|
|||
arrays.Add(value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public sealed class PlanChangeAsyncResult : IChangePlanResult |
|||
{ |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public sealed class RedirectToCheckoutResult : IChangePlanResult |
|||
{ |
|||
public Uri Url { get; } |
|||
|
|||
public RedirectToCheckoutResult(Uri url) |
|||
{ |
|||
Guard.NotNull(url); |
|||
|
|||
Url = url; |
|||
} |
|||
} |
|||
} |
|||
@ -1,136 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Core.Tags; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Operations.Tags |
|||
{ |
|||
public class TagNormalizerTests |
|||
{ |
|||
private readonly ITagService tagService = A.Fake<ITagService>(); |
|||
private readonly DomainId appId = DomainId.NewGuid(); |
|||
private readonly DomainId schemaId = DomainId.NewGuid(); |
|||
private readonly Schema schema; |
|||
|
|||
public TagNormalizerTests() |
|||
{ |
|||
schema = |
|||
new Schema("my-schema") |
|||
.AddTags(1, "tags1", Partitioning.Invariant) |
|||
.AddTags(2, "tags2", Partitioning.Invariant, new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) |
|||
.AddString(3, "string", Partitioning.Invariant) |
|||
.AddArray(4, "array", Partitioning.Invariant, f => f |
|||
.AddTags(401, "nestedTags1") |
|||
.AddTags(402, "nestedTags2", new TagsFieldProperties { Normalization = TagsFieldNormalization.Schema }) |
|||
.AddString(403, "string")); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_normalize_tags_with_old_data() |
|||
{ |
|||
var newData = GenerateData("n_raw"); |
|||
var oldData = GenerateData("o_raw"); |
|||
|
|||
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), |
|||
A<HashSet<string>>.That.Is("n_raw2_1", "n_raw2_2", "n_raw4"), |
|||
A<HashSet<string>>.That.Is("o_raw2_1", "o_raw2_2", "o_raw4"), |
|||
default)) |
|||
.Returns(new Dictionary<string, string> |
|||
{ |
|||
["n_raw2_2"] = "id2_2", |
|||
["n_raw2_1"] = "id2_1", |
|||
["n_raw4"] = "id4" |
|||
}); |
|||
|
|||
await tagService.NormalizeAsync(appId, schemaId, schema, newData, oldData); |
|||
|
|||
Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); |
|||
Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_normalize_tags_without_old_data() |
|||
{ |
|||
var newData = GenerateData("name"); |
|||
|
|||
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), |
|||
A<HashSet<string>>.That.Is("name2_1", "name2_2", "name4"), |
|||
A<HashSet<string>>.That.IsEmpty(), |
|||
default)) |
|||
.Returns(new Dictionary<string, string> |
|||
{ |
|||
["name2_2"] = "id2_2", |
|||
["name2_1"] = "id2_1", |
|||
["name4"] = "id4" |
|||
}); |
|||
|
|||
await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); |
|||
|
|||
Assert.Equal(JsonValue.Array("id2_1", "id2_2"), newData["tags2"]!["iv"]); |
|||
Assert.Equal(JsonValue.Array("id4"), GetNestedTags(newData)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_denormalize_tags() |
|||
{ |
|||
var newData = GenerateData("id"); |
|||
|
|||
A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), |
|||
A<HashSet<string>>.That.Is("id2_1", "id2_2", "id4"), |
|||
A<HashSet<string>>.That.IsEmpty(), |
|||
default)) |
|||
.Returns(new Dictionary<string, string> |
|||
{ |
|||
["id2_2"] = "name2_2", |
|||
["id2_1"] = "name2_1", |
|||
["id4"] = "name4" |
|||
}); |
|||
|
|||
await tagService.NormalizeAsync(appId, schemaId, schema, newData, null); |
|||
|
|||
Assert.Equal(JsonValue.Array("name2_1", "name2_2"), newData["tags2"]!["iv"]); |
|||
Assert.Equal(JsonValue.Array("name4"), GetNestedTags(newData)); |
|||
} |
|||
|
|||
private static JsonValue GetNestedTags(ContentData newData) |
|||
{ |
|||
var arrayValue = newData["array"]!["iv"].AsArray; |
|||
var arrayItem = arrayValue[0].AsObject; |
|||
|
|||
return arrayItem["nestedTags2"]; |
|||
} |
|||
|
|||
private static ContentData GenerateData(string prefix) |
|||
{ |
|||
return new ContentData() |
|||
.AddField("tags1", |
|||
new ContentFieldData() |
|||
.AddInvariant(JsonValue.Array($"{prefix}1"))) |
|||
.AddField("tags2", |
|||
new ContentFieldData() |
|||
.AddInvariant(JsonValue.Array($"{prefix}2_1", $"{prefix}2_2"))) |
|||
.AddField("string", |
|||
new ContentFieldData() |
|||
.AddInvariant($"{prefix}stringValue")) |
|||
.AddField("array", |
|||
new ContentFieldData() |
|||
.AddInvariant( |
|||
JsonValue.Array( |
|||
new JsonObject() |
|||
.Add("nestedTags1", JsonValue.Array($"{prefix}3")) |
|||
.Add("nestedTags2", JsonValue.Array($"{prefix}4")) |
|||
.Add("string", $"{prefix}nestedStringValue")))); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using FakeItEasy; |
|||
using Squidex.Infrastructure.TestHelpers; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.States |
|||
{ |
|||
public class SimpleStateTests |
|||
{ |
|||
private readonly CancellationTokenSource cts = new CancellationTokenSource(); |
|||
private readonly CancellationToken ct; |
|||
private readonly TestState<MyDomainState> testState = new TestState<MyDomainState>(DomainId.NewGuid()); |
|||
private readonly SimpleState<MyDomainState> sut; |
|||
|
|||
public SimpleStateTests() |
|||
{ |
|||
ct = cts.Token; |
|||
|
|||
sut = new SimpleState<MyDomainState>(testState.PersistenceFactory, GetType(), testState.Id); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_init_with_base_data() |
|||
{ |
|||
Assert.Equal(-1, sut.Version); |
|||
Assert.NotNull(sut.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_state_from_persistence_on_load() |
|||
{ |
|||
testState.Version = 42; |
|||
testState.Snapshot = new MyDomainState { Value = 50 }; |
|||
|
|||
await sut.LoadAsync(ct); |
|||
|
|||
Assert.Equal(42, sut.Version); |
|||
Assert.Equal(50, sut.Value.Value); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_delete_when_clearing() |
|||
{ |
|||
await sut.ClearAsync(ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.DeleteAsync(ct)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_invoke_persistence_when_writing_state() |
|||
{ |
|||
await sut.WriteAsync(ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_load_once_on_update() |
|||
{ |
|||
await sut.UpdateAsync(x => true, ct: ct); |
|||
await sut.UpdateAsync(x => true, ct: ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) |
|||
.MustHaveHappenedOnceExactly(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_write_state_on_update_when_callback_returns_true() |
|||
{ |
|||
await sut.UpdateAsync(x => true, ct: ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_write_state_on_update_when_callback_returns_false() |
|||
{ |
|||
await sut.UpdateAsync(x => true, ct: ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_write_state_on_update_and_return_when_callback_returns_true() |
|||
{ |
|||
var result = await sut.UpdateAsync(x => (true, 42), ct: ct); |
|||
|
|||
Assert.Equal(42, result); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_write_state_on_update_and_return_when_callback_returns_false() |
|||
{ |
|||
var result = await sut.UpdateAsync(x => (false, 42), ct: ct); |
|||
|
|||
Assert.Equal(42, result); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_retry_update_when_failed_with_inconsistency_issue() |
|||
{ |
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); |
|||
|
|||
await sut.UpdateAsync(x => true, ct: ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_give_up_update_after_too_many_inconsistency_issues() |
|||
{ |
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); |
|||
|
|||
await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => true, ct: ct)); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_retry_update_with_other_exception() |
|||
{ |
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.Throws(new InvalidOperationException()); |
|||
|
|||
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => true, ct: ct)); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_retry_update_and_return_when_failed_with_inconsistency_issue() |
|||
{ |
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); |
|||
|
|||
await sut.UpdateAsync(x => (true, 42), ct: ct); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 6); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_give_up_update_and_return_after_too_many_inconsistency_issues() |
|||
{ |
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); |
|||
|
|||
await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 20); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_retry_update_and_return_with_other_exception() |
|||
{ |
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.Throws(new InvalidOperationException()); |
|||
|
|||
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); |
|||
|
|||
A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
|||
|
|||
A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) |
|||
.MustHaveHappenedANumberOfTimesMatching(x => x == 1); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue