diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 82507f4d9..d1a38f395 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -7,9 +7,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Orleans; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Rules; @@ -18,50 +16,49 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Security; -using Squidex.Shared; namespace Squidex.Domain.Apps.Entities { public sealed class AppProvider : IAppProvider { - private readonly IGrainFactory grainFactory; private readonly ILocalCache localCache; + private readonly IAppsIndex indexForApps; + private readonly IRulesIndex indexRules; + private readonly ISchemasIndex indexSchemas; - public AppProvider(IGrainFactory grainFactory, ILocalCache localCache) + public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas) { - Guard.NotNull(grainFactory, nameof(grainFactory)); + Guard.NotNull(indexForApps, nameof(indexForApps)); + Guard.NotNull(indexRules, nameof(indexRules)); + Guard.NotNull(indexSchemas, nameof(indexSchemas)); Guard.NotNull(localCache, nameof(localCache)); - this.grainFactory = grainFactory; - this.localCache = localCache; + this.indexForApps = indexForApps; + this.indexRules = indexRules; + this.indexSchemas = indexSchemas; } public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => { - using (Profiler.TraceMethod()) - { - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (!IsExisting(app)) - { - return (null, null); - } + var app = await GetAppAsync(appId); - var schema = await GetSchemaAsync(appId, id, false); + if (app == null) + { + return (null, null); + } - if (schema == null) - { - return (null, null); - } + var schema = await GetSchemaAsync(appId, id, false); - return (app.Value, schema); + if (schema == null) + { + return (null, null); } + + return (app, schema); }); } @@ -69,10 +66,7 @@ namespace Squidex.Domain.Apps.Entities { return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () => { - using (Profiler.TraceMethod()) - { - return await GetAppByIdAsync(appId); - } + return await indexForApps.GetAppAsync(appId); }); } @@ -80,17 +74,15 @@ namespace Squidex.Domain.Apps.Entities { return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => { - using (Profiler.TraceMethod()) - { - var appId = await GetAppIdAsync(appName); - - if (appId == Guid.Empty) - { - return null; - } + return await indexForApps.GetAppAsync(appName); + }); + } - return await GetAppByIdAsync(appId); - } + public Task> GetUserAppsAsync(string userId, PermissionSet permissions) + { + return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => + { + return await indexForApps.GetAppsForUserAsync(userId, permissions); }); } @@ -98,17 +90,7 @@ namespace Squidex.Domain.Apps.Entities { return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => { - using (Profiler.TraceMethod()) - { - var schemaId = await GetSchemaIdAsync(appId, name); - - if (schemaId == Guid.Empty) - { - return null; - } - - return await GetSchemaAsync(appId, schemaId, false); - } + return await indexSchemas.GetSchemaAsync(appId, name); }); } @@ -116,17 +98,7 @@ namespace Squidex.Domain.Apps.Entities { return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => { - using (Profiler.TraceMethod()) - { - var schema = await grainFactory.GetGrain(id).GetStateAsync(); - - if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId) - { - return null; - } - - return schema.Value; - } + return await indexSchemas.GetSchemaAsync(appId, id, allowDeleted); }); } @@ -134,16 +106,7 @@ namespace Squidex.Domain.Apps.Entities { return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => { - using (Profiler.TraceMethod()) - { - var ids = await grainFactory.GetGrain(appId).GetSchemaIdsAsync(); - - var schemas = - await Task.WhenAll( - ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - - return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList(); - } + return await indexSchemas.GetSchemasAsync(appId); }); } @@ -151,100 +114,8 @@ namespace Squidex.Domain.Apps.Entities { return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => { - using (Profiler.TraceMethod()) - { - var ids = await grainFactory.GetGrain(appId).GetRuleIdsAsync(); - - var rules = - await Task.WhenAll( - ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - - return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList(); - } + return await indexRules.GetRulesAsync(appId); }); } - - public Task> GetUserApps(string userId, PermissionSet permissions) - { - Guard.NotNull(userId, nameof(userId)); - Guard.NotNull(permissions, nameof(permissions)); - - return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () => - { - using (Profiler.TraceMethod()) - { - var ids = - await Task.WhenAll( - GetAppIdsByUserAsync(userId), - GetAppIdsAsync(permissions.ToAppNames())); - - var apps = - await Task.WhenAll(ids - .SelectMany(x => x) - .Select(id => grainFactory.GetGrain(id).GetStateAsync())); - - return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList(); - } - }); - } - - private async Task GetAppByIdAsync(Guid appId) - { - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (!IsExisting(app)) - { - return null; - } - - return app.Value; - } - - private async Task> GetAppIdsByUserAsync(string userId) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(userId).GetAppIdsAsync(); - } - } - - private async Task> GetAppIdsAsync(IEnumerable names) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetAppIdsAsync(names.ToArray()); - } - } - - private async Task GetAppIdAsync(string name) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetAppIdAsync(name); - } - } - - private async Task GetSchemaIdAsync(Guid appId, string name) - { - using (Profiler.TraceMethod()) - { - return await grainFactory.GetGrain(appId).GetSchemaIdAsync(name); - } - } - - private static bool IsFound(IEntityWithVersion entity) - { - return entity.Version > EtagVersion.Empty; - } - - private static bool IsExisting(J app) - { - return IsFound(app.Value) && !app.Value.IsArchived; - } - - private static bool IsExisting(J schema, bool allowDeleted) - { - return IsFound(schema.Value) && (!schema.Value.IsDeleted || allowDeleted); - } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index d6f389ecc..2cb2c5236 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps @@ -27,24 +26,24 @@ namespace Squidex.Domain.Apps.Entities.Apps private const string SettingsFile = "Settings.json"; private readonly IGrainFactory grainFactory; private readonly IUserResolver userResolver; - private readonly IAppsByNameIndex appsByNameIndex; + private readonly IAppsIndex grainAppIndex; private readonly HashSet contributors = new HashSet(); private readonly Dictionary userMapping = new Dictionary(); private Dictionary usersWithEmail = new Dictionary(); - private bool isReserved; + private string appReservation; private string appName; public override string Name { get; } = "Apps"; - public BackupApps(IGrainFactory grainFactory, IUserResolver userResolver) + public BackupApps(IGrainFactory grainFactory, IUserResolver userResolver, IAppsIndex grainAppIndex) { + Guard.NotNull(grainAppIndex, nameof(grainAppIndex)); Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(userResolver, nameof(userResolver)); + this.grainAppIndex = grainAppIndex; this.grainFactory = grainFactory; this.userResolver = userResolver; - - appsByNameIndex = grainFactory.GetGrain(SingleGrain.Id); } public override async Task BackupEventAsync(Envelope @event, Guid appId, BackupWriter writer) @@ -125,7 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Apps private async Task ReserveAppAsync(Guid appId) { - if (!(isReserved = await appsByNameIndex.ReserveAppAsync(appId, appName))) + appReservation = await grainAppIndex.ReserveAsync(appId, appName); + + if (appReservation == null) { throw new BackupRestoreException("The app id or name is not available."); } @@ -133,10 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Apps public override async Task CleanupRestoreErrorAsync(Guid appId) { - if (isReserved) - { - await appsByNameIndex.RemoveReservationAsync(appId, appName); - } + await grainAppIndex.RemoveReservationAsync(appReservation); } private RefToken MapUser(string userId, RefToken fallback) @@ -196,12 +194,9 @@ namespace Squidex.Domain.Apps.Entities.Apps public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) { - await appsByNameIndex.AddAppAsync(appId, appName); + await grainAppIndex.AddAsync(appReservation); - foreach (var user in contributors) - { - await grainFactory.GetGrain(user).AddAppAsync(appId); - } + await grainAppIndex.RebuildByContributorsAsync(appId, contributors); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs index 4466f7a5b..729ef1441 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -17,13 +17,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics { public sealed class OrleansAppsHealthCheck : IHealthCheck { - private readonly IAppsByNameIndex index; + private readonly IAppsByNameIndexGrain index; public OrleansAppsHealthCheck(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); - index = grainFactory.GetGrain(SingleGrain.Id); + index = grainFactory.GetGrain(SingleGrain.Id); } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs deleted file mode 100644 index acb09a3e3..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsByNameIndexCommandMiddleware : ICommandMiddleware - { - private readonly IAppsByNameIndex index; - - public AppsByNameIndexCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - index = grainFactory.GetGrain(SingleGrain.Id); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - var createApp = context.Command as CreateApp; - - var isReserved = false; - try - { - if (createApp != null) - { - isReserved = await index.ReserveAppAsync(createApp.AppId, createApp.Name); - - if (!isReserved) - { - var error = new ValidationError("An app with the same name already exists.", nameof(createApp.Name)); - - throw new ValidationException("Cannot create app.", error); - } - } - - await next(); - - if (context.IsCompleted) - { - if (createApp != null) - { - await index.AddAppAsync(createApp.AppId, createApp.Name); - } - else if (context.Command is ArchiveApp archiveApp) - { - await index.RemoveAppAsync(archiveApp.AppId); - } - } - } - finally - { - if (isReserved) - { - await index.RemoveReservationAsync(createApp.AppId, createApp.Name); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs index 968db7c4b..0a0d78e82 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs @@ -6,128 +6,22 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Orleans.Indexes; using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Apps.Indexes { - public sealed class AppsByNameIndexGrain : GrainOfString, IAppsByNameIndex + public sealed class AppsByNameIndexGrain : UniqueNameIndexGrain, IAppsByNameIndexGrain { - private readonly HashSet reservedIds = new HashSet(); - private readonly HashSet reservedNames = new HashSet(); - private readonly IGrainState state; - - [CollectionName("Index_AppsByName")] - public sealed class GrainState - { - public Dictionary Apps { get; set; } = new Dictionary(StringComparer.Ordinal); - } - - public AppsByNameIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task RebuildAsync(Dictionary apps) - { - state.Value = new GrainState { Apps = apps }; - - return state.WriteAsync(); - } - - public Task ReserveAppAsync(Guid appId, string name) - { - var canReserve = !IsInUse(appId, name) && !IsReserved(appId, name); - - if (canReserve) - { - reservedIds.Add(appId); - reservedNames.Add(name); - } - - return Task.FromResult(canReserve); - } - - private bool IsInUse(Guid appId, string name) - { - return state.Value.Apps.ContainsKey(name) || state.Value.Apps.Any(x => x.Value == appId); - } - - private bool IsReserved(Guid appId, string name) + public AppsByNameIndexGrain(IGrainState state) + : base(state) { - return reservedIds.Contains(appId) || reservedNames.Contains(name); - } - - public Task RemoveReservationAsync(Guid appId, string name) - { - reservedIds.Remove(appId); - reservedNames.Remove(name); - - return TaskHelper.Done; - } - - public Task AddAppAsync(Guid appId, string name) - { - state.Value.Apps[name] = appId; - - reservedIds.Remove(appId); - reservedNames.Remove(name); - - return state.WriteAsync(); - } - - public Task RemoveAppAsync(Guid appId) - { - var name = state.Value.Apps.FirstOrDefault(x => x.Value == appId).Key; - - if (!string.IsNullOrWhiteSpace(name)) - { - state.Value.Apps.Remove(name); - - reservedIds.Remove(appId); - reservedNames.Remove(name); - } - - return state.WriteAsync(); - } - - public Task> GetAppIdsAsync(params string[] names) - { - var appIds = new List(); - - foreach (var appName in names) - { - if (state.Value.Apps.TryGetValue(appName, out var appId)) - { - appIds.Add(appId); - } - } - - return Task.FromResult(appIds); - } - - public Task GetAppIdAsync(string appName) - { - state.Value.Apps.TryGetValue(appName, out var appId); - - return Task.FromResult(appId); - } - - public Task> GetAppIdsAsync() - { - return Task.FromResult(state.Value.Apps.Values.ToList()); } + } - public Task CountAsync() - { - return Task.FromResult((long)state.Value.Apps.Count); - } + [CollectionName("Index_AppsByName")] + public sealed class AppsByNameIndexState : UniqueNameIndexState + { } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs deleted file mode 100644 index 5e2454fab..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsByUserIndexCommandMiddleware : ICommandMiddleware - { - private readonly IGrainFactory grainFactory; - - public AppsByUserIndexCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.IsCompleted) - { - switch (context.Command) - { - case CreateApp createApp: - await Index(GetUserId(createApp)).AddAppAsync(createApp.AppId); - break; - case AssignContributor assignContributor: - await Index(GetUserId(assignContributor)).AddAppAsync(assignContributor.AppId); - break; - case RemoveContributor removeContributor: - await Index(GetUserId(removeContributor)).RemoveAppAsync(removeContributor.AppId); - break; - case ArchiveApp archiveApp: - { - var appState = await grainFactory.GetGrain(archiveApp.AppId).GetStateAsync(); - - foreach (var contributorId in appState.Value.Contributors.Keys) - { - await Index(contributorId).RemoveAppAsync(archiveApp.AppId); - } - - break; - } - } - } - - await next(); - } - - private static string GetUserId(CreateApp createApp) - { - return createApp.Actor.Identifier; - } - - private static string GetUserId(AssignContributor assignContributor) - { - return assignContributor.ContributorId; - } - - private static string GetUserId(RemoveContributor removeContributor) - { - return removeContributor.ContributorId; - } - - private IAppsByUserIndex Index(string id) - { - return grainFactory.GetGrain(id); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs index 28b3f5541..acd31e195 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs @@ -6,56 +6,22 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Orleans.Indexes; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Apps.Indexes { - public sealed class AppsByUserIndexGrain : GrainOfString, IAppsByUserIndex + public sealed class AppsByUserIndexGrain : IdsIndexGrain, IAppsByUserIndexGrain { - private readonly IGrainState state; - - [CollectionName("Index_AppsByUser")] - public sealed class GrainState - { - public HashSet Apps { get; set; } = new HashSet(); - } - - public AppsByUserIndexGrain(IGrainState state) + public AppsByUserIndexGrain(IGrainState state) + : base(state) { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task RebuildAsync(HashSet apps) - { - state.Value = new GrainState { Apps = apps }; - - return state.WriteAsync(); - } - - public Task AddAppAsync(Guid appId) - { - state.Value.Apps.Add(appId); - - return state.WriteAsync(); - } - - public Task RemoveAppAsync(Guid appId) - { - state.Value.Apps.Remove(appId); - - return state.WriteAsync(); } + } - public Task> GetAppIdsAsync() - { - return Task.FromResult(state.Value.Apps.ToList()); - } + [CollectionName("Index_AppsByUser")] + public sealed class AppsByUserIndex : IdsIndexState + { } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs new file mode 100644 index 000000000..07ea8ac59 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -0,0 +1,286 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsIndex : IAppsIndex, ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public AppsIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task RebuildByContributorsAsync(Guid appId, HashSet contributors) + { + foreach (var contributorId in contributors) + { + await Index(contributorId).AddAsync(appId); + } + } + + public Task RebuildByContributorsAsync(string contributorId, HashSet apps) + { + return Index(contributorId).RebuildAsync(apps); + } + + public Task RebuildAsync(Dictionary appsByName) + { + return Index().RebuildAsync(appsByName); + } + + public Task RemoveReservationAsync(string token) + { + return Index().RemoveReservationAsync(token); + } + + public Task> GetIdsAsync() + { + return Index().GetIdsAsync(); + } + + public Task AddAsync(string token) + { + return Index().AddAsync(token); + } + + public Task ReserveAsync(Guid id, string name) + { + return Index().ReserveAsync(id, name); + } + + public async Task> GetAppsAsync() + { + using (Profiler.TraceMethod()) + { + var ids = await GetAppIdsAsync(); + + var apps = + await Task.WhenAll(ids + .Select(id => GetAppAsync(id))); + + return apps.Where(x => x != null).ToList(); + } + } + + public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions) + { + using (Profiler.TraceMethod()) + { + var ids = + await Task.WhenAll( + GetAppIdsByUserAsync(userId), + GetAppIdsAsync(permissions.ToAppNames())); + + var apps = + await Task.WhenAll(ids + .SelectMany(x => x) + .Select(id => GetAppAsync(id))); + + return apps.Where(x => x != null).ToList(); + } + } + + public async Task GetAppAsync(string name) + { + using (Profiler.TraceMethod()) + { + var appId = await GetAppIdAsync(name); + + if (appId == default) + { + return null; + } + + return await GetAppAsync(appId); + } + } + + public async Task GetAppAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + var app = await grainFactory.GetGrain(appId).GetStateAsync(); + + if (IsFound(app.Value)) + { + return app.Value; + } + + return null; + } + } + + private async Task> GetAppIdsByUserAsync(string userId) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(userId).GetIdsAsync(); + } + } + + private async Task> GetAppIdsAsync() + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(); + } + } + + private async Task> GetAppIdsAsync(string[] names) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(names); + } + } + + private async Task GetAppIdAsync(string name) + { + using (Profiler.TraceMethod()) + { + return await grainFactory.GetGrain(SingleGrain.Id).GetIdAsync(name); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is CreateApp createApp) + { + var index = Index(); + + string token = await CheckAppAsync(index, createApp); + + try + { + await next(); + } + finally + { + if (token != null) + { + if (context.IsCompleted) + { + await index.AddAsync(token); + + if (createApp.Actor.IsSubject) + { + await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId); + } + } + else + { + await index.RemoveReservationAsync(token); + } + } + } + } + else + { + await next(); + + if (context.IsCompleted) + { + switch (context.Command) + { + case AssignContributor assignContributor: + await AssignContributorAsync(assignContributor); + break; + + case RemoveContributor removeContributor: + await RemoveContributorAsync(removeContributor); + break; + + case ArchiveApp archiveApp: + await ArchiveAppAsync(archiveApp); + break; + } + } + } + } + + private async Task CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command) + { + var name = command.Name; + + if (name.IsSlug()) + { + var token = await index.ReserveAsync(command.AppId, name); + + if (token == null) + { + var error = new ValidationError("An app with this already exists."); + + throw new ValidationException("Cannot create app.", error); + } + + return token; + } + + return null; + } + + private Task AssignContributorAsync(AssignContributor command) + { + return Index(command.ContributorId).AddAsync(command.AppId); + } + + private Task RemoveContributorAsync(RemoveContributor command) + { + return Index(command.ContributorId).RemoveAsync(command.AppId); + } + + private async Task ArchiveAppAsync(ArchiveApp command) + { + var appId = command.AppId; + + var app = await grainFactory.GetGrain(appId).GetStateAsync(); + + if (IsFound(app.Value)) + { + await Index().RemoveAsync(appId); + } + + foreach (var contributorId in app.Value.Contributors.Keys) + { + await Index(contributorId).RemoveAsync(appId); + } + } + + private static bool IsFound(IAppEntity app) + { + return app.Version > EtagVersion.Empty && !app.IsArchived; + } + + private IAppsByNameIndexGrain Index() + { + return grainFactory.GetGrain(SingleGrain.Id); + } + + private IAppsByUserIndexGrain Index(string id) + { + return grainFactory.GetGrain(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs deleted file mode 100644 index 2e98ade47..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public interface IAppsByNameIndex : IGrainWithStringKey - { - Task CountAsync(); - - Task ReserveAppAsync(Guid appId, string name); - - Task AddAppAsync(Guid appId, string name); - - Task RemoveAppAsync(Guid appId); - - Task RebuildAsync(Dictionary apps); - - Task RemoveReservationAsync(Guid appId, string name); - - Task> GetAppIdsAsync(); - - Task> GetAppIdsAsync(string[] names); - - Task GetAppIdAsync(string name); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs similarity index 61% rename from src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs index e769f8803..13566260a 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs @@ -6,20 +6,12 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Orleans; +using Squidex.Infrastructure.Orleans.Indexes; namespace Squidex.Domain.Apps.Entities.Apps.Indexes { - public interface IAppsByUserIndex : IGrainWithStringKey + public interface IAppsByNameIndexGrain : IUniqueNameIndexGrain, IGrainWithStringKey { - Task AddAppAsync(Guid appId); - - Task RemoveAppAsync(Guid appId); - - Task RebuildAsync(HashSet apps); - - Task> GetAppIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs new file mode 100644 index 000000000..6a19ccd43 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; +using Squidex.Infrastructure.Orleans.Indexes; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public interface IAppsByUserIndexGrain : IIdsIndexGrain, IGrainWithStringKey + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs new file mode 100644 index 000000000..505b519a9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public interface IAppsIndex + { + Task> GetIdsAsync(); + + Task> GetAppsAsync(); + + Task> GetAppsForUserAsync(string userId, PermissionSet permissions); + + Task GetAppAsync(string name); + + Task GetAppAsync(Guid appId); + + Task ReserveAsync(Guid id, string name); + + Task AddAsync(string token); + + Task RemoveReservationAsync(string token); + + Task RebuildByContributorsAsync(string contributorId, HashSet apps); + + Task RebuildAsync(Dictionary apps); + + Task RebuildByContributorsAsync(Guid appId, HashSet contributors); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index e7e9f0cf5..fd8fdb53e 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Threading.Tasks; -using Orleans; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -21,26 +20,23 @@ namespace Squidex.Domain.Apps.Entities.Assets public sealed class AssetChangedTriggerHandler : RuleTriggerHandler { private readonly IScriptEngine scriptEngine; - private readonly IGrainFactory grainFactory; + private readonly IAssetLoader assetLoader; - public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory) + public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IAssetLoader assetLoader) { Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(grainFactory, nameof(grainFactory)); + Guard.NotNull(assetLoader, nameof(assetLoader)); this.scriptEngine = scriptEngine; - this.grainFactory = grainFactory; + this.assetLoader = assetLoader; } protected override async Task CreateEnrichedEventAsync(Envelope @event) { var result = new EnrichedAssetEvent(); - var asset = - (await grainFactory - .GetGrain(@event.Payload.AssetId) - .GetStateAsync(@event.Headers.EventStreamNumber())).Value; + var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber()); SimpleMapper.Map(asset, result); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs new file mode 100644 index 000000000..21bcca35f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetLoader + { + Task GetAsync(Guid id, long version = EtagVersion.Any); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs new file mode 100644 index 000000000..82adbe7db --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public sealed class AssetLoader : IAssetLoader + { + private readonly IGrainFactory grainFactory; + + public AssetLoader(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task GetAsync(Guid id, long version) + { + using (Profiler.TraceMethod()) + { + var grain = grainFactory.GetGrain(id); + + var content = await grain.GetStateAsync(version); + + if (content.Value == null || content.Value.Version != version) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IAssetEntity)); + } + + return content.Value; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 8132c6f28..30167fe77 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -7,7 +7,6 @@ using System; using System.Threading.Tasks; -using Orleans; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; @@ -23,26 +22,23 @@ namespace Squidex.Domain.Apps.Entities.Contents public sealed class ContentChangedTriggerHandler : RuleTriggerHandler { private readonly IScriptEngine scriptEngine; - private readonly IGrainFactory grainFactory; + private readonly IContentLoader contentLoader; - public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory) + public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader) { Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(grainFactory, nameof(grainFactory)); + Guard.NotNull(contentLoader, nameof(contentLoader)); this.scriptEngine = scriptEngine; - this.grainFactory = grainFactory; + this.contentLoader = contentLoader; } protected override async Task CreateEnrichedEventAsync(Envelope @event) { var result = new EnrichedContentEvent(); - var content = - (await grainFactory - .GetGrain(@event.Payload.ContentId) - .GetStateAsync(@event.Headers.EventStreamNumber())).Value; + var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber()); SimpleMapper.Map(content, result); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs similarity index 76% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs rename to src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs index e55988d52..664fdde45 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs @@ -7,11 +7,12 @@ using System; using System.Threading.Tasks; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents { - public interface IContentVersionLoader + public interface IContentLoader { - Task LoadAsync(Guid id, long version); + Task GetAsync(Guid id, long version = EtagVersion.Any); } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentVersionLoader.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs similarity index 81% rename from src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentVersionLoader.cs rename to src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs index c020703af..572e4d9fc 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentVersionLoader.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -13,20 +13,20 @@ using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Entities.Contents.Queries { - public sealed class ContentVersionLoader : IContentVersionLoader + public sealed class ContentLoader : IContentLoader { private readonly IGrainFactory grainFactory; - public ContentVersionLoader(IGrainFactory grainFactory) + public ContentLoader(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); this.grainFactory = grainFactory; } - public async Task LoadAsync(Guid id, long version) + public async Task GetAsync(Guid id, long version) { - using (Profiler.TraceMethod()) + using (Profiler.TraceMethod()) { var grain = grainFactory.GetGrain(id); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 739963615..aa43adaa1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IAssetUrlGenerator assetUrlGenerator; private readonly IContentEnricher contentEnricher; private readonly IContentRepository contentRepository; - private readonly IContentVersionLoader contentVersionLoader; + private readonly IContentLoader contentVersionLoader; private readonly IScriptEngine scriptEngine; private readonly ContentQueryParser queryParser; @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries IAssetUrlGenerator assetUrlGenerator, IContentEnricher contentEnricher, IContentRepository contentRepository, - IContentVersionLoader contentVersionLoader, + IContentLoader contentVersionLoader, IScriptEngine scriptEngine, ContentQueryParser queryParser) { @@ -330,7 +330,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private Task FindByVersionAsync(Guid id, long version) { - return contentVersionLoader.LoadAsync(id, version); + return contentVersionLoader.GetAsync(id, version); } private static bool WithDraft(Context context) diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index 114ac3384..8fa6200dc 100644 --- a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities Task GetAppAsync(string appName); + Task> GetUserAppsAsync(string userId, PermissionSet permissions); + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); Task GetSchemaAsync(Guid appId, string name); @@ -30,7 +32,5 @@ namespace Squidex.Domain.Apps.Entities Task> GetSchemasAsync(Guid appId); Task> GetRulesAsync(Guid appId); - - Task> GetUserApps(string userId, PermissionSet permissions); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs index 7ab677449..f021bdc16 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs @@ -8,10 +8,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Orleans; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -22,16 +20,15 @@ namespace Squidex.Domain.Apps.Entities.Rules public sealed class BackupRules : BackupHandler { private readonly HashSet ruleIds = new HashSet(); - private readonly IGrainFactory grainFactory; + private readonly IRulesIndex indexForRules; public override string Name { get; } = "Rules"; - public BackupRules(IGrainFactory grainFactory, IRuleEventRepository ruleEventRepository) + public BackupRules(IRulesIndex indexForRules) { - Guard.NotNull(grainFactory, nameof(grainFactory)); - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(indexForRules, nameof(indexForRules)); - this.grainFactory = grainFactory; + this.indexForRules = indexForRules; } public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) @@ -49,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Rules return TaskHelper.True; } - public override async Task RestoreAsync(Guid appId, BackupReader reader) + public override Task RestoreAsync(Guid appId, BackupReader reader) { - await grainFactory.GetGrain(appId).RebuildAsync(ruleIds); + return indexForRules.RebuildAsync(appId, ruleIds); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs index ef69c574f..829f5fb95 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs @@ -20,5 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Rules NamedId AppId { get; set; } Rule RuleDef { get; } + + bool IsDeleted { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs new file mode 100644 index 000000000..d7eb0223a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; +using Squidex.Infrastructure.Orleans.Indexes; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public interface IRulesByAppIndexGrain : IIdsIndexGrain, IGrainWithGuidKey + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs similarity index 64% rename from src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs rename to src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs index a58689e4c..607b56129 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs @@ -8,20 +8,13 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Orleans; namespace Squidex.Domain.Apps.Entities.Rules.Indexes { - public interface IRulesByAppIndex : IGrainWithGuidKey + public interface IRulesIndex { - Task AddRuleAsync(Guid ruleId); + Task> GetRulesAsync(Guid appId); - Task RemoveRuleAsync(Guid ruleId); - - Task RebuildAsync(HashSet rules); - - Task ClearAsync(); - - Task> GetRuleIdsAsync(); + Task RebuildAsync(Guid appId, HashSet rules); } -} +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs deleted file mode 100644 index faee7b695..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public sealed class RulesByAppIndexCommandMiddleware : ICommandMiddleware - { - private readonly IGrainFactory grainFactory; - - public RulesByAppIndexCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.IsCompleted) - { - switch (context.Command) - { - case CreateRule createRule: - await Index(createRule.AppId.Id).AddRuleAsync(createRule.RuleId); - break; - case DeleteRule deleteRule: - { - var schema = await grainFactory.GetGrain(deleteRule.RuleId).GetStateAsync(); - - await Index(schema.Value.AppId.Id).RemoveRuleAsync(deleteRule.RuleId); - - break; - } - } - } - - await next(); - } - - private IRulesByAppIndex Index(Guid appId) - { - return grainFactory.GetGrain(appId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs index 472cd47f5..aa5ad8935 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs @@ -6,61 +6,22 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Orleans.Indexes; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules.Indexes { - public sealed class RulesByAppIndexGrain : GrainOfGuid, IRulesByAppIndex + public sealed class RulesByAppIndexGrain : IdsIndexGrain, IRulesByAppIndexGrain { - private readonly IGrainState state; - - [CollectionName("Index_RulesByApp")] - public sealed class GrainState - { - public HashSet Rules { get; set; } = new HashSet(); - } - - public RulesByAppIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task RebuildAsync(HashSet rules) - { - state.Value = new GrainState { Rules = rules }; - - return state.WriteAsync(); - } - - public Task AddRuleAsync(Guid ruleId) - { - state.Value.Rules.Add(ruleId); - - return state.WriteAsync(); - } - - public Task RemoveRuleAsync(Guid ruleId) + public RulesByAppIndexGrain(IGrainState state) + : base(state) { - state.Value.Rules.Remove(ruleId); - - return state.WriteAsync(); } + } - public Task> GetRuleIdsAsync() - { - return Task.FromResult(state.Value.Rules.ToList()); - } + [CollectionName("Index_RulesByApp")] + public sealed class RulesByAppIndexState : IdsIndexState + { } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs new file mode 100644 index 000000000..f57d9154f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesIndex : ICommandMiddleware, IRulesIndex + { + private readonly IGrainFactory grainFactory; + + public RulesIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public Task RebuildAsync(Guid appId, HashSet rues) + { + return Index(appId).RebuildAsync(rues); + } + + public async Task> GetRulesAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + var ids = await GetRuleIdsAsync(appId); + + var rules = + await Task.WhenAll( + ids.Select(GetRuleAsync)); + + return rules.Where(x => x != null).ToList(); + } + } + + private async Task GetRuleAsync(Guid id) + { + using (Profiler.TraceMethod()) + { + var ruleEntity = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(ruleEntity.Value)) + { + return ruleEntity.Value; + } + + return null; + } + } + + private async Task> GetRuleIdsAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdsAsync(); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + await next(); + + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateRule createRule: + await CreateRuleAsync(createRule); + break; + case DeleteRule deleteRule: + await DeleteRuleAsync(deleteRule); + break; + } + } + } + + private async Task CreateRuleAsync(CreateRule command) + { + await Index(command.AppId.Id).AddAsync(command.RuleId); + } + + private async Task DeleteRuleAsync(DeleteRule command) + { + var id = command.RuleId; + + var rule = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(rule.Value)) + { + await Index(rule.Value.AppId.Id).RemoveAsync(id); + } + } + + private IRulesByAppIndexGrain Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + + private static bool IsFound(IRuleEntity rule) + { + return rule.Version > EtagVersion.Empty && !rule.IsDeleted; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index d89e1c41c..0f7084352 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -29,13 +29,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking public sealed class Target { + public NamedId AppId { get; set; } + public int Limits { get; set; } public int? NumDays { get; set; } public DateTime? Triggered { get; set; } - - public NamedId AppId { get; set; } } [CollectionName("UsageTracker")] diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs index 3718ac35f..445218b1d 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Orleans; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Events.Schemas; @@ -21,15 +20,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas public sealed class BackupSchemas : BackupHandler { private readonly Dictionary schemasByName = new Dictionary(); - private readonly IGrainFactory grainFactory; + private readonly ISchemasIndex indexSchemas; public override string Name { get; } = "Schemas"; - public BackupSchemas(IGrainFactory grainFactory) + public BackupSchemas(ISchemasIndex indexSchemas) { - Guard.NotNull(grainFactory, nameof(grainFactory)); + Guard.NotNull(indexSchemas, nameof(indexSchemas)); - this.grainFactory = grainFactory; + this.indexSchemas = indexSchemas; } public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) @@ -47,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas return TaskHelper.True; } - public override async Task RestoreAsync(Guid appId, BackupReader reader) + public override Task RestoreAsync(Guid appId, BackupReader reader) { - await grainFactory.GetGrain(appId).RebuildAsync(schemasByName); + return indexSchemas.RebuildAsync(appId, schemasByName); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index 712fe84f2..92966a7f1 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; @@ -20,20 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { public static class GuardSchema { - public static Task CanCreate(CreateSchema command, IAppProvider appProvider) + public static void CanCreate(CreateSchema command) { Guard.NotNull(command, nameof(command)); - return Validate.It(() => "Cannot create schema.", async e => + Validate.It(() => "Cannot create schema.", e => { if (!command.Name.IsSlug()) { e(Not.ValidSlug("Name"), nameof(command.Name)); } - else if (await appProvider.GetSchemaAsync(command.AppId.Id, command.Name) != null) - { - e("A schema with the same name already exists."); - } ValidateUpsert(command, e); }); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs new file mode 100644 index 000000000..7a73fe20d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; +using Squidex.Infrastructure.Orleans.Indexes; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public interface ISchemasByAppIndexGrain : IUniqueNameIndexGrain, IGrainWithGuidKey + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs similarity index 57% rename from src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs rename to src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs index 0cffd11a9..a47dc6019 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs @@ -8,22 +8,17 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Orleans; namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { - public interface ISchemasByAppIndex : IGrainWithGuidKey + public interface ISchemasIndex { - Task AddSchemaAsync(Guid schemaId, string name); + Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); - Task RemoveSchemaAsync(Guid schemaId); + Task GetSchemaAsync(Guid appId, string name, bool allowDeleted = false); - Task RebuildAsync(Dictionary schemas); + Task> GetSchemasAsync(Guid appId, bool allowDeleted = false); - Task ClearAsync(); - - Task GetSchemaIdAsync(string name); - - Task> GetSchemaIdsAsync(); + Task RebuildAsync(Guid appId, Dictionary schemas); } -} +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs deleted file mode 100644 index 1090894d5..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public sealed class SchemasByAppIndexCommandMiddleware : ICommandMiddleware - { - private readonly IGrainFactory grainFactory; - - public SchemasByAppIndexCommandMiddleware(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - - this.grainFactory = grainFactory; - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (context.IsCompleted) - { - switch (context.Command) - { - case CreateSchema createSchema: - await Index(createSchema.AppId.Id).AddSchemaAsync(createSchema.SchemaId, createSchema.Name); - break; - case DeleteSchema deleteSchema: - { - var schema = await grainFactory.GetGrain(deleteSchema.SchemaId).GetStateAsync(); - - await Index(schema.Value.AppId.Id).RemoveSchemaAsync(deleteSchema.SchemaId); - - break; - } - } - } - - await next(); - } - - private ISchemasByAppIndex Index(Guid appId) - { - return grainFactory.GetGrain(appId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs index 4b288d830..81a6e8aa1 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs @@ -6,68 +6,22 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Orleans.Indexes; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { - public sealed class SchemasByAppIndexGrain : GrainOfGuid, ISchemasByAppIndex + public sealed class SchemasByAppIndexGrain : UniqueNameIndexGrain, ISchemasByAppIndexGrain { - private readonly IGrainState state; - - [CollectionName("Index_SchemasByApp")] - public sealed class GrainState - { - public Dictionary Schemas { get; set; } = new Dictionary(); - } - - public SchemasByAppIndexGrain(IGrainState state) - { - Guard.NotNull(state, nameof(state)); - - this.state = state; - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task RebuildAsync(Dictionary schemas) - { - state.Value = new GrainState { Schemas = schemas }; - - return state.WriteAsync(); - } - - public Task AddSchemaAsync(Guid schemaId, string name) - { - state.Value.Schemas[name] = schemaId; - - return state.WriteAsync(); - } - - public Task RemoveSchemaAsync(Guid schemaId) - { - state.Value.Schemas.Remove(state.Value.Schemas.FirstOrDefault(x => x.Value == schemaId).Key ?? string.Empty); - - return state.WriteAsync(); - } - - public Task GetSchemaIdAsync(string name) + public SchemasByAppIndexGrain(IGrainState state) + : base(state) { - state.Value.Schemas.TryGetValue(name, out var schemaId); - - return Task.FromResult(schemaId); } + } - public Task> GetSchemaIdsAsync() - { - return Task.FromResult(state.Value.Schemas.Values.ToList()); - } + [CollectionName("Index_SchemasByApp")] + public sealed class SchemasByAppIndexGrainState : UniqueNameIndexState + { } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs new file mode 100644 index 000000000..49ad16bd9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -0,0 +1,181 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex + { + private readonly IGrainFactory grainFactory; + + public SchemasIndex(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public Task RebuildAsync(Guid appId, Dictionary schemas) + { + return Index(appId).RebuildAsync(schemas); + } + + public async Task> GetSchemasAsync(Guid appId, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var ids = await GetSchemaIdsAsync(appId); + + var schemas = + await Task.WhenAll( + ids.Select(id => GetSchemaAsync(appId, id, allowDeleted))); + + return schemas.Where(x => x != null).ToList(); + } + } + + public async Task GetSchemaAsync(Guid appId, string name, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var id = await GetSchemaIdAsync(appId, name); + + if (id == default) + { + return null; + } + + return await GetSchemaAsync(appId, id, allowDeleted); + } + } + + public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) + { + using (Profiler.TraceMethod()) + { + var schema = await grainFactory.GetGrain(id).GetStateAsync(); + + if (IsFound(schema.Value, allowDeleted)) + { + return schema.Value; + } + + return null; + } + } + + private async Task GetSchemaIdAsync(Guid appId, string name) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdAsync(name); + } + } + + private async Task> GetSchemaIdsAsync(Guid appId) + { + using (Profiler.TraceMethod()) + { + return await Index(appId).GetIdsAsync(); + } + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is CreateSchema createSchema) + { + var index = Index(createSchema.AppId.Id); + + string token = await CheckSchemaAsync(index, createSchema); + + try + { + await next(); + } + finally + { + if (token != null) + { + if (context.IsCompleted) + { + await index.AddAsync(token); + } + else + { + await index.RemoveReservationAsync(token); + } + } + } + } + else + { + await next(); + + if (context.IsCompleted) + { + if (context.Command is DeleteSchema deleteSchema) + { + await DeleteSchemaAsync(deleteSchema); + } + } + } + } + + private async Task CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command) + { + var name = command.Name; + + if (name.IsSlug()) + { + var token = await index.ReserveAsync(command.SchemaId, name); + + if (token == null) + { + var error = new ValidationError("A schema with this name already exists."); + + throw new ValidationException("Cannot create schema.", error); + } + + return token; + } + + return null; + } + + private async Task DeleteSchemaAsync(DeleteSchema commmand) + { + var schemaId = commmand.SchemaId; + + var schema = await grainFactory.GetGrain(schemaId).GetStateAsync(); + + if (IsFound(schema.Value, true)) + { + await Index(schema.Value.AppId.Id).RemoveAsync(schemaId); + } + } + + private ISchemasByAppIndexGrain Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + + private static bool IsFound(ISchemaEntity entity, bool allowDeleted) + { + return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 4623db885..1689b4cd1 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -27,17 +27,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas { public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain { - private readonly IAppProvider appProvider; private readonly IJsonSerializer serializer; - public SchemaGrain(IStore store, ISemanticLog log, IAppProvider appProvider, IJsonSerializer serializer) + public SchemaGrain(IStore store, ISemanticLog log, IJsonSerializer serializer) : base(store, log) { - Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(serializer, nameof(serializer)); - this.appProvider = appProvider; - this.serializer = serializer; } @@ -69,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas }); case CreateSchema createSchema: - return CreateReturnAsync(createSchema, async c => + return CreateReturn(createSchema, c => { - await GuardSchema.CanCreate(c, appProvider); + GuardSchema.CanCreate(c); Create(c); diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs new file mode 100644 index 000000000..4dd1427a3 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public interface IIdsIndexGrain + { + Task CountAsync(); + + Task RebuildAsync(HashSet ids); + + Task AddAsync(T id); + + Task RemoveAsync(T id); + + Task ClearAsync(); + + Task> GetIdsAsync(); + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs new file mode 100644 index 000000000..bca54d1a2 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public interface IUniqueNameIndexGrain + { + Task ReserveAsync(T id, string name); + + Task AddAsync(string token); + + Task CountAsync(); + + Task RemoveReservationAsync(string token); + + Task RemoveAsync(T id); + + Task RebuildAsync(Dictionary values); + + Task ClearAsync(); + + Task GetIdAsync(string name); + + Task> GetIdsAsync(string[] names); + + Task> GetIdsAsync(); + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs new file mode 100644 index 000000000..fdae2ceb2 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class IdsIndexGrain : Grain, IIdsIndexGrain where TState : IdsIndexState, new() + { + private readonly IGrainState state; + + public IdsIndexGrain(IGrainState state) + { + Guard.NotNull(state, nameof(state)); + + this.state = state; + } + + public Task CountAsync() + { + return Task.FromResult(state.Value.Ids.Count); + } + + public Task RebuildAsync(HashSet ids) + { + state.Value = new TState { Ids = ids }; + + return state.WriteAsync(); + } + + public Task AddAsync(T id) + { + state.Value.Ids.Add(id); + + return state.WriteAsync(); + } + + public Task RemoveAsync(T id) + { + state.Value.Ids.Remove(id); + + return state.WriteAsync(); + } + + public Task ClearAsync() + { + return state.ClearAsync(); + } + + public Task> GetIdsAsync() + { + return Task.FromResult(state.Value.Ids.ToList()); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs b/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs new file mode 100644 index 000000000..d6e4a150d --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class IdsIndexState + { + public HashSet Ids { get; set; } = new HashSet(); + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs b/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs new file mode 100644 index 000000000..5b71e57ca --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs @@ -0,0 +1,136 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexGrain : Grain, IUniqueNameIndexGrain where TState : UniqueNameIndexState, new() + { + private readonly Dictionary reservations = new Dictionary(); + private readonly IGrainState state; + + public UniqueNameIndexGrain(IGrainState state) + { + Guard.NotNull(state, nameof(state)); + + this.state = state; + } + + public Task CountAsync() + { + return Task.FromResult(state.Value.Names.Count); + } + + public Task ClearAsync() + { + reservations.Clear(); + + return state.ClearAsync(); + } + + public Task RebuildAsync(Dictionary names) + { + state.Value = new TState { Names = names }; + + return state.WriteAsync(); + } + + public Task ReserveAsync(T id, string name) + { + string token = default; + + if (!IsInUse(name) && !IsReserved(name)) + { + token = RandomHash.Simple(); + + reservations.Add(token, (name, id)); + } + + return Task.FromResult(token); + } + + public async Task AddAsync(string token) + { + if (reservations.TryGetValue(token ?? string.Empty, out var reservation)) + { + state.Value.Names.Add(reservation.Name, reservation.Id); + + await state.WriteAsync(); + + reservations.Remove(token); + + return true; + } + + return false; + } + + public Task RemoveReservationAsync(string token) + { + reservations.Remove(token ?? string.Empty); + + return TaskHelper.Done; + } + + public async Task RemoveAsync(T id) + { + var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key; + + if (name != null) + { + state.Value.Names.Remove(name); + + await state.WriteAsync(); + } + } + + public Task> GetIdsAsync(string[] names) + { + var result = new List(); + + if (names != null) + { + foreach (var name in names) + { + if (state.Value.Names.TryGetValue(name, out var id)) + { + result.Add(id); + } + } + } + + return Task.FromResult(result); + } + + public Task GetIdAsync(string name) + { + state.Value.Names.TryGetValue(name, out var id); + + return Task.FromResult(id); + } + + public Task> GetIdsAsync() + { + return Task.FromResult(state.Value.Names.Values.ToList()); + } + + private bool IsInUse(string name) + { + return state.Value.Names.ContainsKey(name); + } + + private bool IsReserved(string name) + { + return reservations.Values.Any(x => x.Name == name); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs b/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs new file mode 100644 index 000000000..66ea6c939 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexState + { + public Dictionary Names { get; set; } = new Dictionary(); + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 3fd53d8ee..8f4b398b0 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var userOrClientId = HttpContext.User.UserOrClientId(); var userPermissions = HttpContext.Permissions(); - var apps = await appProvider.GetUserApps(userOrClientId, userPermissions); + var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); var response = Deferred.Response(() => { diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index b831a6da2..13f2b9ca4 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -108,6 +108,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs(c => new Lazy(() => c.GetRequiredService())) .AsSelf(); @@ -117,8 +120,8 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); + services.AddSingletonAs() + .As(); services.AddSingletonAs() .As(); @@ -236,34 +239,31 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); + services.AddSingletonAs() + .As().As(); - services.AddSingletonAs() - .As(); + services.AddSingletonAs() + .As().As(); - services.AddSingletonAs() - .As(); + services.AddSingletonAs() + .As().As(); services.AddSingletonAs() .As(); - services.AddSingletonAs>() - .As(); - - services.AddSingletonAs>() + services.AddSingletonAs() .As(); - services.AddSingletonAs>() + services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); services.AddSingletonAs() diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs index ea4aea955..188e134bb 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs @@ -22,7 +22,6 @@ namespace Squidex.Domain.Apps.Entities.Apps public AppUISettingsGrainTests() { sut = new AppUISettingsGrain(grainState); - sut.ActivateAsync(Guid.Empty).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs deleted file mode 100644 index affdb6977..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public class AppsByNameIndexCommandMiddlewareTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly IAppsByNameIndex index = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly AppsByNameIndexCommandMiddleware sut; - - public AppsByNameIndexCommandMiddlewareTests() - { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(index); - - sut = new AppsByNameIndexCommandMiddleware(grainFactory); - } - - [Fact] - public async Task Should_add_app_to_index_on_create() - { - A.CallTo(() => index.ReserveAppAsync(appId, "my-app")) - .Returns(true); - - var context = - new CommandContext(new CreateApp { AppId = appId, Name = "my-app" }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.ReserveAppAsync(appId, "my-app")) - .MustHaveHappened(); - - A.CallTo(() => index.AddAppAsync(appId, "my-app")) - .MustHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(appId, "my-app")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_remove_reservation_when_not_reserved() - { - A.CallTo(() => index.ReserveAppAsync(appId, "my-app")) - .Returns(false); - - var context = - new CommandContext(new CreateApp { AppId = appId, Name = "my-app" }, commandBus) - .Complete(); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => index.ReserveAppAsync(appId, "my-app")) - .MustHaveHappened(); - - A.CallTo(() => index.AddAppAsync(appId, "my-app")) - .MustNotHaveHappened(); - - A.CallTo(() => index.RemoveReservationAsync(appId, "my-app")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_remove_app_from_index_on_archive() - { - var context = - new CommandContext(new ArchiveApp { AppId = appId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveAppAsync(appId)) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs deleted file mode 100644 index 2275ea963..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public class AppsByNameIndexGrainTests - { - private readonly IGrainState grainState = A.Fake>(); - private readonly NamedId appId1 = NamedId.Of(Guid.NewGuid(), "my-app1"); - private readonly NamedId appId2 = NamedId.Of(Guid.NewGuid(), "my-app2"); - private readonly AppsByNameIndexGrain sut; - - public AppsByNameIndexGrainTests() - { - sut = new AppsByNameIndexGrain(grainState); - sut.ActivateAsync(SingleGrain.Id).Wait(); - } - - [Fact] - public async Task Should_add_app_id_to_index() - { - await sut.AddAppAsync(appId1.Id, appId1.Name); - - var result = await sut.GetAppIdAsync(appId1.Name); - - Assert.Equal(appId1.Id, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_be_able_to_reserve_index_if_name_taken() - { - await sut.AddAppAsync(appId2.Id, appId1.Name); - - Assert.False(await sut.ReserveAppAsync(appId1.Id, appId1.Name)); - } - - [Fact] - public async Task Should_not_be_able_to_reserve_if_name_reserved() - { - await sut.ReserveAppAsync(appId2.Id, appId1.Name); - - Assert.False(await sut.ReserveAppAsync(appId1.Id, appId1.Name)); - } - - [Fact] - public async Task Should_not_be_able_to_reserve_if_id_taken() - { - await sut.AddAppAsync(appId1.Id, appId1.Name); - - Assert.False(await sut.ReserveAppAsync(appId1.Id, appId2.Name)); - } - - [Fact] - public async Task Should_not_be_able_to_reserve_if_id_reserved() - { - await sut.ReserveAppAsync(appId1.Id, appId1.Name); - - Assert.False(await sut.ReserveAppAsync(appId1.Id, appId2.Name)); - } - - [Fact] - public async Task Should_be_able_to_reserve_if_id_and_name_not_reserved() - { - await sut.ReserveAppAsync(appId1.Id, appId1.Name); - - Assert.True(await sut.ReserveAppAsync(appId2.Id, appId2.Name)); - } - - [Fact] - public async Task Should_be_able_to_reserve_after_app_removed() - { - await sut.AddAppAsync(appId1.Id, appId1.Name); - await sut.RemoveAppAsync(appId1.Id); - - Assert.True(await sut.ReserveAppAsync(appId1.Id, appId1.Name)); - } - - [Fact] - public async Task Should_be_able_to_reserve_after_reservation_removed() - { - await sut.ReserveAppAsync(appId1.Id, appId1.Name); - await sut.RemoveReservationAsync(appId1.Id, appId1.Name); - - Assert.True(await sut.ReserveAppAsync(appId1.Id, appId1.Name)); - } - - [Fact] - public async Task Should_return_many_app_ids() - { - await sut.AddAppAsync(appId1.Id, appId1.Name); - await sut.AddAppAsync(appId2.Id, appId2.Name); - - var ids = await sut.GetAppIdsAsync(appId1.Name, appId2.Name); - - Assert.Equal(new List { appId1.Id, appId2.Id }, ids); - } - - [Fact] - public async Task Should_remove_app_id_from_index() - { - await sut.AddAppAsync(appId1.Id, appId1.Name); - await sut.RemoveAppAsync(appId1.Id); - - var result = await sut.GetAppIdAsync(appId1.Name); - - Assert.Equal(Guid.Empty, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_replace_app_ids_on_rebuild() - { - var state = new Dictionary - { - [appId1.Name] = appId1.Id, - [appId2.Name] = appId2.Id - }; - - await sut.RebuildAsync(state); - - Assert.Equal(appId1.Id, await sut.GetAppIdAsync(appId1.Name)); - Assert.Equal(appId2.Id, await sut.GetAppIdAsync(appId2.Name)); - - Assert.Equal(new List { appId1.Id, appId2.Id }, await sut.GetAppIdsAsync()); - - Assert.Equal(2, await sut.CountAsync()); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs deleted file mode 100644 index ab1b64e64..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public class AppsByUserIndexCommandMiddlewareTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly IAppsByUserIndex index = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly string userId = "123"; - private readonly AppsByUserIndexCommandMiddleware sut; - - public AppsByUserIndexCommandMiddlewareTests() - { - A.CallTo(() => grainFactory.GetGrain(userId, null)) - .Returns(index); - - sut = new AppsByUserIndexCommandMiddleware(grainFactory); - } - - [Fact] - public async Task Should_add_app_to_index_on_create() - { - var context = - new CommandContext(new CreateApp { AppId = appId.Id, Actor = new RefToken("user", userId) }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddAppAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_add_app_to_index_on_assign_of_contributor() - { - var context = - new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddAppAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_add_app_to_index_on_remove_of_contributor() - { - var context = - new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveAppAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_app_from_index_on_archive() - { - var appGrain = A.Fake(); - var appState = Mocks.App(appId); - - A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) - .Returns(appGrain); - - A.CallTo(() => appGrain.GetStateAsync()) - .Returns(J.AsTask(appState)); - - A.CallTo(() => appState.Contributors) - .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); - - var context = - new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveAppAsync(appId.Id)) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs deleted file mode 100644 index 401e17614..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ==========================================================================.WriteAsync() -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public class AppsByUserIndexGrainTests - { - private readonly IGrainState grainState = A.Fake>(); - private readonly Guid appId1 = Guid.NewGuid(); - private readonly Guid appId2 = Guid.NewGuid(); - private readonly string userId = "user"; - private readonly AppsByUserIndexGrain sut; - - public AppsByUserIndexGrainTests() - { - sut = new AppsByUserIndexGrain(grainState); - sut.ActivateAsync(userId).Wait(); - } - - [Fact] - public async Task Should_add_app_id_to_index() - { - await sut.AddAppAsync(appId1); - await sut.AddAppAsync(appId2); - - var result = await sut.GetAppIdsAsync(); - - Assert.Equal(new List { appId1, appId2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_remove_app_id_from_index() - { - await sut.AddAppAsync(appId1); - await sut.AddAppAsync(appId2); - await sut.RemoveAppAsync(appId1); - - var result = await sut.GetAppIdsAsync(); - - Assert.Equal(new List { appId2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceOrMore(); - } - - [Fact] - public async Task Should_replace_app_ids_on_rebuild() - { - var state = HashSet.Of(appId1, appId2); - - await sut.RebuildAsync(state); - - var result = await sut.GetAppIdsAsync(); - - Assert.Equal(new List { appId1, appId2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs new file mode 100644 index 000000000..4eb06a70e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -0,0 +1,387 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAppsByNameIndexGrain indexByName = A.Fake(); + private readonly IAppsByUserIndexGrain indexByUser = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly string userId = "user-1"; + private readonly AppsIndex sut; + + public AppsIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(indexByName); + + A.CallTo(() => grainFactory.GetGrain(userId, null)) + .Returns(indexByUser); + + sut = new AppsIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_all_apps_from_user_permissions() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new string[] { appId.Name }))) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_all_apps_from_user() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByUser.GetIdsAsync()) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_all_apps() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdsAsync()) + .Returns(new List { appId.Id }); + + var actual = await sut.GetAppsAsync(); + + Assert.Same(expected, actual[0]); + } + + [Fact] + public async Task Should_resolve_app_by_name() + { + var expected = SetupApp(0, false); + + A.CallTo(() => indexByName.GetIdAsync(appId.Name)) + .Returns(appId.Id); + + var actual = await sut.GetAppAsync(appId.Name); + + Assert.Same(expected, actual); + } + + [Fact] + public async Task Should_resolve_app_by_id() + { + var expected = SetupApp(0, false); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Same(expected, actual); + } + + [Fact] + public async Task Should_return_null_if_app_archived() + { + SetupApp(0, true); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_return_null_if_app_not_created() + { + SetupApp(-1, false); + + var actual = await sut.GetAppAsync(appId.Id); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_add_app_to_indexes_on_create() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(appId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_app_to_user_index_if_app_created_by_client() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(CreateFromClient(appId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_clear_reservation_when_app_creation_failed() + { + var token = RandomHash.Simple(); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); + + var context = + new CommandContext(CreateFromClient(appId.Name), commandBus); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.AddAsync(token)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_indexes_on_create_if_name_taken() + { + A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + .Returns(Task.FromResult(null)); + + var context = + new CommandContext(Create(appId.Name), commandBus) + .Complete(); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => indexByName.AddAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_indexes_on_create_if_name_invalid() + { + var context = + new CommandContext(Create("INVALID"), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.ReserveAsync(appId.Id, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByName.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_app_to_index_on_contributor_assignment() + { + var context = + new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_from_user_index_on_remove_of_contributor() + { + var context = + new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_app_from_indexes_on_archive() + { + var app = SetupApp(0, false); + + var context = + new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => indexByName.RemoveAsync(appId.Id)) + .MustHaveHappened(); + + A.CallTo(() => indexByUser.RemoveAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding_for_contributors1() + { + var apps = new HashSet(); + + await sut.RebuildByContributorsAsync(userId, apps); + + A.CallTo(() => indexByUser.RebuildAsync(apps)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding_for_contributors2() + { + var users = new HashSet { userId }; + + await sut.RebuildByContributorsAsync(appId.Id, users); + + A.CallTo(() => indexByUser.AddAsync(appId.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var apps = new Dictionary(); + + await sut.RebuildAsync(apps); + + A.CallTo(() => indexByName.RebuildAsync(apps)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_reserveration() + { + await sut.AddAsync("token"); + + A.CallTo(() => indexByName.AddAsync("token")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_remove_reservation() + { + await sut.RemoveReservationAsync("token"); + + A.CallTo(() => indexByName.RemoveReservationAsync("token")) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_request_for_ids() + { + await sut.GetIdsAsync(); + + A.CallTo(() => indexByName.GetIdsAsync()) + .MustHaveHappened(); + } + + private IAppEntity SetupApp(long version, bool archived) + { + var appEntity = A.Fake(); + + A.CallTo(() => appEntity.Name) + .Returns(appId.Name); + A.CallTo(() => appEntity.Version) + .Returns(version); + A.CallTo(() => appEntity.IsArchived) + .Returns(archived); + A.CallTo(() => appEntity.Contributors) + .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); + + var appGrain = A.Fake(); + + A.CallTo(() => appGrain.GetStateAsync()) + .Returns(J.Of(appEntity)); + + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(appGrain); + + return appEntity; + } + + private CreateApp Create(string name) + { + return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorSubject() }; + } + + private CreateApp CreateFromClient(string name) + { + return new CreateApp { AppId = appId.Id, Name = name, Actor = ActorClient() }; + } + + private RefToken ActorSubject() + { + return new RefToken(RefTokenType.Subject, userId); + } + + private RefToken ActorClient() + { + return new RefToken(RefTokenType.Client, userId); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index fc4711887..bcb311f9a 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -18,7 +17,6 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; using Xunit; #pragma warning disable SA1401 // Fields must be private @@ -28,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public class AssetChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAssetLoader assetLoader = A.Fake(); private readonly IRuleTriggerHandler sut; public AssetChangedTriggerHandlerTests() @@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) .Returns(false); - sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); + sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); } public static IEnumerable TestEvents = new[] @@ -56,13 +54,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - var assetGrain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(@event.AssetId, null)) - .Returns(assetGrain); - - A.CallTo(() => assetGrain.GetStateAsync(12)) - .Returns(J.Of(new AssetEntity())); + A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12)) + .Returns(new AssetEntity()); var result = await sut.CreateEnrichedEventAsync(envelope); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs new file mode 100644 index 000000000..373c252ed --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Queries +{ + public class AssetLoaderTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IAssetGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly AssetLoader sut; + + public AssetLoaderTests() + { + A.CallTo(() => grainFactory.GetGrain(id, null)) + .Returns(grain); + + sut = new AssetLoader(grainFactory); + } + + [Fact] + public async Task Should_throw_exception_if_no_state_returned() + { + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(null)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_throw_exception_if_state_has_other_version() + { + var content = new AssetEntity { Version = 5 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); + } + + [Fact] + public async Task Should_return_content_from_state() + { + var content = new AssetEntity { Version = 10 }; + + A.CallTo(() => grain.GetStateAsync(10)) + .Returns(J.Of(content)); + + var result = await sut.GetAsync(id, 10); + + Assert.Same(content, result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index b943249a8..980a44197 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading.Tasks; using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; @@ -21,7 +20,6 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; using Xunit; #pragma warning disable SA1401 // Fields must be private @@ -32,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public class ContentChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IContentLoader contentLoader = A.Fake(); private readonly IRuleTriggerHandler sut; private readonly Guid ruleId = Guid.NewGuid(); private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); @@ -46,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) .Returns(false); - sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory); + sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); } public static IEnumerable TestEvents = new[] @@ -65,13 +63,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - var contentGrain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(@event.ContentId, null)) - .Returns(contentGrain); - - A.CallTo(() => contentGrain.GetStateAsync(12)) - .Returns(J.Of(new ContentEntity { SchemaId = SchemaMatch })); + A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) + .Returns(new ContentEntity { SchemaId = SchemaMatch }); var result = await sut.CreateEnrichedEventAsync(envelope); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentVersionLoaderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs similarity index 85% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentVersionLoaderTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs index 1497bd2c7..4047f916d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentVersionLoaderTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs @@ -15,19 +15,19 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Queries { - public class ContentVersionLoaderTests + public class ContentLoaderTests { private readonly IGrainFactory grainFactory = A.Fake(); private readonly IContentGrain grain = A.Fake(); private readonly Guid id = Guid.NewGuid(); - private readonly ContentVersionLoader sut; + private readonly ContentLoader sut; - public ContentVersionLoaderTests() + public ContentLoaderTests() { A.CallTo(() => grainFactory.GetGrain(id, null)) .Returns(grain); - sut = new ContentVersionLoader(grainFactory); + sut = new ContentLoader(grainFactory); } [Fact] @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => grain.GetStateAsync(10)) .Returns(J.Of(null)); - await Assert.ThrowsAsync(() => sut.LoadAsync(id, 10)); + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); } [Fact] @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => grain.GetStateAsync(10)) .Returns(J.Of(content)); - await Assert.ThrowsAsync(() => sut.LoadAsync(id, 10)); + await Assert.ThrowsAsync(() => sut.GetAsync(id, 10)); } [Fact] @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => grain.GetStateAsync(10)) .Returns(J.Of(content)); - var result = await sut.LoadAsync(id, 10); + var result = await sut.GetAsync(id, 10); Assert.Same(content, result); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index e2d70efb0..bc89098d3 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IAssetUrlGenerator urlGenerator = A.Fake(); private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContentRepository contentRepository = A.Fake(); - private readonly IContentVersionLoader contentVersionLoader = A.Fake(); + private readonly IContentLoader contentVersionLoader = A.Fake(); private readonly ISchemaEntity schema; private readonly IScriptEngine scriptEngine = A.Fake(); private readonly Guid contentId = Guid.NewGuid(); @@ -204,7 +204,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries SetupSchemaFound(); SetupSchemaScripting(contentId); - A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10)) + A.CallTo(() => contentVersionLoader.GetAsync(contentId, 10)) .Returns(content); var ctx = requestContext; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs deleted file mode 100644 index 671c0cfab..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public class RulesByAppIndexCommandMiddlewareTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly IRulesByAppIndex index = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly RulesByAppIndexCommandMiddleware sut; - - public RulesByAppIndexCommandMiddlewareTests() - { - A.CallTo(() => grainFactory.GetGrain(appId, null)) - .Returns(index); - - sut = new RulesByAppIndexCommandMiddleware(grainFactory); - } - - [Fact] - public async Task Should_add_rule_to_index_on_create() - { - var context = - new CommandContext(new CreateRule { RuleId = appId, AppId = BuildAppId() }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddRuleAsync(appId)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_rule_from_index_on_delete() - { - var ruleGrain = A.Fake(); - var ruleState = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(appId, null)) - .Returns(ruleGrain); - - A.CallTo(() => ruleGrain.GetStateAsync()) - .Returns(J.AsTask(ruleState)); - - A.CallTo(() => ruleState.AppId) - .Returns(BuildAppId()); - - var context = - new CommandContext(new DeleteRule { RuleId = appId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveRuleAsync(appId)) - .MustHaveHappened(); - } - - private NamedId BuildAppId() - { - return NamedId.Of(appId, "my-app"); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs deleted file mode 100644 index 279bdb976..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public class RulesByAppIndexGrainTests - { - private readonly IGrainState grainState = A.Fake>(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Guid ruleId1 = Guid.NewGuid(); - private readonly Guid ruleId2 = Guid.NewGuid(); - private readonly RulesByAppIndexGrain sut; - - public RulesByAppIndexGrainTests() - { - A.CallTo(() => grainState.ClearAsync()) - .Invokes(() => grainState.Value = new RulesByAppIndexGrain.GrainState()); - - sut = new RulesByAppIndexGrain(grainState); - sut.ActivateAsync(appId).Wait(); - } - - [Fact] - public async Task Should_add_rule_id_to_index() - { - await sut.AddRuleAsync(ruleId1); - await sut.AddRuleAsync(ruleId2); - - var result = await sut.GetRuleIdsAsync(); - - Assert.Equal(new List { ruleId1, ruleId2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_delete_and_reset_state_when_cleaning() - { - await sut.AddRuleAsync(ruleId1); - await sut.AddRuleAsync(ruleId2); - await sut.ClearAsync(); - - var ids = await sut.GetRuleIdsAsync(); - - Assert.Empty(ids); - - A.CallTo(() => grainState.ClearAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_rule_id_from_index() - { - await sut.AddRuleAsync(ruleId1); - await sut.AddRuleAsync(ruleId2); - await sut.RemoveRuleAsync(ruleId1); - - var result = await sut.GetRuleIdsAsync(); - - Assert.Equal(new List { ruleId2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceOrMore(); - } - - [Fact] - public async Task Should_replace_rule_ids_on_rebuild() - { - var state = new HashSet - { - ruleId1, - ruleId2 - }; - - await sut.RebuildAsync(state); - - var result = await sut.GetRuleIdsAsync(); - - Assert.Equal(new List { ruleId1, ruleId2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs new file mode 100644 index 000000000..f547607cc --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs @@ -0,0 +1,143 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public class RulesIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly IRulesByAppIndexGrain index = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly RulesIndex sut; + + public RulesIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(index); + + sut = new RulesIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_rules_by_id() + { + var rule = SetupRule(0, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { rule.Id }); + + var actual = await sut.GetRulesAsync(appId.Id); + + Assert.Same(actual[0], rule); + } + + [Fact] + public async Task Should_return_empty_rule_if_rule_not_created() + { + var rule = SetupRule(-1, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { rule.Id }); + + var actual = await sut.GetRulesAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_return_empty_rule_if_rule_deleted() + { + var rule = SetupRule(-1, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { rule.Id }); + + var actual = await sut.GetRulesAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_add_rule_to_index_on_create() + { + var ruleId = Guid.NewGuid(); + + var context = + new CommandContext(new CreateRule { RuleId = ruleId, AppId = appId }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(ruleId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_rule_from_index_on_delete() + { + var rule = SetupRule(0, false); + + var context = + new CommandContext(new DeleteRule { RuleId = rule.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAsync(rule.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var rules = new HashSet(); + + await sut.RebuildAsync(appId.Id, rules); + + A.CallTo(() => index.RebuildAsync(rules)) + .MustHaveHappened(); + } + + private IRuleEntity SetupRule(long version, bool deleted) + { + var ruleEntity = A.Fake(); + + var ruleId = Guid.NewGuid(); + + A.CallTo(() => ruleEntity.Id) + .Returns(ruleId); + A.CallTo(() => ruleEntity.AppId) + .Returns(appId); + A.CallTo(() => ruleEntity.Version) + .Returns(version); + A.CallTo(() => ruleEntity.IsDeleted) + .Returns(deleted); + + var ruleGrain = A.Fake(); + + A.CallTo(() => ruleGrain.GetStateAsync()) + .Returns(J.Of(ruleEntity)); + + A.CallTo(() => grainFactory.GetGrain(ruleId, null)) + .Returns(ruleGrain); + + return ruleEntity; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs index a2530581f..557737a10 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; @@ -23,7 +21,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { public class GuardSchemaTests { - private readonly IAppProvider appProvider = A.Fake(); private readonly Schema schema_0; private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); @@ -33,34 +30,19 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards new Schema("my-schema") .AddString(1, "field1", Partitioning.Invariant) .AddString(2, "field2", Partitioning.Invariant); - - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored)) - .Returns(Task.FromResult(null)); - - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, "existing")) - .Returns(A.Dummy()); } [Fact] - public async Task CanCreate_should_throw_exception_if_name_not_valid() + public void CanCreate_should_throw_exception_if_name_not_valid() { var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Name is not a valid slug.", "Name")); } [Fact] - public async Task CanCreate_should_throw_exception_if_name_already_in_use() - { - var command = new CreateSchema { AppId = appId, Name = "existing" }; - - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), - new ValidationError("A schema with the same name already exists.")); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_field_name_invalid() + public void CanCreate_should_throw_exception_if_field_name_invalid() { var command = new CreateSchema { @@ -77,13 +59,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Field name must be a valid javascript property name.", "Fields[1].Name")); } [Fact] - public async Task CanCreate_should_throw_exception_if_field_properties_null() + public void CanCreate_should_throw_exception_if_field_properties_null() { var command = new CreateSchema { @@ -100,13 +82,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Field properties is required.", "Fields[1].Properties")); } [Fact] - public async Task CanCreate_should_throw_exception_if_field_properties_not_valid() + public void CanCreate_should_throw_exception_if_field_properties_not_valid() { var command = new CreateSchema { @@ -123,14 +105,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Max length must be greater or equal to min length.", "Fields[1].Properties.MinLength", "Fields[1].Properties.MaxLength")); } [Fact] - public async Task CanCreate_should_throw_exception_if_field_partitioning_not_valid() + public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() { var command = new CreateSchema { @@ -147,13 +129,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Partitioning is not a valid value.", "Fields[1].Partitioning")); } [Fact] - public async Task CanCreate_should_throw_exception_if_fields_contains_duplicate_name() + public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() { var command = new CreateSchema { @@ -176,13 +158,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Fields cannot have duplicate names.", "Fields")); } [Fact] - public async Task CanCreate_should_throw_exception_if_nested_field_name_invalid() + public void CanCreate_should_throw_exception_if_nested_field_name_invalid() { var command = new CreateSchema { @@ -207,13 +189,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Field name must be a valid javascript property name.", "Fields[1].Nested[1].Name")); } [Fact] - public async Task CanCreate_should_throw_exception_if_nested_field_properties_null() + public void CanCreate_should_throw_exception_if_nested_field_properties_null() { var command = new CreateSchema { @@ -238,13 +220,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Field properties is required.", "Fields[1].Nested[1].Properties")); } [Fact] - public async Task CanCreate_should_throw_exception_if_nested_field_is_array() + public void CanCreate_should_throw_exception_if_nested_field_is_array() { var command = new CreateSchema { @@ -269,13 +251,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Nested field cannot be array fields.", "Fields[1].Nested[1].Properties")); } [Fact] - public async Task CanCreate_should_throw_exception_if_nested_field_properties_not_valid() + public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() { var command = new CreateSchema { @@ -300,14 +282,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Max length must be greater or equal to min length.", "Fields[1].Nested[1].Properties.MinLength", "Fields[1].Nested[1].Properties.MaxLength")); } [Fact] - public async Task CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() + public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() { var command = new CreateSchema { @@ -337,13 +319,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("Fields cannot have duplicate names.", "Fields[1].Nested")); } [Fact] - public async Task CanCreate_should_throw_exception_if_ui_field_is_invalid() + public void CanCreate_should_throw_exception_if_ui_field_is_invalid() { var command = new CreateSchema { @@ -366,7 +348,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await ValidationAssert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider), + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), new ValidationError("UI field cannot be a list field.", "Fields[1].Properties.IsListField"), new ValidationError("UI field cannot be a reference field.", @@ -378,7 +360,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards } [Fact] - public async Task CanCreate_should_not_throw_exception_if_command_is_valid() + public void CanCreate_should_not_throw_exception_if_command_is_valid() { var command = new CreateSchema { @@ -425,7 +407,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "new-schema" }; - await GuardSchema.CanCreate(command, appProvider); + GuardSchema.CanCreate(command); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs deleted file mode 100644 index 58045e36a..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public class SchemasByAppIndexCommandMiddlewareTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly ISchemasByAppIndex index = A.Fake(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly SchemasByAppIndexCommandMiddleware sut; - - public SchemasByAppIndexCommandMiddlewareTests() - { - A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) - .Returns(index); - - sut = new SchemasByAppIndexCommandMiddleware(grainFactory); - } - - [Fact] - public async Task Should_add_schema_to_index_on_create() - { - var context = - new CommandContext(new CreateSchema { SchemaId = schemaId.Id, Name = schemaId.Name, AppId = appId }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.AddSchemaAsync(schemaId.Id, schemaId.Name)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_schema_from_index_on_delete() - { - var schemaGrain = A.Fake(); - var schemaState = Mocks.Schema(appId, schemaId); - - A.CallTo(() => grainFactory.GetGrain(schemaId.Id, null)) - .Returns(schemaGrain); - - A.CallTo(() => schemaGrain.GetStateAsync()) - .Returns(J.AsTask(schemaState)); - - var context = - new CommandContext(new DeleteSchema { SchemaId = schemaId.Id }, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.RemoveSchemaAsync(schemaId.Id)) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs deleted file mode 100644 index 27ee3fefb..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public class SchemasByAppIndexGrainTests - { - private readonly IGrainState grainState = A.Fake>(); - private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId1 = NamedId.Of(Guid.NewGuid(), "my-schema1"); - private readonly NamedId schemaId2 = NamedId.Of(Guid.NewGuid(), "my-schema2"); - private readonly SchemasByAppIndexGrain sut; - - public SchemasByAppIndexGrainTests() - { - A.CallTo(() => grainState.ClearAsync()) - .Invokes(() => grainState.Value = new SchemasByAppIndexGrain.GrainState()); - - sut = new SchemasByAppIndexGrain(grainState); - sut.ActivateAsync(appId.Id).Wait(); - } - - [Fact] - public async Task Should_add_schema_id_to_index() - { - await sut.AddSchemaAsync(schemaId1.Id, schemaId1.Name); - - var result = await sut.GetSchemaIdAsync(schemaId1.Name); - - Assert.Equal(schemaId1.Id, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_delete_and_reset_state_when_cleaning() - { - await sut.AddSchemaAsync(schemaId1.Id, schemaId1.Name); - await sut.ClearAsync(); - - var id = await sut.GetSchemaIdAsync(schemaId1.Name); - - Assert.Equal(id, Guid.Empty); - - A.CallTo(() => grainState.ClearAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_schema_id_from_index() - { - await sut.AddSchemaAsync(schemaId1.Id, schemaId1.Name); - await sut.RemoveSchemaAsync(schemaId1.Id); - - var result = await sut.GetSchemaIdAsync(schemaId1.Name); - - Assert.Equal(Guid.Empty, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_replace_schema_ids_on_rebuild() - { - var state = new Dictionary - { - [schemaId1.Name] = schemaId1.Id, - [schemaId2.Name] = schemaId2.Id - }; - - await sut.RebuildAsync(state); - - Assert.Equal(schemaId1.Id, await sut.GetSchemaIdAsync(schemaId1.Name)); - Assert.Equal(schemaId2.Id, await sut.GetSchemaIdAsync(schemaId2.Name)); - - Assert.Equal(new List { schemaId1.Id, schemaId2.Id }, await sut.GetSchemaIdsAsync()); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs new file mode 100644 index 000000000..7d171fe9e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -0,0 +1,248 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public class SchemasIndexTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly ISchemasByAppIndexGrain index = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly SchemasIndex sut; + + public SchemasIndexTests() + { + A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) + .Returns(index); + + sut = new SchemasIndex(grainFactory); + } + + [Fact] + public async Task Should_resolve_schema_by_id() + { + var schema = SetupSchema(0, false); + + var actual = await sut.GetSchemaAsync(appId.Id, schema.Id); + + Assert.Same(actual, schema); + } + + [Fact] + public async Task Should_resolve_schema_by_name() + { + var schema = SetupSchema(0, false); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemaAsync(appId.Id, schema.SchemaDef.Name); + + Assert.Same(actual, schema); + } + + [Fact] + public async Task Should_resolve_schemas_by_id() + { + var schema = SetupSchema(0, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { schema.Id }); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Same(actual[0], schema); + } + + [Fact] + public async Task Should_return_empty_schema_if_schema_not_created() + { + var schema = SetupSchema(-1, false); + + A.CallTo(() => index.GetIdsAsync()) + .Returns(new List { schema.Id }); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_return_empty_schema_if_schema_deleted() + { + var schema = SetupSchema(0, true); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemasAsync(appId.Id); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_also_return_schema_if_deleted_allowed() + { + var schema = SetupSchema(-1, true); + + A.CallTo(() => index.GetIdAsync(schema.SchemaDef.Name)) + .Returns(schema.Id); + + var actual = await sut.GetSchemasAsync(appId.Id, true); + + Assert.Empty(actual); + } + + [Fact] + public async Task Should_add_schema_to_index_on_create() + { + var token = RandomHash.Simple(); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(schemaId.Name), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(token)) + .MustHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_clear_reservation_when_schema_creation_failed() + { + var token = RandomHash.Simple(); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(token); + + var context = + new CommandContext(Create(schemaId.Name), commandBus); + + await sut.HandleAsync(context); + + A.CallTo(() => index.AddAsync(token)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(token)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_index_on_create_if_name_taken() + { + A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(Task.FromResult(null)); + + var context = + new CommandContext(Create(schemaId.Name), commandBus) + .Complete(); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => index.AddAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_add_to_index_on_create_if_name_invalid() + { + var context = + new CommandContext(Create("INVALID"), commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.ReserveAsync(schemaId.Id, A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => index.RemoveReservationAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_remove_schema_from_index_on_delete() + { + var schema = SetupSchema(0, false); + + var context = + new CommandContext(new DeleteSchema { SchemaId = schema.Id }, commandBus) + .Complete(); + + await sut.HandleAsync(context); + + A.CallTo(() => index.RemoveAsync(schema.Id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_call_when_rebuilding() + { + var schemas = new Dictionary(); + + await sut.RebuildAsync(appId.Id, schemas); + + A.CallTo(() => index.RebuildAsync(schemas)) + .MustHaveHappened(); + } + + private CreateSchema Create(string name) + { + return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; + } + + private ISchemaEntity SetupSchema(long version, bool deleted) + { + var schemaEntity = A.Fake(); + + A.CallTo(() => schemaEntity.SchemaDef) + .Returns(new Schema(schemaId.Name)); + A.CallTo(() => schemaEntity.Id) + .Returns(schemaId.Id); + A.CallTo(() => schemaEntity.AppId) + .Returns(appId); + A.CallTo(() => schemaEntity.Version) + .Returns(version); + A.CallTo(() => schemaEntity.IsDeleted) + .Returns(deleted); + + var schemaGrain = A.Fake(); + + A.CallTo(() => schemaGrain.GetStateAsync()) + .Returns(J.Of(schemaEntity)); + + A.CallTo(() => grainFactory.GetGrain(schemaId.Id, null)) + .Returns(schemaGrain); + + return schemaEntity; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs index e9f12b52b..7d7acabc0 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs @@ -25,7 +25,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { public class SchemaGrainTests : HandlerTestBase { - private readonly IAppProvider appProvider = A.Fake(); private readonly string fieldName = "age"; private readonly string arrayName = "array"; private readonly NamedId fieldId = NamedId.Of(1L, "age"); @@ -40,10 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas public SchemaGrainTests() { - A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName)) - .Returns((ISchemaEntity)null); - - sut = new SchemaGrain(Store, A.Dummy(), appProvider, TestUtils.DefaultSerializer); + sut = new SchemaGrain(Store, A.Dummy(), TestUtils.DefaultSerializer); sut.ActivateAsync(Id).Wait(); } diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs new file mode 100644 index 000000000..e97512322 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class IdsIndexGrainTests + { + private readonly IGrainState> grainState = A.Fake>>(); + private readonly Guid id1 = Guid.NewGuid(); + private readonly Guid id2 = Guid.NewGuid(); + private readonly IdsIndexGrain, Guid> sut; + + public IdsIndexGrainTests() + { + A.CallTo(() => grainState.ClearAsync()) + .Invokes(() => grainState.Value = new IdsIndexState()); + + sut = new IdsIndexGrain, Guid>(grainState); + } + + [Fact] + public async Task Should_add_id_to_index() + { + await sut.AddAsync(id1); + await sut.AddAsync(id2); + + var result = await sut.GetIdsAsync(); + + Assert.Equal(new List { id1, id2 }, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_provide_number_of_entries() + { + await sut.AddAsync(id1); + await sut.AddAsync(id2); + + var count = await sut.CountAsync(); + + Assert.Equal(2, count); + } + + [Fact] + public async Task Should_clear_all_entries() + { + await sut.AddAsync(id1); + await sut.AddAsync(id2); + + await sut.ClearAsync(); + + var count = await sut.CountAsync(); + + Assert.Equal(0, count); + } + + [Fact] + public async Task Should_remove_id_from_index() + { + await sut.AddAsync(id1); + await sut.AddAsync(id2); + await sut.RemoveAsync(id1); + + var result = await sut.GetIdsAsync(); + + Assert.Equal(new List { id2 }, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappenedTwiceOrMore(); + } + + [Fact] + public async Task Should_replace__ids_on_rebuild() + { + var state = new HashSet + { + id1, + id2 + }; + + await sut.RebuildAsync(state); + + var result = await sut.GetIdsAsync(); + + Assert.Equal(new List { id1, id2 }, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs new file mode 100644 index 000000000..dc1e277a0 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs @@ -0,0 +1,197 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameIndexGrainTests + { + private readonly IGrainState> grainState = A.Fake>>(); + private readonly NamedId id1 = NamedId.Of(Guid.NewGuid(), "my-name1"); + private readonly NamedId id2 = NamedId.Of(Guid.NewGuid(), "my-name2"); + private readonly UniqueNameIndexGrain, Guid> sut; + + public UniqueNameIndexGrainTests() + { + A.CallTo(() => grainState.ClearAsync()) + .Invokes(() => grainState.Value = new UniqueNameIndexState()); + + sut = new UniqueNameIndexGrain, Guid>(grainState); + } + + [Fact] + public async Task Should_not_write_to_state_for_reservation() + { + await sut.ReserveAsync(id1.Id, id1.Name); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_to_index_if_reservation_token_acquired() + { + await AddAsync(id1); + + var result = await sut.GetIdAsync(id1.Name); + + Assert.Equal(id1.Id, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_make_reservation_if_name_already_reserved() + { + await sut.ReserveAsync(id1.Id, id1.Name); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.Null(newToken); + } + + [Fact] + public async Task Should_not_make_reservation_if_name_taken() + { + await AddAsync(id1); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.Null(newToken); + } + + [Fact] + public async Task Should_provide_number_of_entries() + { + await AddAsync(id1); + await AddAsync(id2); + + var count = await sut.CountAsync(); + + Assert.Equal(2, count); + } + + [Fact] + public async Task Should_clear_all_entries() + { + await AddAsync(id1); + await AddAsync(id2); + + await sut.ClearAsync(); + + var count = await sut.CountAsync(); + + Assert.Equal(0, count); + } + + [Fact] + public async Task Should_make_reservation_after_reservation_removed() + { + var token = await sut.ReserveAsync(id1.Id, id1.Name); + + await sut.RemoveReservationAsync(token); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.NotNull(newToken); + } + + [Fact] + public async Task Should_make_reservation_after_id_removed() + { + await AddAsync(id1); + + await sut.RemoveAsync(id1.Id); + + var newToken = await sut.ReserveAsync(id1.Id, id1.Name); + + Assert.NotNull(newToken); + } + + [Fact] + public async Task Should_remove_id_from_index() + { + await AddAsync(id1); + + await sut.RemoveAsync(id1.Id); + + var result = await sut.GetIdAsync(id1.Name); + + Assert.Equal(Guid.Empty, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_not_write_to_state_if_nothing_removed() + { + await sut.RemoveAsync(id1.Id); + + A.CallTo(() => grainState.WriteAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_ignore_error_if_removing_reservation_with_Invalid_token() + { + await sut.RemoveReservationAsync(null); + } + + [Fact] + public async Task Should_ignore_error_if_completing_reservation_with_Invalid_token() + { + await sut.AddAsync(null); + } + + [Fact] + public async Task Should_replace_ids_on_rebuild() + { + var state = new Dictionary + { + [id1.Name] = id1.Id, + [id2.Name] = id2.Id + }; + + await sut.RebuildAsync(state); + + Assert.Equal(id1.Id, await sut.GetIdAsync(id1.Name)); + Assert.Equal(id2.Id, await sut.GetIdAsync(id2.Name)); + + var result = await sut.GetIdsAsync(); + + Assert.Equal(new List { id1.Id, id2.Id }, result); + + A.CallTo(() => grainState.WriteAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_provide_multiple_ids_by_names() + { + await AddAsync(id1); + await AddAsync(id2); + + var result = await sut.GetIdsAsync(new string[] { id1.Name, id2.Name, "not-found" }); + + Assert.Equal(new List { id1.Id, id2.Id }, result); + } + + private async Task AddAsync(NamedId id) + { + var token = await sut.ReserveAsync(id.Id, id.Name); + + await sut.AddAsync(token); + } + } +} diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs index 13d0b16fc..6070a13a9 100644 --- a/tools/Migrate_01/MigrationPath.cs +++ b/tools/Migrate_01/MigrationPath.cs @@ -17,7 +17,7 @@ namespace Migrate_01 { public sealed class MigrationPath : IMigrationPath { - private const int CurrentVersion = 18; + private const int CurrentVersion = 19; private readonly IServiceProvider serviceProvider; public MigrationPath(IServiceProvider serviceProvider) @@ -69,6 +69,11 @@ namespace Migrate_01 if (version < 9) { yield return serviceProvider.GetService(); + } + + // Version 19: Unify indexes. + if (version < 19) + { yield return serviceProvider.GetRequiredService(); } diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs index 14dd415ee..4509e0956 100644 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -7,53 +7,51 @@ using System; 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.Commands; using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Orleans; namespace Migrate_01.Migrations { public sealed class AddPatterns : IMigration { private readonly InitialPatterns initialPatterns; - private readonly IGrainFactory grainFactory; + private readonly ICommandBus commandBus; + private readonly IAppsIndex indexForApps; - public AddPatterns(InitialPatterns initialPatterns, IGrainFactory grainFactory) + public AddPatterns(InitialPatterns initialPatterns, ICommandBus commandBus, IAppsIndex indexForApps) { + this.indexForApps = indexForApps; this.initialPatterns = initialPatterns; - - this.grainFactory = grainFactory; + this.commandBus = commandBus; } public async Task UpdateAsync() { - var ids = await grainFactory.GetGrain(SingleGrain.Id).GetAppIdsAsync(); + var ids = await indexForApps.GetIdsAsync(); foreach (var id in ids) { - var app = grainFactory.GetGrain(id); - - var state = await app.GetStateAsync(); + var app = await indexForApps.GetAppAsync(id); - if (state.Value.Patterns.Count == 0) + if (app.Patterns.Count == 0) { foreach (var pattern in initialPatterns.Values) { var command = new AddPattern { - Actor = state.Value.CreatedBy, - AppId = state.Value.Id, + Actor = app.CreatedBy, + AppId = id, Name = pattern.Name, PatternId = Guid.NewGuid(), Pattern = pattern.Pattern, Message = pattern.Message }; - await app.ExecuteAsync(command); + await commandBus.PublishAsync(command); } } } diff --git a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs index 98f2f0f53..37aa3cbbc 100644 --- a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs +++ b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs @@ -8,7 +8,6 @@ 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; @@ -17,7 +16,6 @@ using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.State; using Squidex.Infrastructure; using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; @@ -25,18 +23,24 @@ namespace Migrate_01.Migrations { public class PopulateGrainIndexes : IMigration { - private readonly IGrainFactory grainFactory; + private readonly IAppsIndex indexApps; + private readonly IRulesIndex indexRules; + private readonly ISchemasIndex indexSchemas; private readonly ISnapshotStore statesForApps; private readonly ISnapshotStore statesForRules; private readonly ISnapshotStore statesForSchemas; public PopulateGrainIndexes( - IGrainFactory grainFactory, + IAppsIndex indexApps, + IRulesIndex indexRules, + ISchemasIndex indexSchemas, ISnapshotStore statesForApps, ISnapshotStore statesForRules, ISnapshotStore statesForSchemas) { - this.grainFactory = grainFactory; + this.indexApps = indexApps; + this.indexRules = indexRules; + this.indexSchemas = indexSchemas; this.statesForApps = statesForApps; this.statesForRules = statesForRules; this.statesForSchemas = statesForSchemas; @@ -70,11 +74,11 @@ namespace Migrate_01.Migrations return TaskHelper.Done; }); - await grainFactory.GetGrain(SingleGrain.Id).RebuildAsync(appsByName); + await indexApps.RebuildAsync(appsByName); foreach (var kvp in appsByUser) { - await grainFactory.GetGrain(kvp.Key).RebuildAsync(kvp.Value); + await indexApps.RebuildByContributorsAsync(kvp.Key, kvp.Value); } } @@ -94,7 +98,7 @@ namespace Migrate_01.Migrations foreach (var kvp in rulesByApp) { - await grainFactory.GetGrain(kvp.Key).RebuildAsync(kvp.Value); + await indexRules.RebuildAsync(kvp.Key, kvp.Value); } } @@ -114,7 +118,7 @@ namespace Migrate_01.Migrations foreach (var kvp in schemasByApp) { - await grainFactory.GetGrain(kvp.Key).RebuildAsync(kvp.Value); + await indexSchemas.RebuildAsync(kvp.Key, kvp.Value); } } }