Browse Source

Indexes (#413)

* Custom classes for indexes and to load assets.
pull/415/head^2
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
02b26bf312
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 197
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  2. 27
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  3. 4
      src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs
  4. 72
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs
  5. 122
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  6. 80
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
  7. 50
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs
  8. 286
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  9. 35
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs
  10. 12
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs
  11. 17
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs
  12. 39
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs
  13. 14
      src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  14. 18
      src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs
  15. 44
      src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs
  16. 14
      src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  17. 5
      src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs
  18. 8
      src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs
  19. 6
      src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  20. 4
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  21. 15
      src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs
  22. 2
      src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs
  23. 17
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs
  24. 15
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs
  25. 56
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs
  26. 55
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs
  27. 118
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs
  28. 4
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  29. 13
      src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs
  30. 9
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  31. 17
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs
  32. 17
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs
  33. 56
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs
  34. 62
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs
  35. 181
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  36. 10
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  37. 27
      src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs
  38. 35
      src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs
  39. 62
      src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs
  40. 16
      src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs
  41. 136
      src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs
  42. 16
      src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs
  43. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  44. 32
      src/Squidex/Config/Domain/EntitiesServices.cs
  45. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs
  46. 93
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs
  47. 149
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs
  48. 103
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs
  49. 76
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs
  50. 387
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs
  51. 15
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  52. 66
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs
  53. 15
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  54. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs
  55. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  56. 79
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs
  57. 97
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs
  58. 143
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs
  59. 70
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs
  60. 73
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs
  61. 96
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs
  62. 248
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs
  63. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs
  64. 103
      tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs
  65. 197
      tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs
  66. 7
      tools/Migrate_01/MigrationPath.cs
  67. 26
      tools/Migrate_01/Migrations/AddPatterns.cs
  68. 22
      tools/Migrate_01/Migrations/PopulateGrainIndexes.cs

197
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<AppProvider>())
{
var app = await grainFactory.GetGrain<IAppGrain>(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<AppProvider>())
{
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<AppProvider>())
{
var appId = await GetAppIdAsync(appName);
if (appId == Guid.Empty)
{
return null;
}
return await indexForApps.GetAppAsync(appName);
});
}
return await GetAppByIdAsync(appId);
}
public Task<List<IAppEntity>> 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<AppProvider>())
{
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<AppProvider>())
{
var schema = await grainFactory.GetGrain<ISchemaGrain>(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<AppProvider>())
{
var ids = await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdsAsync();
var schemas =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(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<AppProvider>())
{
var ids = await grainFactory.GetGrain<IRulesByAppIndex>(appId).GetRuleIdsAsync();
var rules =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync()));
return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList();
}
return await indexRules.GetRulesAsync(appId);
});
}
public Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions)
{
Guard.NotNull(userId, nameof(userId));
Guard.NotNull(permissions, nameof(permissions));
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
using (Profiler.TraceMethod<AppProvider>())
{
var ids =
await Task.WhenAll(
GetAppIdsByUserAsync(userId),
GetAppIdsAsync(permissions.ToAppNames()));
var apps =
await Task.WhenAll(ids
.SelectMany(x => x)
.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList();
}
});
}
private async Task<IAppEntity> GetAppByIdAsync(Guid appId)
{
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (!IsExisting(app))
{
return null;
}
return app.Value;
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync(IEnumerable<string> names)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdsAsync(names.ToArray());
}
}
private async Task<Guid> GetAppIdAsync(string name)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdAsync(name);
}
}
private async Task<Guid> GetSchemaIdAsync(Guid appId, string name)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdAsync(name);
}
}
private static bool IsFound(IEntityWithVersion entity)
{
return entity.Version > EtagVersion.Empty;
}
private static bool IsExisting(J<IAppEntity> app)
{
return IsFound(app.Value) && !app.Value.IsArchived;
}
private static bool IsExisting(J<ISchemaEntity> schema, bool allowDeleted)
{
return IsFound(schema.Value) && (!schema.Value.IsDeleted || allowDeleted);
}
}
}

27
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<string> contributors = new HashSet<string>();
private readonly Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private Dictionary<string, string> usersWithEmail = new Dictionary<string, string>();
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<IAppsByNameIndex>(SingleGrain.Id);
}
public override async Task BackupEventAsync(Envelope<IEvent> @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<IAppsByUserIndex>(user).AddAppAsync(appId);
}
await grainAppIndex.RebuildByContributorsAsync(appId, contributors);
}
}
}

4
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<IAppsByNameIndex>(SingleGrain.Id);
index = grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id);
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)

72
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs

@ -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<IAppsByNameIndex>(SingleGrain.Id);
}
public async Task HandleAsync(CommandContext context, Func<Task> 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);
}
}
}
}
}

122
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<AppsByNameIndexState, Guid>, IAppsByNameIndexGrain
{
private readonly HashSet<Guid> reservedIds = new HashSet<Guid>();
private readonly HashSet<string> reservedNames = new HashSet<string>();
private readonly IGrainState<GrainState> state;
[CollectionName("Index_AppsByName")]
public sealed class GrainState
{
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(StringComparer.Ordinal);
}
public AppsByNameIndexGrain(IGrainState<GrainState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task RebuildAsync(Dictionary<string, Guid> apps)
{
state.Value = new GrainState { Apps = apps };
return state.WriteAsync();
}
public Task<bool> 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<AppsByNameIndexState> 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<List<Guid>> GetAppIdsAsync(params string[] names)
{
var appIds = new List<Guid>();
foreach (var appName in names)
{
if (state.Value.Apps.TryGetValue(appName, out var appId))
{
appIds.Add(appId);
}
}
return Task.FromResult(appIds);
}
public Task<Guid> GetAppIdAsync(string appName)
{
state.Value.Apps.TryGetValue(appName, out var appId);
return Task.FromResult(appId);
}
public Task<List<Guid>> GetAppIdsAsync()
{
return Task.FromResult(state.Value.Apps.Values.ToList());
}
}
public Task<long> CountAsync()
{
return Task.FromResult((long)state.Value.Apps.Count);
}
[CollectionName("Index_AppsByName")]
public sealed class AppsByNameIndexState : UniqueNameIndexState<Guid>
{
}
}

80
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs

@ -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<Task> 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<IAppGrain>(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<IAppsByUserIndex>(id);
}
}
}

50
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<AppsByUserIndex, Guid>, IAppsByUserIndexGrain
{
private readonly IGrainState<GrainState> state;
[CollectionName("Index_AppsByUser")]
public sealed class GrainState
{
public HashSet<Guid> Apps { get; set; } = new HashSet<Guid>();
}
public AppsByUserIndexGrain(IGrainState<GrainState> state)
public AppsByUserIndexGrain(IGrainState<AppsByUserIndex> state)
: base(state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task RebuildAsync(HashSet<Guid> 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<List<Guid>> GetAppIdsAsync()
{
return Task.FromResult(state.Value.Apps.ToList());
}
[CollectionName("Index_AppsByUser")]
public sealed class AppsByUserIndex : IdsIndexState<Guid>
{
}
}

286
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<string> contributors)
{
foreach (var contributorId in contributors)
{
await Index(contributorId).AddAsync(appId);
}
}
public Task RebuildByContributorsAsync(string contributorId, HashSet<Guid> apps)
{
return Index(contributorId).RebuildAsync(apps);
}
public Task RebuildAsync(Dictionary<string, Guid> appsByName)
{
return Index().RebuildAsync(appsByName);
}
public Task RemoveReservationAsync(string token)
{
return Index().RemoveReservationAsync(token);
}
public Task<List<Guid>> GetIdsAsync()
{
return Index().GetIdsAsync();
}
public Task<bool> AddAsync(string token)
{
return Index().AddAsync(token);
}
public Task<string> ReserveAsync(Guid id, string name)
{
return Index().ReserveAsync(id, name);
}
public async Task<List<IAppEntity>> GetAppsAsync()
{
using (Profiler.TraceMethod<AppsIndex>())
{
var ids = await GetAppIdsAsync();
var apps =
await Task.WhenAll(ids
.Select(id => GetAppAsync(id)));
return apps.Where(x => x != null).ToList();
}
}
public async Task<List<IAppEntity>> GetAppsForUserAsync(string userId, PermissionSet permissions)
{
using (Profiler.TraceMethod<AppsIndex>())
{
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<IAppEntity> GetAppAsync(string name)
{
using (Profiler.TraceMethod<AppsIndex>())
{
var appId = await GetAppIdAsync(name);
if (appId == default)
{
return null;
}
return await GetAppAsync(appId);
}
}
public async Task<IAppEntity> GetAppAsync(Guid appId)
{
using (Profiler.TraceMethod<AppsIndex>())
{
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (IsFound(app.Value))
{
return app.Value;
}
return null;
}
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByUserIndexGrain>(userId).GetIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync()
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id).GetIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync(string[] names)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id).GetIdsAsync(names);
}
}
private async Task<Guid> GetAppIdAsync(string name)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id).GetIdAsync(name);
}
}
public async Task HandleAsync(CommandContext context, Func<Task> 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<string> 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<IAppGrain>(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<IAppsByNameIndexGrain>(SingleGrain.Id);
}
private IAppsByUserIndexGrain Index(string id)
{
return grainFactory.GetGrain<IAppsByUserIndexGrain>(id);
}
}
}

35
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs

@ -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<long> CountAsync();
Task<bool> ReserveAppAsync(Guid appId, string name);
Task AddAppAsync(Guid appId, string name);
Task RemoveAppAsync(Guid appId);
Task RebuildAsync(Dictionary<string, Guid> apps);
Task RemoveReservationAsync(Guid appId, string name);
Task<List<Guid>> GetAppIdsAsync();
Task<List<Guid>> GetAppIdsAsync(string[] names);
Task<Guid> GetAppIdAsync(string name);
}
}

12
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs → 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<Guid>, IGrainWithStringKey
{
Task AddAppAsync(Guid appId);
Task RemoveAppAsync(Guid appId);
Task RebuildAsync(HashSet<Guid> apps);
Task<List<Guid>> GetAppIdsAsync();
}
}

17
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<Guid>, IGrainWithStringKey
{
}
}

39
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<List<Guid>> GetIdsAsync();
Task<List<IAppEntity>> GetAppsAsync();
Task<List<IAppEntity>> GetAppsForUserAsync(string userId, PermissionSet permissions);
Task<IAppEntity> GetAppAsync(string name);
Task<IAppEntity> GetAppAsync(Guid appId);
Task<string> ReserveAsync(Guid id, string name);
Task<bool> AddAsync(string token);
Task RemoveReservationAsync(string token);
Task RebuildByContributorsAsync(string contributorId, HashSet<Guid> apps);
Task RebuildAsync(Dictionary<string, Guid> apps);
Task RebuildByContributorsAsync(Guid appId, HashSet<string> contributors);
}
}

14
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<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent>
{
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<EnrichedAssetEvent> CreateEnrichedEventAsync(Envelope<AssetEvent> @event)
{
var result = new EnrichedAssetEvent();
var asset =
(await grainFactory
.GetGrain<IAssetGrain>(@event.Payload.AssetId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber());
SimpleMapper.Map(asset, result);

18
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<IAssetEntity> GetAsync(Guid id, long version = EtagVersion.Any);
}
}

44
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<IAssetEntity> GetAsync(Guid id, long version)
{
using (Profiler.TraceMethod<AssetLoader>())
{
var grain = grainFactory.GetGrain<IAssetGrain>(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;
}
}
}
}

14
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<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>
{
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<EnrichedContentEvent> CreateEnrichedEventAsync(Envelope<ContentEvent> @event)
{
var result = new EnrichedContentEvent();
var content =
(await grainFactory
.GetGrain<IContentGrain>(@event.Payload.ContentId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber());
SimpleMapper.Map(content, result);

5
src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs → 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<IContentEntity> LoadAsync(Guid id, long version);
Task<IContentEntity> GetAsync(Guid id, long version = EtagVersion.Any);
}
}

8
src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentVersionLoader.cs → 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<IContentEntity> LoadAsync(Guid id, long version)
public async Task<IContentEntity> GetAsync(Guid id, long version)
{
using (Profiler.TraceMethod<ContentVersionLoader>())
using (Profiler.TraceMethod<ContentLoader>())
{
var grain = grainFactory.GetGrain<IContentGrain>(id);

6
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<IContentEntity> FindByVersionAsync(Guid id, long version)
{
return contentVersionLoader.LoadAsync(id, version);
return contentVersionLoader.GetAsync(id, version);
}
private static bool WithDraft(Context context)

4
src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities
Task<IAppEntity> GetAppAsync(string appName);
Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name);
@ -30,7 +32,5 @@ namespace Squidex.Domain.Apps.Entities
Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId);
Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions);
}
}

15
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<Guid> ruleIds = new HashSet<Guid>();
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<bool> RestoreEventAsync(Envelope<IEvent> @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<IRulesByAppIndex>(appId).RebuildAsync(ruleIds);
return indexForRules.RebuildAsync(appId, ruleIds);
}
}
}

2
src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs

@ -20,5 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
NamedId<Guid> AppId { get; set; }
Rule RuleDef { get; }
bool IsDeleted { get; }
}
}

17
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<Guid>, IGrainWithGuidKey
{
}
}

15
src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs → 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<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task RemoveRuleAsync(Guid ruleId);
Task RebuildAsync(HashSet<Guid> rules);
Task ClearAsync();
Task<List<Guid>> GetRuleIdsAsync();
Task RebuildAsync(Guid appId, HashSet<Guid> rules);
}
}
}

56
src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs

@ -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<Task> 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<IRuleGrain>(deleteRule.RuleId).GetStateAsync();
await Index(schema.Value.AppId.Id).RemoveRuleAsync(deleteRule.RuleId);
break;
}
}
}
await next();
}
private IRulesByAppIndex Index(Guid appId)
{
return grainFactory.GetGrain<IRulesByAppIndex>(appId);
}
}
}

55
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<RulesByAppIndexState, Guid>, IRulesByAppIndexGrain
{
private readonly IGrainState<GrainState> state;
[CollectionName("Index_RulesByApp")]
public sealed class GrainState
{
public HashSet<Guid> Rules { get; set; } = new HashSet<Guid>();
}
public RulesByAppIndexGrain(IGrainState<GrainState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task ClearAsync()
{
return state.ClearAsync();
}
public Task RebuildAsync(HashSet<Guid> 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<RulesByAppIndexState> state)
: base(state)
{
state.Value.Rules.Remove(ruleId);
return state.WriteAsync();
}
}
public Task<List<Guid>> GetRuleIdsAsync()
{
return Task.FromResult(state.Value.Rules.ToList());
}
[CollectionName("Index_RulesByApp")]
public sealed class RulesByAppIndexState : IdsIndexState<Guid>
{
}
}

118
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<Guid> rues)
{
return Index(appId).RebuildAsync(rues);
}
public async Task<List<IRuleEntity>> GetRulesAsync(Guid appId)
{
using (Profiler.TraceMethod<RulesIndex>())
{
var ids = await GetRuleIdsAsync(appId);
var rules =
await Task.WhenAll(
ids.Select(GetRuleAsync));
return rules.Where(x => x != null).ToList();
}
}
private async Task<IRuleEntity> GetRuleAsync(Guid id)
{
using (Profiler.TraceMethod<RulesIndex>())
{
var ruleEntity = await grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync();
if (IsFound(ruleEntity.Value))
{
return ruleEntity.Value;
}
return null;
}
}
private async Task<List<Guid>> GetRuleIdsAsync(Guid appId)
{
using (Profiler.TraceMethod<RulesIndex>())
{
return await Index(appId).GetIdsAsync();
}
}
public async Task HandleAsync(CommandContext context, Func<Task> 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<IRuleGrain>(id).GetStateAsync();
if (IsFound(rule.Value))
{
await Index(rule.Value.AppId.Id).RemoveAsync(id);
}
}
private IRulesByAppIndexGrain Index(Guid appId)
{
return grainFactory.GetGrain<IRulesByAppIndexGrain>(appId);
}
private static bool IsFound(IRuleEntity rule)
{
return rule.Version > EtagVersion.Empty && !rule.IsDeleted;
}
}
}

4
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<Guid> AppId { get; set; }
public int Limits { get; set; }
public int? NumDays { get; set; }
public DateTime? Triggered { get; set; }
public NamedId<Guid> AppId { get; set; }
}
[CollectionName("UsageTracker")]

13
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<string, Guid> schemasByName = new Dictionary<string, Guid>();
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<bool> RestoreEventAsync(Envelope<IEvent> @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<ISchemasByAppIndex>(appId).RebuildAsync(schemasByName);
return indexSchemas.RebuildAsync(appId, schemasByName);
}
}
}

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

17
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<Guid>, IGrainWithGuidKey
{
}
}

17
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs → 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<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false);
Task RemoveSchemaAsync(Guid schemaId);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool allowDeleted = false);
Task RebuildAsync(Dictionary<string, Guid> schemas);
Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId, bool allowDeleted = false);
Task ClearAsync();
Task<Guid> GetSchemaIdAsync(string name);
Task<List<Guid>> GetSchemaIdsAsync();
Task RebuildAsync(Guid appId, Dictionary<string, Guid> schemas);
}
}
}

56
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs

@ -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<Task> 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<ISchemaGrain>(deleteSchema.SchemaId).GetStateAsync();
await Index(schema.Value.AppId.Id).RemoveSchemaAsync(deleteSchema.SchemaId);
break;
}
}
}
await next();
}
private ISchemasByAppIndex Index(Guid appId)
{
return grainFactory.GetGrain<ISchemasByAppIndex>(appId);
}
}
}

62
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<SchemasByAppIndexGrainState, Guid>, ISchemasByAppIndexGrain
{
private readonly IGrainState<GrainState> state;
[CollectionName("Index_SchemasByApp")]
public sealed class GrainState
{
public Dictionary<string, Guid> Schemas { get; set; } = new Dictionary<string, Guid>();
}
public SchemasByAppIndexGrain(IGrainState<GrainState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task ClearAsync()
{
return state.ClearAsync();
}
public Task RebuildAsync(Dictionary<string, Guid> 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<Guid> GetSchemaIdAsync(string name)
public SchemasByAppIndexGrain(IGrainState<SchemasByAppIndexGrainState> state)
: base(state)
{
state.Value.Schemas.TryGetValue(name, out var schemaId);
return Task.FromResult(schemaId);
}
}
public Task<List<Guid>> GetSchemaIdsAsync()
{
return Task.FromResult(state.Value.Schemas.Values.ToList());
}
[CollectionName("Index_SchemasByApp")]
public sealed class SchemasByAppIndexGrainState : UniqueNameIndexState<Guid>
{
}
}

181
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<string, Guid> schemas)
{
return Index(appId).RebuildAsync(schemas);
}
public async Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId, bool allowDeleted = false)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
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<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool allowDeleted = false)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
var id = await GetSchemaIdAsync(appId, name);
if (id == default)
{
return null;
}
return await GetSchemaAsync(appId, id, allowDeleted);
}
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
if (IsFound(schema.Value, allowDeleted))
{
return schema.Value;
}
return null;
}
}
private async Task<Guid> GetSchemaIdAsync(Guid appId, string name)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
return await Index(appId).GetIdAsync(name);
}
}
private async Task<List<Guid>> GetSchemaIdsAsync(Guid appId)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
return await Index(appId).GetIdsAsync();
}
}
public async Task HandleAsync(CommandContext context, Func<Task> 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<string> 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<ISchemaGrain>(schemaId).GetStateAsync();
if (IsFound(schema.Value, true))
{
await Index(schema.Value.AppId.Id).RemoveAsync(schemaId);
}
}
private ISchemasByAppIndexGrain Index(Guid appId)
{
return grainFactory.GetGrain<ISchemasByAppIndexGrain>(appId);
}
private static bool IsFound(ISchemaEntity entity, bool allowDeleted)
{
return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted);
}
}
}

10
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -27,17 +27,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{
public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain
{
private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer;
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, IJsonSerializer serializer)
public SchemaGrain(IStore<Guid> 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);

27
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<T>
{
Task<long> CountAsync();
Task RebuildAsync(HashSet<T> ids);
Task AddAsync(T id);
Task RemoveAsync(T id);
Task ClearAsync();
Task<List<T>> GetIdsAsync();
}
}

35
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<T>
{
Task<string> ReserveAsync(T id, string name);
Task<bool> AddAsync(string token);
Task<long> CountAsync();
Task RemoveReservationAsync(string token);
Task RemoveAsync(T id);
Task RebuildAsync(Dictionary<string, T> values);
Task ClearAsync();
Task<T> GetIdAsync(string name);
Task<List<T>> GetIdsAsync(string[] names);
Task<List<T>> GetIdsAsync();
}
}

62
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<TState, T> : Grain, IIdsIndexGrain<T> where TState : IdsIndexState<T>, new()
{
private readonly IGrainState<TState> state;
public IdsIndexGrain(IGrainState<TState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task<long> CountAsync()
{
return Task.FromResult<long>(state.Value.Ids.Count);
}
public Task RebuildAsync(HashSet<T> 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<List<T>> GetIdsAsync()
{
return Task.FromResult(state.Value.Ids.ToList());
}
}
}

16
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<T>
{
public HashSet<T> Ids { get; set; } = new HashSet<T>();
}
}

136
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<TState, T> : Grain, IUniqueNameIndexGrain<T> where TState : UniqueNameIndexState<T>, new()
{
private readonly Dictionary<string, (string Name, T Id)> reservations = new Dictionary<string, (string Name, T Id)>();
private readonly IGrainState<TState> state;
public UniqueNameIndexGrain(IGrainState<TState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task<long> CountAsync()
{
return Task.FromResult<long>(state.Value.Names.Count);
}
public Task ClearAsync()
{
reservations.Clear();
return state.ClearAsync();
}
public Task RebuildAsync(Dictionary<string, T> names)
{
state.Value = new TState { Names = names };
return state.WriteAsync();
}
public Task<string> 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<bool> 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<List<T>> GetIdsAsync(string[] names)
{
var result = new List<T>();
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<T> GetIdAsync(string name)
{
state.Value.Names.TryGetValue(name, out var id);
return Task.FromResult(id);
}
public Task<List<T>> 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);
}
}
}

16
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<T>
{
public Dictionary<string, T> Names { get; set; } = new Dictionary<string, T>();
}
}

2
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(() =>
{

32
src/Squidex/Config/Domain/EntitiesServices.cs

@ -108,6 +108,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetQueryService>()
.As<IAssetQueryService>();
services.AddSingletonAs<AssetLoader>()
.As<IAssetLoader>();
services.AddSingletonAs(c => new Lazy<IContentQueryService>(() => c.GetRequiredService<IContentQueryService>()))
.AsSelf();
@ -117,8 +120,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>();
services.AddSingletonAs<ContentVersionLoader>()
.As<IContentVersionLoader>();
services.AddSingletonAs<ContentLoader>()
.As<IContentLoader>();
services.AddSingletonAs<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
@ -236,34 +239,31 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<InviteUserCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<AppsIndex>()
.As<ICommandMiddleware>().As<IAppsIndex>();
services.AddSingletonAs<ContentCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<RulesIndex>()
.As<ICommandMiddleware>().As<IRulesIndex>();
services.AddSingletonAs<AppsByNameIndexCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<SchemasIndex>()
.As<ICommandMiddleware>().As<ISchemasIndex>();
services.AddSingletonAs<AppCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<SchemaCommand, ISchemaGrain>>()
services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<RuleCommand, IRuleGrain>>()
services.AddSingletonAs<ContentCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<AppsByUserIndexCommandMiddleware>()
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<RulesByAppIndexCommandMiddleware>()
services.AddSingletonAs<GrainCommandMiddleware<SchemaCommand, ISchemaGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<SchemasByAppIndexCommandMiddleware>()
services.AddSingletonAs<GrainCommandMiddleware<RuleCommand, IRuleGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<SingletonCommandMiddleware>()

1
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]

93
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexCommandMiddlewareTests.cs

@ -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<IGrainFactory>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly IAppsByNameIndex index = A.Fake<IAppsByNameIndex>();
private readonly Guid appId = Guid.NewGuid();
private readonly AppsByNameIndexCommandMiddleware sut;
public AppsByNameIndexCommandMiddlewareTests()
{
A.CallTo(() => grainFactory.GetGrain<IAppsByNameIndex>(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<ValidationException>(() => 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();
}
}
}

149
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs

@ -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<AppsByNameIndexGrain.GrainState> grainState = A.Fake<IGrainState<AppsByNameIndexGrain.GrainState>>();
private readonly NamedId<Guid> appId1 = NamedId.Of(Guid.NewGuid(), "my-app1");
private readonly NamedId<Guid> 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<Guid> { 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<string, Guid>
{
[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<Guid> { appId1.Id, appId2.Id }, await sut.GetAppIdsAsync());
Assert.Equal(2, await sut.CountAsync());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
}
}

103
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexCommandMiddlewareTests.cs

@ -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<IGrainFactory>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly IAppsByUserIndex index = A.Fake<IAppsByUserIndex>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly string userId = "123";
private readonly AppsByUserIndexCommandMiddleware sut;
public AppsByUserIndexCommandMiddlewareTests()
{
A.CallTo(() => grainFactory.GetGrain<IAppsByUserIndex>(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<IAppGrain>();
var appState = Mocks.App(appId);
A.CallTo(() => grainFactory.GetGrain<IAppGrain>(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();
}
}
}

76
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs

@ -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<AppsByUserIndexGrain.GrainState> grainState = A.Fake<IGrainState<AppsByUserIndexGrain.GrainState>>();
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<Guid> { 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<Guid> { 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<Guid> { appId1, appId2 }, result);
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
}
}

387
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<IGrainFactory>();
private readonly IAppsByNameIndexGrain indexByName = A.Fake<IAppsByNameIndexGrain>();
private readonly IAppsByUserIndexGrain indexByUser = A.Fake<IAppsByUserIndexGrain>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly string userId = "user-1";
private readonly AppsIndex sut;
public AppsIndexTests()
{
A.CallTo(() => grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id, null))
.Returns(indexByName);
A.CallTo(() => grainFactory.GetGrain<IAppsByUserIndexGrain>(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<string[]>.That.IsSameSequenceAs(new string[] { appId.Name })))
.Returns(new List<Guid> { 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<Guid> { 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<Guid> { 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<string>.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<string>.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<string>(null));
var context =
new CommandContext(Create(appId.Name), commandBus)
.Complete();
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
A.CallTo(() => indexByName.AddAsync(A<string>.Ignored))
.MustNotHaveHappened();
A.CallTo(() => indexByName.RemoveReservationAsync(A<string>.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<string>.Ignored))
.MustNotHaveHappened();
A.CallTo(() => indexByName.RemoveReservationAsync(A<string>.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<Guid>();
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<string> { 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<string, Guid>();
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<IAppEntity>();
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<IAppGrain>();
A.CallTo(() => appGrain.GetStateAsync())
.Returns(J.Of(appEntity));
A.CallTo(() => grainFactory.GetGrain<IAppGrain>(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);
}
}
}

15
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<IScriptEngine>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>();
private readonly IRuleTriggerHandler sut;
public AssetChangedTriggerHandlerTests()
@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, "false"))
.Returns(false);
sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory);
sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader);
}
public static IEnumerable<object[]> TestEvents = new[]
@ -56,13 +54,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
var assetGrain = A.Fake<IAssetGrain>();
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(@event.AssetId, null))
.Returns(assetGrain);
A.CallTo(() => assetGrain.GetStateAsync(12))
.Returns(J.Of<IAssetEntity>(new AssetEntity()));
A.CallTo(() => assetLoader.GetAsync(@event.AssetId, 12))
.Returns(new AssetEntity());
var result = await sut.CreateEnrichedEventAsync(envelope);

66
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<IGrainFactory>();
private readonly IAssetGrain grain = A.Fake<IAssetGrain>();
private readonly Guid id = Guid.NewGuid();
private readonly AssetLoader sut;
public AssetLoaderTests()
{
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(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<IAssetEntity>(null));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => 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<IAssetEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => 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<IAssetEntity>(content));
var result = await sut.GetAsync(id, 10);
Assert.Same(content, result);
}
}
}

15
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<IScriptEngine>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IContentLoader contentLoader = A.Fake<IContentLoader>();
private readonly IRuleTriggerHandler sut;
private readonly Guid ruleId = Guid.NewGuid();
private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1");
@ -46,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.Evaluate("event", A<object>.Ignored, "false"))
.Returns(false);
sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory);
sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader);
}
public static IEnumerable<object[]> TestEvents = new[]
@ -65,13 +63,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12);
var contentGrain = A.Fake<IContentGrain>();
A.CallTo(() => grainFactory.GetGrain<IContentGrain>(@event.ContentId, null))
.Returns(contentGrain);
A.CallTo(() => contentGrain.GetStateAsync(12))
.Returns(J.Of<IContentEntity>(new ContentEntity { SchemaId = SchemaMatch }));
A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12))
.Returns(new ContentEntity { SchemaId = SchemaMatch });
var result = await sut.CreateEnrichedEventAsync(envelope);

14
tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentVersionLoaderTests.cs → 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<IGrainFactory>();
private readonly IContentGrain grain = A.Fake<IContentGrain>();
private readonly Guid id = Guid.NewGuid();
private readonly ContentVersionLoader sut;
private readonly ContentLoader sut;
public ContentVersionLoaderTests()
public ContentLoaderTests()
{
A.CallTo(() => grainFactory.GetGrain<IContentGrain>(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<IContentEntity>(null));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(id, 10));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(id, 10));
}
[Fact]
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(id, 10));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(id, 10));
}
[Fact]
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(content));
var result = await sut.LoadAsync(id, 10);
var result = await sut.GetAsync(id, 10);
Assert.Same(content, result);
}

4
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<IAssetUrlGenerator>();
private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContentVersionLoader contentVersionLoader = A.Fake<IContentVersionLoader>();
private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>();
private readonly ISchemaEntity schema;
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
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;

79
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexCommandMiddlewareTests.cs

@ -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<IGrainFactory>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly IRulesByAppIndex index = A.Fake<IRulesByAppIndex>();
private readonly Guid appId = Guid.NewGuid();
private readonly RulesByAppIndexCommandMiddleware sut;
public RulesByAppIndexCommandMiddlewareTests()
{
A.CallTo(() => grainFactory.GetGrain<IRulesByAppIndex>(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<IRuleGrain>();
var ruleState = A.Fake<IRuleEntity>();
A.CallTo(() => grainFactory.GetGrain<IRuleGrain>(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<Guid> BuildAppId()
{
return NamedId.Of(appId, "my-app");
}
}
}

97
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs

@ -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<RulesByAppIndexGrain.GrainState> grainState = A.Fake<IGrainState<RulesByAppIndexGrain.GrainState>>();
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<Guid> { 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<Guid> { ruleId2 }, result);
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceOrMore();
}
[Fact]
public async Task Should_replace_rule_ids_on_rebuild()
{
var state = new HashSet<Guid>
{
ruleId1,
ruleId2
};
await sut.RebuildAsync(state);
var result = await sut.GetRuleIdsAsync();
Assert.Equal(new List<Guid> { ruleId1, ruleId2 }, result);
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
}
}

143
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<IGrainFactory>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly IRulesByAppIndexGrain index = A.Fake<IRulesByAppIndexGrain>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly RulesIndex sut;
public RulesIndexTests()
{
A.CallTo(() => grainFactory.GetGrain<IRulesByAppIndexGrain>(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<Guid> { 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<Guid> { 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<Guid> { 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<Guid>();
await sut.RebuildAsync(appId.Id, rules);
A.CallTo(() => index.RebuildAsync(rules))
.MustHaveHappened();
}
private IRuleEntity SetupRule(long version, bool deleted)
{
var ruleEntity = A.Fake<IRuleEntity>();
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<IRuleGrain>();
A.CallTo(() => ruleGrain.GetStateAsync())
.Returns(J.Of(ruleEntity));
A.CallTo(() => grainFactory.GetGrain<IRuleGrain>(ruleId, null))
.Returns(ruleGrain);
return ruleEntity;
}
}
}

70
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<IAppProvider>();
private readonly Schema schema_0;
private readonly NamedId<Guid> 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<Guid>.Ignored, A<string>.Ignored))
.Returns(Task.FromResult<ISchemaEntity>(null));
A.CallTo(() => appProvider.GetSchemaAsync(A<Guid>.Ignored, "existing"))
.Returns(A.Dummy<ISchemaEntity>());
}
[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]

73
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexCommandMiddlewareTests.cs

@ -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<IGrainFactory>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly ISchemasByAppIndex index = A.Fake<ISchemasByAppIndex>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly SchemasByAppIndexCommandMiddleware sut;
public SchemasByAppIndexCommandMiddlewareTests()
{
A.CallTo(() => grainFactory.GetGrain<ISchemasByAppIndex>(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<ISchemaGrain>();
var schemaState = Mocks.Schema(appId, schemaId);
A.CallTo(() => grainFactory.GetGrain<ISchemaGrain>(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();
}
}
}

96
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs

@ -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<SchemasByAppIndexGrain.GrainState> grainState = A.Fake<IGrainState<SchemasByAppIndexGrain.GrainState>>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId1 = NamedId.Of(Guid.NewGuid(), "my-schema1");
private readonly NamedId<Guid> 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<string, Guid>
{
[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<Guid> { schemaId1.Id, schemaId2.Id }, await sut.GetSchemaIdsAsync());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
}
}

248
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<IGrainFactory>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly ISchemasByAppIndexGrain index = A.Fake<ISchemasByAppIndexGrain>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly SchemasIndex sut;
public SchemasIndexTests()
{
A.CallTo(() => grainFactory.GetGrain<ISchemasByAppIndexGrain>(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<Guid> { 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<Guid> { 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<string>.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<string>(null));
var context =
new CommandContext(Create(schemaId.Name), commandBus)
.Complete();
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
A.CallTo(() => index.AddAsync(A<string>.Ignored))
.MustNotHaveHappened();
A.CallTo(() => index.RemoveReservationAsync(A<string>.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<string>.Ignored))
.MustNotHaveHappened();
A.CallTo(() => index.RemoveReservationAsync(A<string>.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<string, Guid>();
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<ISchemaEntity>();
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<ISchemaGrain>();
A.CallTo(() => schemaGrain.GetStateAsync())
.Returns(J.Of(schemaEntity));
A.CallTo(() => grainFactory.GetGrain<ISchemaGrain>(schemaId.Id, null))
.Returns(schemaGrain);
return schemaEntity;
}
}
}

6
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs

@ -25,7 +25,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaGrainTests : HandlerTestBase<SchemaState>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly string fieldName = "age";
private readonly string arrayName = "array";
private readonly NamedId<long> 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<ISemanticLog>(), appProvider, TestUtils.DefaultSerializer);
sut = new SchemaGrain(Store, A.Dummy<ISemanticLog>(), TestUtils.DefaultSerializer);
sut.ActivateAsync(Id).Wait();
}

103
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<IdsIndexState<Guid>> grainState = A.Fake<IGrainState<IdsIndexState<Guid>>>();
private readonly Guid id1 = Guid.NewGuid();
private readonly Guid id2 = Guid.NewGuid();
private readonly IdsIndexGrain<IdsIndexState<Guid>, Guid> sut;
public IdsIndexGrainTests()
{
A.CallTo(() => grainState.ClearAsync())
.Invokes(() => grainState.Value = new IdsIndexState<Guid>());
sut = new IdsIndexGrain<IdsIndexState<Guid>, 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<Guid> { 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<Guid> { id2 }, result);
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceOrMore();
}
[Fact]
public async Task Should_replace__ids_on_rebuild()
{
var state = new HashSet<Guid>
{
id1,
id2
};
await sut.RebuildAsync(state);
var result = await sut.GetIdsAsync();
Assert.Equal(new List<Guid> { id1, id2 }, result);
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
}
}

197
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<UniqueNameIndexState<Guid>> grainState = A.Fake<IGrainState<UniqueNameIndexState<Guid>>>();
private readonly NamedId<Guid> id1 = NamedId.Of(Guid.NewGuid(), "my-name1");
private readonly NamedId<Guid> id2 = NamedId.Of(Guid.NewGuid(), "my-name2");
private readonly UniqueNameIndexGrain<UniqueNameIndexState<Guid>, Guid> sut;
public UniqueNameIndexGrainTests()
{
A.CallTo(() => grainState.ClearAsync())
.Invokes(() => grainState.Value = new UniqueNameIndexState<Guid>());
sut = new UniqueNameIndexGrain<UniqueNameIndexState<Guid>, 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<string, Guid>
{
[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<Guid> { 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<Guid> { id1.Id, id2.Id }, result);
}
private async Task AddAsync(NamedId<Guid> id)
{
var token = await sut.ReserveAsync(id.Id, id.Name);
await sut.AddAsync(token);
}
}
}

7
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<ConvertOldSnapshotStores>();
}
// Version 19: Unify indexes.
if (version < 19)
{
yield return serviceProvider.GetRequiredService<PopulateGrainIndexes>();
}

26
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<IAppsByNameIndex>(SingleGrain.Id).GetAppIdsAsync();
var ids = await indexForApps.GetIdsAsync();
foreach (var id in ids)
{
var app = grainFactory.GetGrain<IAppGrain>(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);
}
}
}

22
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<AppState, Guid> statesForApps;
private readonly ISnapshotStore<RuleState, Guid> statesForRules;
private readonly ISnapshotStore<SchemaState, Guid> statesForSchemas;
public PopulateGrainIndexes(
IGrainFactory grainFactory,
IAppsIndex indexApps,
IRulesIndex indexRules,
ISchemasIndex indexSchemas,
ISnapshotStore<AppState, Guid> statesForApps,
ISnapshotStore<RuleState, Guid> statesForRules,
ISnapshotStore<SchemaState, Guid> 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<IAppsByNameIndex>(SingleGrain.Id).RebuildAsync(appsByName);
await indexApps.RebuildAsync(appsByName);
foreach (var kvp in appsByUser)
{
await grainFactory.GetGrain<IAppsByUserIndex>(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<IRulesByAppIndex>(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<ISchemasByAppIndex>(kvp.Key).RebuildAsync(kvp.Value);
await indexSchemas.RebuildAsync(kvp.Key, kvp.Value);
}
}
}

Loading…
Cancel
Save