diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index e49efeb4d..e9157981f 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; +using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Backup; @@ -23,6 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public sealed class BackupAssets : BackupHandlerWithStore { + private static readonly JsonSerializer Serializer = JsonSerializer.Create(); private readonly HashSet assetIds = new HashSet(); private readonly IAssetStore assetStore; private readonly IAssetRepository assetRepository; @@ -73,6 +76,11 @@ namespace Squidex.Domain.Apps.Entities.Assets return TaskHelper.Done; } + public override Task BackupAsync(Guid appId, BackupWriter writer) + { + return BackupTagsAsync(appId, writer); + } + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader) { switch (@event.Payload) @@ -88,9 +96,37 @@ namespace Squidex.Domain.Apps.Entities.Assets return TaskHelper.Done; } - public override Task RestoreAsync(Guid appId, BackupReader reader) + public override async Task RestoreAsync(Guid appId, BackupReader reader) + { + await RestoreTagsAsync(appId, reader); + + await RebuildManyAsync(assetIds, id => RebuildAsync(id, (e, s) => s.Apply(e))); + } + + private Task RestoreTagsAsync(Guid appId, BackupReader reader) + { + return reader.ReadAttachmentAsync("AssetTags.json", async stream => + { + using (var textReader = new StreamReader(stream)) + { + var tags = (TagSet)Serializer.Deserialize(textReader, typeof(TagSet)); + + await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); + } + }); + } + + private Task BackupTagsAsync(Guid appId, BackupWriter writer) { - return RebuildManyAsync(assetIds, id => RebuildAsync(id, (e, s) => s.Apply(e))); + return writer.WriteAttachmentAsync("AssetTags.json", async stream => + { + var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); + + using (var textWriter = new StreamWriter(stream)) + { + Serializer.Serialize(textWriter, tags); + } + }); } private Task WriteAssetAsync(Guid assetId, long fileVersion, BackupWriter writer) diff --git a/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs index ec545f3dd..aa48fdbd4 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs @@ -8,16 +8,19 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Orleans; using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Backup { - public interface IBackupGrain : ICleanableAppGrain + public interface IBackupGrain : IGrainWithGuidKey { Task RunAsync(); Task DeleteAsync(Guid id); + Task ClearAsync(); + Task>> GetStateAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs index e1e7ac82c..a58689e4c 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs @@ -8,10 +8,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Orleans; namespace Squidex.Domain.Apps.Entities.Rules.Indexes { - public interface IRulesByAppIndex : ICleanableAppGrain + public interface IRulesByAppIndex : IGrainWithGuidKey { Task AddRuleAsync(Guid ruleId); @@ -19,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes Task RebuildAsync(HashSet rules); + Task ClearAsync(); + Task> GetRuleIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs index 701caa5ad..0cffd11a9 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs @@ -8,10 +8,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Orleans; namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { - public interface ISchemasByAppIndex : ICleanableAppGrain + public interface ISchemasByAppIndex : IGrainWithGuidKey { Task AddSchemaAsync(Guid schemaId, string name); @@ -19,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes Task RebuildAsync(Dictionary schemas); + Task ClearAsync(); + Task GetSchemaIdAsync(string name); Task> GetSchemaIdsAsync(); diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs index 48d7e1e49..d88dd023a 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -49,9 +49,14 @@ namespace Squidex.Domain.Apps.Entities.Tags return GetGrain(appId, group).GetTagsAsync(); } - public Task RebuildTagsAsync(Guid appId, string group, Dictionary allTags) + public Task GetExportableTagsAsync(Guid appId, string group) { - return GetGrain(appId, group).RebuildTagsAsync(allTags); + return GetGrain(appId, group).GetExportableTagsAsync(); + } + + public Task RebuildTagsAsync(Guid appId, string group, TagSet tags) + { + return GetGrain(appId, group).RebuildAsync(tags); } public Task ClearAsync(Guid appId, string group) diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs index 8218255c8..952702b10 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -21,7 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Tags Task> GetTagsAsync(); + Task GetExportableTagsAsync(); + Task ClearAsync(); - Task RebuildTagsAsync(Dictionary allTags); + + Task RebuildAsync(TagSet tags); } } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs index 19724eb1b..01fb3c9e8 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs @@ -21,7 +21,9 @@ namespace Squidex.Domain.Apps.Entities.Tags Task> GetTagsAsync(Guid appId, string group); - Task RebuildTagsAsync(Guid appId, string group, Dictionary allTags); + Task GetExportableTagsAsync(Guid appId, string group); + + Task RebuildTagsAsync(Guid appId, string group, TagSet tags); Task ClearAsync(Guid appId, string group); } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/Tag.cs b/src/Squidex.Domain.Apps.Entities/Tags/Tag.cs new file mode 100644 index 000000000..60fab8187 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Tags/Tag.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class Tag + { + public string Name { get; set; } + + public int Count { get; set; } = 1; + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs index a40110ac4..f4d837206 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -24,14 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Tags [CollectionName("Index_Tags")] public sealed class State { - public Dictionary Tags { get; set; } = new Dictionary(); - } - - public sealed class TagInfo - { - public string Name { get; set; } - - public int Count { get; set; } = 1; + public TagSet Tags { get; set; } = new TagSet(); } public TagGrain(IStore store) @@ -58,9 +51,11 @@ namespace Squidex.Domain.Apps.Entities.Tags return persistence.DeleteAsync(); } - public Task RebuildTagsAsync(Dictionary allTags) + public Task RebuildAsync(TagSet tags) { - throw new NotImplementedException(); + state.Tags = tags; + + return persistence.DeleteAsync(); } public async Task> NormalizeTagsAsync(HashSet names, HashSet ids) @@ -91,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Tags { tagId = Guid.NewGuid().ToString(); - state.Tags.Add(tagId, new TagInfo { Name = tagName }); + state.Tags.Add(tagId, new Tag { Name = tagName }); } result.Add(tagId); @@ -159,5 +154,10 @@ namespace Squidex.Domain.Apps.Entities.Tags { return Task.FromResult(state.Tags.Values.ToDictionary(x => x.Name, x => x.Count)); } + + public Task GetExportableTagsAsync() + { + return Task.FromResult(state.Tags); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs new file mode 100644 index 000000000..48815870e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class TagSet : Dictionary + { + } +} diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 9f704a490..b690a4d1b 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -96,7 +96,8 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - AddCommandPipeline(services); + services.AddCommandPipeline(); + services.AddBackupHandlers(); services.AddSingleton>(DomainObjectGrainFormatter.Format); @@ -119,7 +120,7 @@ namespace Squidex.Config.Domain }); } - private static void AddCommandPipeline(IServiceCollection services) + private static void AddCommandPipeline(this IServiceCollection services) { services.AddSingletonAs() .As(); @@ -145,6 +146,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs>() .As(); @@ -157,9 +161,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs>() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); @@ -182,6 +183,16 @@ namespace Squidex.Config.Domain .As(); } + private static void AddBackupHandlers(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + public static void AddMyMigrationServices(this IServiceCollection services) { services.AddSingletonAs() diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs index 7667f15a8..368b9c417 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs @@ -47,6 +47,64 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes .MustHaveHappened(); } + [Fact] + public async Task Should_not_be_able_to_reserve_index_if_name_taken() + { + await sut.AddAppAsync(appId2, appName1); + + Assert.False(await sut.ReserveAppAsync(appId1, appName1)); + } + + [Fact] + public async Task Should_not_be_able_to_reserve_if_name_reserved() + { + await sut.ReserveAppAsync(appId2, appName1); + + Assert.False(await sut.ReserveAppAsync(appId1, appName1)); + } + + [Fact] + public async Task Should_not_be_able_to_reserve_if_id_taken() + { + await sut.AddAppAsync(appId1, appName1); + + Assert.False(await sut.ReserveAppAsync(appId1, appName2)); + } + + [Fact] + public async Task Should_not_be_able_to_reserve_if_id_reserved() + { + await sut.ReserveAppAsync(appId1, appName1); + + Assert.False(await sut.ReserveAppAsync(appId1, appName2)); + } + + [Fact] + public async Task Should_be_able_to_reserve_if_id_and_name_not_reserved() + { + await sut.ReserveAppAsync(appId1, appName1); + + Assert.True(await sut.ReserveAppAsync(appId2, appName2)); + } + + [Fact] + public async Task Should_be_able_to_reserve_after_app_removed() + { + await sut.AddAppAsync(appId1, appName1); + await sut.RemoveAppAsync(appId1); + + Assert.True(await sut.ReserveAppAsync(appId1, appName1)); + } + + [Fact] + public async Task Should_be_able_to_reserve_after_reservation_removed() + { + await sut.ReserveAppAsync(appId1, appName1); + await sut.RemoveReservationAsync(appId1, appName1); + + Assert.True(await sut.ReserveAppAsync(appId1, appName1)); + } + [Fact] public async Task Should_remove_app_id_from_index() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs index 563429da0..c3667ee77 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs @@ -44,6 +44,26 @@ namespace Squidex.Domain.Apps.Entities.Tags .MustHaveHappened(); } + [Fact] + public async Task Should_call_grain_when_rebuilding() + { + var tags = new TagSet(); + + await sut.RebuildTagsAsync(appId, TagGroups.Assets, tags); + + A.CallTo(() => grain.RebuildAsync(tags)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_call_grain_when_retrieving_raw_tags() + { + await sut.GetExportableTagsAsync(appId, TagGroups.Assets); + + A.CallTo(() => grain.GetTagsAsync()) + .MustHaveHappened(); + } + [Fact] public async Task Should_call_grain_when_retrieving_tags() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs index 998252ada..90b9fa0c8 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs @@ -45,6 +45,30 @@ namespace Squidex.Domain.Apps.Entities.Tags .MustHaveHappened(); } + [Fact] + public async Task Should_rebuild_tags() + { + var tags = new TagSet + { + ["1"] = new Tag { Name = "tag1", Count = 1 }, + ["2"] = new Tag { Name = "tag2", Count = 2 }, + ["3"] = new Tag { Name = "tag3", Count = 6 } + }; + + await sut.RebuildAsync(tags); + + var allTags = await sut.GetTagsAsync(); + + Assert.Equal(new Dictionary + { + ["tag1"] = 1, + ["tag2"] = 2, + ["tag3"] = 6 + }, allTags); + + Assert.Same(tags, await sut.GetExportableTagsAsync()); + } + [Fact] public async Task Should_add_tags_to_grain() { diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs index 693435f22..4426982c6 100644 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Orleans; diff --git a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs index 225e67a10..24ed5e092 100644 --- a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs +++ b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs @@ -9,8 +9,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.State; using Squidex.Infrastructure; using Squidex.Infrastructure.Migrations;