diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bac5c346..22ea89900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [7.0.0-rc3] - 2022-07-29 + +### Fixed + +* **Assets**: Fix recursive asset deletion. Query was selecting the wrong assets. +* **Assets**: Compatibility with 6.X collections fixed. +* **Contents**: Compatibility with 6.X collections fixed. + +### Changed + +* **Assets**: Moved the update of tag counts to a event consumers to improve consistency. + +### Added + +* **API**: New tests to cover more cases. + ## [7.0.0-rc2] - 2022-07-25 ### Fixed @@ -24,6 +40,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * **Contents**: New flag to store each contnent in a dedicated collection, so that indexes can be created. +## [6.11.0] - 2022-07-29 + +### Fixed + +* **Assets**: Fix recursive asset deletion. Query was selecting the wrong assets. + ## [6.10.0] - 2022-07-19 ### Fixed diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index 17527dc9d..4135fb83c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; @@ -66,49 +65,39 @@ namespace Squidex.Domain.Apps.Entities.Assets return; } - if (@event.Payload is AssetFolderDeleted folderDeleted) + if (@event.Payload is not AssetFolderDeleted folderDeleted) { - async Task PublishAsync(SquidexCommand command) + return; + } + + async Task PublishAsync(SquidexCommand command) + { + try { - try - { - if (command is IAppCommand appCommand) - { - // Unfortunately, the commands to not share a base class. - appCommand.AppId = folderDeleted.AppId; - } - - command.Actor = folderDeleted.Actor; - - await commandBus.PublishAsync(command, default); - } - catch (DomainObjectNotFoundException) - { - // The asset could already be deleted by another operation. - } - catch (Exception ex) - { - log.LogError(ex, "Failed to delete asset recursively."); - } + command.Actor = folderDeleted.Actor; + + await commandBus.PublishAsync(command); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to delete asset recursively."); } + } - var appId = folderDeleted.AppId; + var appId = folderDeleted.AppId; - var childAssetFolders = await assetFolderRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId, default); + var childAssetFolders = await assetFolderRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId, default); - foreach (var assetFolderId in childAssetFolders) - { - // These deletions will create more events which will then be deleted as well. - await PublishAsync(new DeleteAssetFolder { AssetFolderId = assetFolderId }); - } + foreach (var assetFolderId in childAssetFolders) + { + await PublishAsync(new DeleteAssetFolder { AppId = appId, AssetFolderId = assetFolderId }); + } - var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId, default); + var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId, default); - foreach (var assetId in childAssets) - { - // Basically the leaves of the tree. - await PublishAsync(new DeleteAsset { AssetId = assetId }); - } + foreach (var assetId in childAssets) + { + await PublishAsync(new DeleteAsset { AppId = appId, AssetId = assetId }); } } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientExtensions.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientExtensions.cs new file mode 100644 index 000000000..7e111b203 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientExtensions.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.ClientLibrary.Management; + +namespace TestSuite +{ + public static class ClientExtensions + { + public static async Task WaitForDeletionAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout) + { + try + { + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) + { + try + { + await assetsClient.GetAssetAsync(app, id, cts.Token); + } + catch (SquidexManagementException ex) when (ex.StatusCode == 404) + { + return true; + } + + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { + } + + return false; + } + + public static async Task> 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 await assetsClient.GetTagsAsync(app); + } + + public static async Task WaitForBackupAsync(this IBackupsClient backupsClient, string app, TimeSpan timeout) + { + try + { + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) + { + var backups = await backupsClient.GetBackupsAsync(app, cts.Token); + var backup = backups.Items.Find(x => x.Status == JobStatus.Completed || x.Status == JobStatus.Failed); + + if (backup != null) + { + return backup; + } + + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { + } + + return null; + } + + public static async Task WaitForRestoreAsync(this IBackupsClient backupsClient, Uri url, TimeSpan timeout) + { + try + { + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) + { + var restore = await backupsClient.GetRestoreJobAsync(cts.Token); + + if (restore.Url == url && restore.Status is JobStatus.Completed or JobStatus.Failed) + { + return restore; + } + + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { + } + + return null; + } + } +}