Browse Source

Temp

pull/906/head
Sebastian 4 years ago
parent
commit
db803e4dba
  1. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
  2. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs
  3. 151
      backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs
  4. 34
      backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs
  5. 16
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs
  6. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs
  7. 74
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  9. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs
  10. 10
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs
  11. 11
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs
  14. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs
  15. 16
      backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs
  16. 178
      backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs
  19. 5
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  20. 5
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs
  21. 2
      backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs
  22. 2
      backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs
  23. 10
      backend/src/Squidex.Infrastructure/States/NameReservationState.cs
  24. 38
      backend/src/Squidex.Infrastructure/States/SimpleState.cs
  25. 52
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  26. 25
      backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs

7
backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs

@ -14,10 +14,10 @@ namespace Squidex.Domain.Apps.Core.Tags
Task<Dictionary<string, string>> GetTagIdsAsync(DomainId id, string group, HashSet<string> names,
CancellationToken ct = default);
Task<Dictionary<string, string>> NormalizeTagsAsync(DomainId id, string group, HashSet<string>? names, HashSet<string>? ids,
Task<Dictionary<string, string>> GetTagNamesAsync(DomainId id, string group, HashSet<string> ids,
CancellationToken ct = default);
Task<Dictionary<string, string>> DenormalizeTagsAsync(DomainId id, string group, HashSet<string> ids,
Task UpdateAsync(DomainId id, string group, Dictionary<string, int> updates,
CancellationToken ct = default);
Task<TagsSet> GetTagsAsync(DomainId id, string group,
@ -34,5 +34,8 @@ namespace Squidex.Domain.Apps.Core.Tags
Task ClearAsync(DomainId id, string group,
CancellationToken ct = default);
Task ClearAsync(
CancellationToken ct);
}
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs

@ -7,10 +7,10 @@
namespace Squidex.Domain.Apps.Core.Tags
{
public sealed class Tag
public sealed record Tag
{
public string Name { get; set; }
public int Count { get; set; } = 1;
public int Count { get; set; }
}
}

151
backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs

@ -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);
}
}
}
}

34
backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs

@ -9,27 +9,31 @@ namespace Squidex.Domain.Apps.Core.Tags
{
public class TagsExport
{
public Dictionary<string, Tag>? Tags { get; set; }
private Dictionary<string, Tag> tags;
private Dictionary<string, string> alias;
public Dictionary<string, string>? Alias { get; set; }
public TagsExport Clone()
public Dictionary<string, Tag> Tags
{
var alias = (Dictionary<string, string>?)null;
get => tags ??= new Dictionary<string, Tag>();
set => tags = value ?? new Dictionary<string, Tag>();
}
if (Alias != null)
{
alias = new Dictionary<string, string>(Alias);
}
public Dictionary<string, string> Alias
{
get => alias ??= new Dictionary<string, string>();
set => alias = value ?? new Dictionary<string, string>();
}
var tags = (Dictionary<string, Tag>?)null;
public TagsExport Clone()
{
var clonedAlias = new Dictionary<string, string>(Alias);
if (Tags != null)
{
tags = new Dictionary<string, Tag>(Tags);
}
var clonedTags =
Tags.ToDictionary(
x => x.Key,
x => x.Value with { });
return new TagsExport { Alias = alias, Tags = tags };
return new TagsExport { Alias = clonedAlias, Tags = clonedTags };
}
}
}

16
backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs

@ -20,9 +20,17 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
public JsonObject Settings { get; set; } = new JsonObject();
public void Set(JsonObject settings)
public bool Set(JsonObject settings)
{
Settings = settings;
var isChanged = false;
if (!Settings.Equals(settings))
{
Settings = settings;
isChanged = true;
}
return isChanged;
}
public bool Set(string path, JsonValue value)
@ -134,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
var state = await GetStateAsync(appId, userId, ct);
await state.UpdateIfAsync(s => s.Remove(path), ct: ct);
await state.UpdateAsync(s => s.Remove(path), ct: ct);
}
public async Task SetAsync(DomainId appId, string? userId, string path, JsonValue value,
@ -142,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
var state = await GetStateAsync(appId, userId, ct);
await state.UpdateIfAsync(s => s.Set(path, value), ct: ct);
await state.UpdateAsync(s => s.Set(path, value), ct: ct);
}
public async Task SetAsync(DomainId appId, string? userId, JsonObject settings,

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.UsageTracking;
@ -19,10 +20,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
private const string CounterTotalSize = "TotalSize";
private static readonly DateTime SummaryDate;
private readonly IUsageTracker usageTracker;
private readonly ITagService tagService;
public AssetUsageTracker(IUsageTracker usageTracker)
public AssetUsageTracker(IUsageTracker usageTracker,
ITagService tagService)
{
this.usageTracker = usageTracker;
this.tagService = tagService;
}
Task IDeleter.DeleteAppAsync(IAppEntity app,

74
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@ -36,18 +37,77 @@ namespace Squidex.Domain.Apps.Entities.Assets
get => "^asset-";
}
public Task On(Envelope<IEvent> @event)
public bool CanClear
{
get => false;
}
public async Task On(IEnumerable<Envelope<IEvent>> events)
{
var tags = new Dictionary<DomainId, Dictionary<string, int>>();
foreach (var @event in events)
{
await TrackUsageAsync(@event);
AddTags(@event, tags);
}
foreach (var (appId, updates) in tags)
{
await tagService.UpdateAsync(appId, TagGroups.Assets, updates);
}
}
private static void AddTags(Envelope<IEvent> @event, Dictionary<DomainId, Dictionary<string, int>> tags)
{
if (@event.Headers.Restored())
{
return;
}
void AddTags(DomainId appId, HashSet<string>? tagIds, int count)
{
if (tagIds != null)
{
foreach (var tag in tagIds)
{
var perApp = tags.GetOrAddNew(appId);
perApp[tag] = perApp.GetOrAddDefault(tag) + count;
}
}
}
switch (@event.Payload)
{
case AssetCreated assetCreated:
AddTags(assetCreated.AppId.Id, assetCreated.Tags, 1);
break;
case AssetAnnotated assetAnnotated when assetAnnotated.Tags != null && assetAnnotated.OldTags != null:
AddTags(assetAnnotated.AppId.Id, assetAnnotated.Tags, 1);
AddTags(assetAnnotated.AppId.Id, assetAnnotated.OldTags, -1);
break;
case AssetDeleted assetDeleted:
AddTags(assetDeleted.AppId.Id, assetDeleted.OldTags, -1);
break;
}
}
private Task TrackUsageAsync(Envelope<IEvent> @event)
{
switch (@event.Payload)
{
case AssetCreated e:
return UpdateSizeAsync(e.AppId.Id, GetDate(@event), e.FileSize, 1);
case AssetCreated assetCreated:
return UpdateSizeAsync(assetCreated.AppId.Id, GetDate(@event), assetCreated.FileSize, 1);
case AssetUpdated e:
return UpdateSizeAsync(e.AppId.Id, GetDate(@event), e.FileSize, 0);
case AssetUpdated assetUpdated:
return UpdateSizeAsync(assetUpdated.AppId.Id, GetDate(@event), assetUpdated.FileSize, 0);
case AssetDeleted e:
return UpdateSizeAsync(e.AppId.Id, GetDate(@event), -e.DeletedSize, -1);
case AssetDeleted assetDeleted:
return UpdateSizeAsync(assetDeleted.AppId.Id, GetDate(@event), -assetDeleted.DeletedSize, -1);
}
return Task.CompletedTask;

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

@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return;
}
var export = new TagsExport { Tags = tags, Alias = alias };
var export = new TagsExport { Tags = tags!, Alias = alias! };
await tagService.RebuildTagsAsync(context.AppId, TagGroups.Assets, export, ct);
}

1
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs

@ -130,7 +130,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
if (result.IsChanged && context.Command is UploadAssetCommand)
{
var tempFile = context.ContextId.ToString();
try
{
await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null, ct);

10
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs

@ -166,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
if (create.Tags != null)
{
create.Tags = await operation.NormalizeTags(create.Tags);
create.Tags = await operation.NormalizeTagsAsync(create.Tags);
}
Create(create);
@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
if (annotate.Tags != null)
{
annotate.Tags = await operation.NormalizeTags(annotate.Tags);
annotate.Tags = await operation.NormalizeTagsAsync(annotate.Tags);
}
Annotate(annotate);
@ -224,8 +224,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
await operation.ExecuteDeleteScriptAsync(delete);
}
await operation.UnsetTags();
Delete(delete);
}
@ -252,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
private void Annotate(AnnotateAsset command)
{
Raise(command, new AssetAnnotated());
Raise(command, new AssetAnnotated { OldTags = Snapshot.Tags });
}
private void Move(MoveAsset command)
@ -262,7 +260,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
private void Delete(DeleteAsset command)
{
Raise(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize });
Raise(command, new AssetDeleted { OldTags = Snapshot.Tags, DeletedSize = Snapshot.TotalSize });
}
private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent

11
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs

@ -11,20 +11,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public static class TagsExtensions
{
public static async Task<HashSet<string>> NormalizeTags(this AssetOperation operation, HashSet<string> tags)
public static async Task<HashSet<string>> NormalizeTagsAsync(this AssetOperation operation, HashSet<string> names)
{
var tagService = operation.Resolve<ITagService>();
var normalized = await tagService.NormalizeTagsAsync(operation.App.Id, TagGroups.Assets, tags, operation.Snapshot.Tags);
var normalized = await tagService.GetTagIdsAsync(operation.App.Id, TagGroups.Assets, names);
return new HashSet<string>(normalized.Values);
}
public static async Task UnsetTags(this AssetOperation operation)
{
var tagService = operation.Resolve<ITagService>();
await tagService.NormalizeTagsAsync(operation.App.Id, TagGroups.Assets, null, operation.Snapshot.Tags);
}
}
}

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

@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet();
return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds, ct);
return await tagService.GetTagNamesAsync(group.Key, TagGroups.Assets, uniqueIds, ct);
}
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
public Dictionary<string, Instant> Users { get; } = new Dictionary<string, Instant>();
public string[] Add(string watcherId, IClock clock)
public (bool, string[]) Add(string watcherId, IClock clock)
{
var now = clock.GetCurrentInstant();
@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
Users[watcherId] = now;
return Users.Keys.ToArray();
return (true, Users.Keys.ToArray());
}
}

8
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs

@ -20,14 +20,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public Dictionary<string, long> Counters { get; set; } = new Dictionary<string, long>();
public void Increment(string name)
public bool Increment(string name)
{
Counters[name] = Counters.GetValueOrDefault(name) + 1;
return true;
}
public void Reset(string name, long value)
public bool Reset(string name, long value)
{
Counters[name] = value;
return true;
}
}

16
backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs

@ -18,7 +18,8 @@ namespace Squidex.Domain.Apps.Entities
{
private readonly List<ValidationError> errors = new List<ValidationError>();
private readonly IServiceProvider serviceProvider;
private readonly Func<TSnapShot> snapshot;
private readonly Func<TSnapShot> snapshotProvider;
private readonly TSnapShot snapshotInitial;
public RefToken Actor => Command.Actor;
@ -28,17 +29,22 @@ namespace Squidex.Domain.Apps.Entities
public TCommand Command { get; init; }
public TSnapShot Snapshot => snapshot();
public TSnapShot Snapshot => snapshotProvider();
public TSnapShot SnapshotInitial => snapshotInitial;
public ClaimsPrincipal? User => Command.User;
protected OperationContextBase(IServiceProvider serviceProvider, Func<TSnapShot> snapshot)
public Dictionary<string, object> Context { get; } = new Dictionary<string, object>();
protected OperationContextBase(IServiceProvider serviceProvider, Func<TSnapShot> snapshotProvider)
{
Guard.NotNull(serviceProvider);
Guard.NotNull(snapshot);
Guard.NotNull(snapshotProvider);
this.serviceProvider = serviceProvider;
this.snapshot = snapshot;
this.snapshotProvider = snapshotProvider;
this.snapshotInitial = snapshotProvider();
}
public T Resolve<T>() where T : notnull

178
backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs

@ -18,128 +18,128 @@ namespace Squidex.Domain.Apps.Entities.Tags
[CollectionName("Index_Tags")]
public sealed class State : TagsExport
{
public void Rebuild(TagsExport export)
public bool Rebuild(TagsExport export)
{
Tags = export.Tags;
var isChanged = false;
Alias = export.Alias;
if (!Tags.EqualsDictionary(export.Tags))
{
Tags = export.Tags;
isChanged = true;
}
if (!Alias.EqualsDictionary(export.Alias))
{
Alias = export.Alias;
isChanged = true;
}
return isChanged;
}
public void Rename(string name, string newName)
public bool Rename(string name, string newName)
{
Guard.NotNull(name);
Guard.NotNull(newName);
name = NormalizeName(name);
var (_, tag) = FindTag(name);
if (tag == null)
if (!TryGetTag(name, out var tag))
{
return;
return false;
}
newName = NormalizeName(newName);
tag.Name = newName;
if (string.Equals(name, newName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
tag.Value.Name = newName;
if (Alias != null)
foreach (var alias in Alias.Where(x => x.Value == name).ToList())
{
foreach (var alias in Alias.Where(x => x.Value == name).ToList())
{
Alias.Remove(alias.Key);
Alias.Remove(alias.Key);
if (alias.Key != newName)
{
Alias[alias.Key] = newName;
}
if (alias.Key != tag.Value.Name)
{
Alias[alias.Key] = tag.Value.Name;
}
}
Alias ??= new Dictionary<string, string>();
Alias[name] = newName;
return true;
}
public Dictionary<string, string> Normalize(HashSet<string>? names, HashSet<string>? ids)
public bool Update(Dictionary<string, int> updates)
{
var result = new Dictionary<string, string>();
var isChanged = false;
if (names != null)
foreach (var (id, update) in updates)
{
foreach (var tag in names)
if (update != 0 && Tags.TryGetValue(id, out var tag))
{
var name = NormalizeName(tag);
if (!string.IsNullOrWhiteSpace(name))
{
result.Add(name, GetId(name, ids));
}
}
}
var newCount = Math.Max(0, tag.Count + update);
if (ids != null)
{
foreach (var id in ids)
{
if (!result.ContainsValue(id))
if (newCount != tag.Count)
{
if (Tags != null && Tags.TryGetValue(id, out var tagInfo))
{
tagInfo.Count--;
if (tagInfo.Count <= 0)
{
Tags.Remove(id);
}
}
tag.Count = newCount;
isChanged = true;
}
}
}
return result;
return isChanged;
}
public Dictionary<string, string> GetTagIds(HashSet<string> names)
public (bool, Dictionary<string, string>) GetIds(HashSet<string> names)
{
Guard.NotNull(names);
var result = new Dictionary<string, string>();
var tagIds = new Dictionary<string, string>();
foreach (var tag in names)
var isChanged = false;
foreach (var name in names.Select(NormalizeName))
{
var name = NormalizeName(tag);
if (TryGetTag(name, out var tag))
{
tagIds[name] = tag.Key;
}
else
{
var id = Guid.NewGuid().ToString();
var (id, _) = FindTag(name);
Tags[id] = new Tag { Name = name };
tagIds[name] = id;
if (!string.IsNullOrWhiteSpace(id))
{
result.Add(name, id);
isChanged = true;
}
}
return result;
return (isChanged, tagIds);
}
public Dictionary<string, string> Denormalize(HashSet<string> ids)
public Dictionary<string, string> GetNames(HashSet<string> ids)
{
var result = new Dictionary<string, string>();
var tagNames = new Dictionary<string, string>();
foreach (var id in ids)
{
if (Tags?.TryGetValue(id, out var tagInfo) == true)
if (Tags.TryGetValue(id, out var tagInfo))
{
result[id] = tagInfo.Name;
tagNames[id] = tagInfo.Name;
}
}
return result;
return tagNames;
}
public TagsSet GetTags(long version)
{
var tags = Tags?.Values.ToDictionary(x => x.Name, x => x.Count) ?? new Dictionary<string, int>();
var clone = Tags.Values.ToDictionary(x => x.Name, x => x.Count);
return new TagsSet(tags, version);
return new TagsSet(clone, version);
}
public TagsExport GetExportableTags()
@ -149,41 +149,29 @@ namespace Squidex.Domain.Apps.Entities.Tags
return clone;
}
private string GetId(string name, HashSet<string>? ids)
{
var (id, tag) = FindTag(name);
if (tag != null)
{
if (ids == null || !ids.Contains(id))
{
tag.Count++;
}
}
else
{
id = DomainId.NewGuid().ToString();
Tags ??= new Dictionary<string, Tag>();
Tags.Add(id, new Tag { Name = name });
}
return id;
}
private static string NormalizeName(string name)
{
return name.Trim().ToLowerInvariant();
}
private KeyValuePair<string, Tag> FindTag(string name)
private bool TryGetTag(string name, out KeyValuePair<string, Tag> result)
{
if (Alias?.TryGetValue(name, out var newName) == true)
result = default;
if (Alias.TryGetValue(name, out var newName))
{
name = newName;
}
return Tags?.FirstOrDefault(x => x.Value.Name == name) ?? default;
var found = Tags.FirstOrDefault(x => x.Value.Name == name);
if (found.Value != null)
{
result = new KeyValuePair<string, Tag>(found.Key, found.Value);
return true;
}
return false;
}
}
@ -220,25 +208,25 @@ namespace Squidex.Domain.Apps.Entities.Tags
var state = await GetStateAsync(id, group, ct);
return await state.UpdateAsync(s => s.GetTagIds(names), ct: ct);
return await state.UpdateAsync(s => s.GetIds(names), ct: ct);
}
public async Task<Dictionary<string, string>> DenormalizeTagsAsync(DomainId id, string group, HashSet<string> ids,
public async Task<Dictionary<string, string>> GetTagNamesAsync(DomainId id, string group, HashSet<string> ids,
CancellationToken ct = default)
{
Guard.NotNull(ids);
var state = await GetStateAsync(id, group, ct);
return await state.UpdateAsync(s => s.Denormalize(ids), ct: ct);
return state.Value.GetNames(ids);
}
public async Task<Dictionary<string, string>> NormalizeTagsAsync(DomainId id, string group, HashSet<string>? names, HashSet<string>? ids,
public async Task UpdateAsync(DomainId id, string group, Dictionary<string, int> update,
CancellationToken ct = default)
{
var state = await GetStateAsync(id, group, ct);
return await state.UpdateAsync(s => s.Normalize(names, ids), ct: ct);
await state.UpdateAsync(s => s.Update(update), ct: ct);
}
public async Task<TagsSet> GetTagsAsync(DomainId id, string group,
@ -274,5 +262,11 @@ namespace Squidex.Domain.Apps.Entities.Tags
return state;
}
public Task ClearAsync(
CancellationToken ct)
{
return persistenceFactory.Snapshots.ClearAsync(ct);
}
}
}

2
backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs

@ -22,5 +22,7 @@ namespace Squidex.Domain.Apps.Events.Assets
public AssetMetadata? Metadata { get; set; }
public HashSet<string>? Tags { get; set; }
public HashSet<string>? OldTags { get; set; }
}
}

2
backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs

@ -13,5 +13,7 @@ namespace Squidex.Domain.Apps.Events.Assets
public sealed class AssetDeleted : AssetEvent
{
public long DeletedSize { get; set; }
public HashSet<string>? OldTags { get; set; }
}
}

5
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -327,11 +327,6 @@ namespace Squidex.Infrastructure
return dictionary.GetOrAdd(key, _ => default!);
}
public static TValue GetOrNew<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull where TValue : class, new()
{
return dictionary.GetOrCreate(key, _ => new TValue());
}
public static TValue GetOrAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull where TValue : class, new()
{
return dictionary.GetOrAdd(key, _ => new TValue());

5
backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs

@ -144,6 +144,11 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
public virtual async Task ResetAsync()
{
if (!eventConsumer.CanClear)
{
return;
}
await UpdateAsync(async () =>
{
Unsubscribe();

2
backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs

@ -17,6 +17,8 @@ namespace Squidex.Infrastructure.EventSourcing
string EventsFilter => ".*";
bool CanClear => true;
bool Handles(StoredEvent @event)
{
return true;

2
backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs

@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.States
public record struct SnapshotWriteJob<T>(DomainId Key, T Value, long NewVersion, long OldVersion = EtagVersion.Any)
{
public SnapshotWriteJob<TOther> As<TOther>(TOther snapshot)
public readonly SnapshotWriteJob<TOther> As<TOther>(TOther snapshot)
{
return new SnapshotWriteJob<TOther>(Key, snapshot, NewVersion, OldVersion);
}

10
backend/src/Squidex.Infrastructure/States/NameReservationState.cs

@ -14,11 +14,12 @@ namespace Squidex.Infrastructure.States
{
public List<NameReservation> Reservations { get; set; } = new List<NameReservation>();
public string? Reserve(DomainId id, string name)
public (bool, string?) Reserve(DomainId id, string name)
{
string? token = null;
var reservation = Reservations.Find(x => x.Name == name);
var reserved = false;
if (reservation?.Id == id)
{
@ -29,14 +30,15 @@ namespace Squidex.Infrastructure.States
token = RandomHash.Simple();
Reservations.Add(new NameReservation(token, name, id));
reserved = true;
}
return token;
return (reserved, token);
}
public void Remove(string? token)
public bool Remove(string? token)
{
Reservations.RemoveAll(x => x.Token == token);
return Reservations.RemoveAll(x => x.Token == token) > 0;
}
}

38
backend/src/Squidex.Infrastructure/States/SimpleState.cs

@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.States
return persistence.WriteEventAsync(envelope, ct);
}
public async Task UpdateIfAsync(Func<T, bool> updater, int retries = 20,
public async Task UpdateAsync(Func<T, bool> updater, int retries = 20,
CancellationToken ct = default)
{
await EnsureLoadedAsync(ct);
@ -73,7 +73,9 @@ namespace Squidex.Infrastructure.States
{
try
{
if (!updater(Value))
var isChanged = updater(Value);
if (!isChanged)
{
return;
}
@ -81,14 +83,14 @@ namespace Squidex.Infrastructure.States
await WriteAsync(ct);
return;
}
catch (InconsistentStateException) when (i < retries)
catch (InconsistentStateException) when (i < retries - 1)
{
await LoadAsync(ct);
}
}
}
public async Task UpdateAsync(Action<T> updater, int retries = 20,
public async Task<TResult> UpdateAsync<TResult>(Func<T, (bool, TResult)> updater, int retries = 20,
CancellationToken ct = default)
{
await EnsureLoadedAsync(ct);
@ -97,33 +99,17 @@ namespace Squidex.Infrastructure.States
{
try
{
updater(Value);
await WriteAsync(ct);
return;
}
catch (InconsistentStateException) when (i < retries)
{
await LoadAsync(ct);
}
}
}
var (isChanged, result) = updater(Value);
public async Task<TResult> UpdateAsync<TResult>(Func<T, TResult> updater, int retries = 5,
CancellationToken ct = default)
{
await EnsureLoadedAsync(ct);
for (var i = 0; i < retries; i++)
{
try
{
var result = updater(Value);
if (!isChanged)
{
return result;
}
await WriteAsync(ct);
return result;
}
catch (InconsistentStateException) when (i < retries)
catch (InconsistentStateException) when (i < retries - 1)
{
await LoadAsync(ct);
}

52
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -319,6 +319,58 @@ namespace TestSuite.ApiTests
Assert.Equal(fileNameRequest.FileName, asset_4.FileName);
}
[Fact]
public async Task Should_annotate_asset_in_parallel()
{
var metadataRequest = new AnnotateAssetDto
{
Tags = new List<string>
{
$"tag_{Guid.NewGuid()}",
$"tag_{Guid.NewGuid()}"
}
};
// STEP 1: Create asset
var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png");
var numErrors = 0;
var numSuccess = 0;
// STEP 3: Make parallel upserts.
await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) =>
{
try
{
await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest);
Interlocked.Increment(ref numSuccess);
}
catch (SquidexManagementException ex) when (ex.StatusCode is 409 or 412)
{
Interlocked.Increment(ref numErrors);
return;
}
});
// At least some errors and success should have happened.
Assert.True(numErrors > 0);
Assert.True(numSuccess > 0);
// STEP 3: Make an normal update to ensure nothing is corrupt.
await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest);
// STEP 4: Check tags
var tags = await _.Assets.WaitForTagsAsync(_.AppName, metadataRequest.Tags[0], TimeSpan.FromMinutes(2));
Assert.Equal(1, tags[metadataRequest.Tags[0]]);
Assert.Equal(1, tags[metadataRequest.Tags[1]]);
}
[Fact]
public async Task Should_protect_asset()
{

25
backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs

@ -38,6 +38,31 @@ namespace TestSuite
return false;
}
public static async Task<IDictionary<string, int>> WaitForTagsAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout)
{
try
{
using var cts = new CancellationTokenSource(timeout);
while (!cts.IsCancellationRequested)
{
var tags = await assetsClient.GetTagsAsync(app, cts.Token);
if (tags.TryGetValue(id, out var count) && count > 0)
{
return tags;
}
await Task.Delay(200, cts.Token);
}
}
catch (OperationCanceledException)
{
}
return null;
}
public static async Task<BackupJobDto> WaitForBackupAsync(this IBackupsClient backupsClient, string app, TimeSpan timeout)
{
try

Loading…
Cancel
Save