From 274a371b27a5f5115b7dfdc17fb42232c937915a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 4 May 2018 18:35:35 +0200 Subject: [PATCH] Feature grains. --- Dockerfile | 2 +- .../Apps/MongoAppEntity.cs | 44 ------ .../Apps/MongoAppRepository.cs | 64 --------- .../Apps/MongoAppRepository_SnapshotStore.cs | 44 ------ .../MongoAssetRepository_SnapshotStore.cs | 5 + .../MongoContentRepository_SnapshotStore.cs | 5 + .../History/ParsedHistoryEvent.cs | 15 +-- .../Rules/MongoRuleEntity.cs | 41 ------ .../Rules/MongoRuleRepository.cs | 45 ------- .../MongoRuleRepository_SnapshotStore.cs | 42 ------ .../Schemas/MongoSchemaEntity.cs | 45 ------- .../Schemas/MongoSchemaRepository.cs | 54 -------- .../MongoSchemaRepository_SnapshotStore.cs | 43 ------ .../AppProvider.cs | 31 +---- .../Apps/AppHistoryEventsCreator.cs | 38 +++++- .../AppsByNameIndexCommandMiddleware.cs | 47 +++++++ .../Apps/Indexes/AppsByNameIndexGrain.cs | 80 +++++++++++ .../AppsByUserIndexCommandMiddleware.cs | 80 +++++++++++ .../Apps/Indexes/AppsByUserIndexGrain.cs | 73 ++++++++++ .../Apps/Indexes/IAppsByNameIndex.cs | 27 ++++ .../IAppsByUserIndex.cs} | 15 ++- .../Apps/State/AppState.cs | 5 +- .../Assets/State/AssetState.cs | 4 +- .../Contents/State/ContentState.cs | 3 +- .../State/ContentStateScheduleItem.cs | 26 ---- .../Indexes/IRulesByAppIndex.cs} | 15 ++- .../RulesByAppIndexCommandMiddleware.cs | 56 ++++++++ .../Rules/Indexes/RulesByAppIndexGrain.cs | 73 ++++++++++ .../Rules/State/RuleState.cs | 5 +- .../Schemas/Indexes/ISchemasByAppIndex.cs | 27 ++++ .../SchemasByAppIndexCommandMiddleware.cs | 56 ++++++++ .../Schemas/Indexes/SchemasByAppIndexGrain.cs | 80 +++++++++++ .../Schemas/State/SchemaState.cs | 5 +- .../States/MongoSnapshotStore.cs | 13 +- .../States/MongoState.cs | 1 + .../Grains/OrleansEventNotifier.cs | 3 +- .../Orleans/Bootstrap.cs | 2 +- .../Orleans/SingleGrain.cs} | 12 +- .../States/CollectionNameAttribute.cs} | 18 +-- .../States/DefaultStreamNameResolver.cs | 2 +- .../States/ISnapshotStore.cs | 3 + .../EventConsumersController.cs | 3 +- src/Squidex/Config/Domain/EntitiesServices.cs | 34 +++-- src/Squidex/Config/Domain/StoreServices.cs | 71 +++------- .../Grains/OrleansEventNotifierTests.cs | 3 +- tools/Migrate_01/MigrationPath.cs | 15 ++- tools/Migrate_01/Migrations/AddPatterns.cs | 8 +- .../Migrations/ConvertOldSnapshotStores.cs | 44 ++++++ .../Migrations/PopulateGrainIndexes.cs | 127 ++++++++++++++++++ tools/Migrate_01/Rebuilder.cs | 16 +-- 50 files changed, 958 insertions(+), 612 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs rename src/Squidex.Domain.Apps.Entities/Apps/{Repositories/IAppRepository.cs => Indexes/IAppsByUserIndex.cs} (55%) delete mode 100644 src/Squidex.Domain.Apps.Entities/Contents/State/ContentStateScheduleItem.cs rename src/Squidex.Domain.Apps.Entities/{Schemas/Repositories/ISchemaRepository.cs => Rules/Indexes/IRulesByAppIndex.cs} (54%) create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs rename src/{Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs => Squidex.Infrastructure/Orleans/SingleGrain.cs} (53%) rename src/{Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs => Squidex.Infrastructure/States/CollectionNameAttribute.cs} (57%) create mode 100644 tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs create mode 100644 tools/Migrate_01/Migrations/PopulateGrainIndexes.cs diff --git a/Dockerfile b/Dockerfile index ed342ba9d..cde0085b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ FROM microsoft/aspnetcore:2.0.3-jessie # Default AspNetCore directory WORKDIR /app -# Copy from nuild stage +# Copy from build stage COPY --from=builder /out/ . EXPOSE 80 diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs deleted file mode 100644 index 58a61860d..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps -{ - public sealed class MongoAppEntity : IVersionedEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonElement] - [BsonRequired] - [BsonJson] - public AppState State { get; set; } - - [BsonElement] - [BsonRequired] - public long Version { get; set; } - - [BsonElement] - [BsonRequired] - public string Name { get; set; } - - [BsonElement] - [BsonRequired] - public string[] UserIds { get; set; } - - [BsonElement] - [BsonIgnoreIfDefault] - public bool IsArchived { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs deleted file mode 100644 index 0b68080ab..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Apps.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps -{ - public sealed partial class MongoAppRepository : MongoRepositoryBase, IAppRepository - { - public MongoAppRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Apps"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); - } - - public async Task> QueryAppIdsAsync() - { - var appEntities = - await Collection.Find(new BsonDocument()).Only(x => x.Id) - .ToListAsync(); - - return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task> QueryUserAppIdsAsync(string userId) - { - var appEntities = - await Collection.Find(x => x.UserIds.Contains(userId) && x.IsArchived != true).Only(x => x.Id) - .ToListAsync(); - - return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task FindAppIdByNameAsync(string name) - { - var appEntity = - await Collection.Find(x => x.Name == name && x.IsArchived != true).Only(x => x.Id) - .FirstOrDefaultAsync(); - - return appEntity != null ? Guid.Parse(appEntity["_id"].AsString) : Guid.Empty; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs deleted file mode 100644 index da46ed5d2..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository_SnapshotStore.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps -{ - public sealed partial class MongoAppRepository : ISnapshotStore - { - public async Task<(AppState Value, long Version)> ReadAsync(Guid key) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.State, existing.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task WriteAsync(Guid key, AppState value, long oldVersion, long newVersion) - { - return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u - .Set(x => x.Name, value.Name) - .Set(x => x.State, value) - .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) - .Set(x => x.IsArchived, value.IsArchived)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index a4056d0a7..92633415d 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -17,6 +17,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public sealed partial class MongoAssetRepository : ISnapshotStore { + Task ISnapshotStore.ReadAllAsync(Func callback) + { + throw new NotSupportedException(); + } + public async Task<(AssetState Value, long Version)> ReadAsync(Guid key) { var existing = diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 4bd67fd96..93df9d115 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -20,6 +20,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { public partial class MongoContentRepository : ISnapshotStore { + Task ISnapshotStore.ReadAllAsync(Func callback) + { + throw new NotSupportedException(); + } + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) { var contentEntity = diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs index 2ac151ee8..156a32822 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs @@ -11,8 +11,6 @@ using NodaTime; using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure; -#pragma warning disable RECS0029 // Warns about property or indexer setters and event adders or removers that do not use the value parameter - namespace Squidex.Domain.Apps.Entities.MongoDb.History { internal sealed class ParsedHistoryEvent : IHistoryEventEntity @@ -23,19 +21,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History public Guid Id { get { return inner.Id; } - set { } + } + + public Guid EventId + { + get { return inner.Id; } } public Instant Created { get { return inner.Created; } - set { } } public Instant LastModified { get { return inner.LastModified; } - set { } } public RefToken Actor @@ -43,11 +43,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History get { return inner.Actor; } } - public Guid EventId - { - get { return inner.Id; } - } - public long Version { get { return inner.Version; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs deleted file mode 100644 index 962c632aa..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed class MongoRuleEntity : IVersionedEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonElement] - [BsonRequired] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonElement] - [BsonRequired] - [BsonJson] - public RuleState State { get; set; } - - [BsonElement] - [BsonRequired] - public long Version { get; set; } - - [BsonElement] - [BsonRequired] - public bool IsDeleted { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs deleted file mode 100644 index 6ab0b1f3b..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed partial class MongoRuleRepository : MongoRepositoryBase, IRuleRepository - { - public MongoRuleRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Rules"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); - } - - public async Task> QueryRuleIdsAsync(Guid appId) - { - var ruleEntities = - await Collection.Find(x => x.AppId == appId && !x.IsDeleted).Only(x => x.Id) - .ToListAsync(); - - return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs deleted file mode 100644 index cd8a2ee02..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules -{ - public sealed partial class MongoRuleRepository : ISnapshotStore - { - public async Task<(RuleState Value, long Version)> ReadAsync(Guid key) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.State, existing.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task WriteAsync(Guid key, RuleState value, long oldVersion, long newVersion) - { - return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u - .Set(x => x.State, value) - .Set(x => x.AppId, value.AppId.Id) - .Set(x => x.IsDeleted, value.IsDeleted)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs deleted file mode 100644 index 100986972..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas -{ - public sealed class MongoSchemaEntity : IVersionedEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonElement] - [BsonRequired] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonElement] - [BsonRequired] - [BsonJson] - public SchemaState State { get; set; } - - [BsonElement] - [BsonRequired] - public string Name { get; set; } - - [BsonElement] - [BsonRequired] - public long Version { get; set; } - - [BsonElement] - [BsonRequired] - public bool IsDeleted { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs deleted file mode 100644 index df56da8b8..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas -{ - public sealed partial class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository - { - public MongoSchemaRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "States_Schemas"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.IsDeleted)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.Name).Ascending(x => x.IsDeleted)); - } - - public async Task FindSchemaIdAsync(Guid appId, string name) - { - var schemaEntity = - await Collection.Find(x => x.AppId == appId && x.Name == name && !x.IsDeleted).Only(x => x.Id).SortByDescending(x => x.Version) - .FirstOrDefaultAsync(); - - return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty; - } - - public async Task> QuerySchemaIdsAsync(Guid appId) - { - var schemaEntities = - await Collection.Find(x => x.AppId == appId && !x.IsDeleted).Only(x => x.Id) - .ToListAsync(); - - return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs deleted file mode 100644 index a23899a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas -{ - public sealed partial class MongoSchemaRepository : ISnapshotStore - { - public async Task<(SchemaState Value, long Version)> ReadAsync(Guid key) - { - var existing = - await Collection.Find(x => x.Id == key) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.State, existing.Version); - } - - return (null, EtagVersion.NotFound); - } - - public Task WriteAsync(Guid key, SchemaState value, long oldVersion, long newVersion) - { - return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u - .Set(x => x.State, value) - .Set(x => x.AppId, value.AppId.Id) - .Set(x => x.Name, value.Name) - .Set(x => x.IsDeleted, value.IsDeleted)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index ef9f2c419..f7ada02c3 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -11,11 +11,8 @@ using System.Linq; using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Log; @@ -26,29 +23,15 @@ namespace Squidex.Domain.Apps.Entities public sealed class AppProvider : IAppProvider { private readonly IGrainFactory grainFactory; - private readonly IAppRepository appRepository; - private readonly IRuleRepository ruleRepository; private readonly IRequestCache requestCache; - private readonly ISchemaRepository schemaRepository; - - public AppProvider( - IGrainFactory grainFactory, - IAppRepository appRepository, - ISchemaRepository schemaRepository, - IRuleRepository ruleRepository, - IRequestCache requestCache) + + public AppProvider(IGrainFactory grainFactory, IRequestCache requestCache) { Guard.NotNull(grainFactory, nameof(grainFactory)); - Guard.NotNull(appRepository, nameof(appRepository)); - Guard.NotNull(schemaRepository, nameof(schemaRepository)); Guard.NotNull(requestCache, nameof(requestCache)); - Guard.NotNull(ruleRepository, nameof(ruleRepository)); this.grainFactory = grainFactory; - this.appRepository = appRepository; - this.schemaRepository = schemaRepository; this.requestCache = requestCache; - this.ruleRepository = ruleRepository; } public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) @@ -143,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - var ids = await schemaRepository.QuerySchemaIdsAsync(appId); + var ids = await grainFactory.GetGrain(appId).GetSchemaIdsAsync(); var schemas = await Task.WhenAll( @@ -160,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - var ids = await ruleRepository.QueryRuleIdsAsync(appId); + var ids = await grainFactory.GetGrain(appId).GetRuleIdsAsync(); var rules = await Task.WhenAll( @@ -177,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - var ids = await appRepository.QueryUserAppIdsAsync(userId); + var ids = await grainFactory.GetGrain(userId).GetAppIdsAsync(); var apps = await Task.WhenAll( @@ -192,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - return await appRepository.FindAppIdByNameAsync(name); + return await grainFactory.GetGrain(SingleGrain.Id).GetAppIdAsync(name); } } @@ -200,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities { using (Profile.Method()) { - return await schemaRepository.FindSchemaIdAsync(appId, name); + return await grainFactory.GetGrain(appId).GetSchemaIdAsync(name); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index e722f8d06..d36f551f0 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Apps : base(typeNameRegistry) { AddEventMessage( - "assigned {user:[Contributor]} as [Permission]"); + "assigned {user:[Contributor]} as {[Permission]}"); AddEventMessage( "removed {user:[Contributor]} from app"); @@ -48,6 +48,15 @@ namespace Squidex.Domain.Apps.Entities.Apps AddEventMessage( "changed master language to {[Language]}"); + + AddEventMessage( + "added pattern {[Name]}"); + + AddEventMessage( + "deleted pattern {[Name]}"); + + AddEventMessage( + "updated pattern {[Name]}"); } protected Task On(AppContributorRemoved @event) @@ -131,6 +140,33 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } + protected Task On(AppPatternAdded @event) + { + const string channel = "settings.patterns"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Name", @event.Name)); + } + + protected Task On(AppPatternUpdated @event) + { + const string channel = "settings.patterns"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Name", @event.Name)); + } + + protected Task On(AppPatternDeleted @event) + { + const string channel = "settings.patterns"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Name", @event.Name)); + } + protected override Task CreateEventCoreAsync(Envelope @event) { return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs new file mode 100644 index 000000000..798fd6f1b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// 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; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByNameIndexCommandMiddleware : ICommandMiddleware + { + private readonly IAppsByNameIndex index; + + public AppsByNameIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + index = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateApp createApp: + await index.AddAppAsync(createApp.AppId, createApp.Name); + break; + case ArchiveApp archiveApp: + await index.RemoveAppAsync(archiveApp.AppId); + break; + } + } + + await next(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs new file mode 100644 index 000000000..72e698cc1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// 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 Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByNameIndexGrain : GrainOfString, IAppsByNameIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_AppsByName")] + private sealed class State + { + public Dictionary Apps { get; set; } = new Dictionary(); + } + + public AppsByNameIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(string key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(Dictionary apps) + { + state = new State { Apps = apps }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddAppAsync(Guid appId, string name) + { + state.Apps[name] = appId; + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveAppAsync(Guid appId) + { + state.Apps.Remove(state.Apps.FirstOrDefault(x => x.Value == appId).Key ?? string.Empty); + + return persistence.WriteSnapshotAsync(state); + } + + public Task GetAppIdAsync(string appName) + { + state.Apps.TryGetValue(appName, out var appId); + + return Task.FromResult(appId); + } + + public Task> GetAppIdAsync() + { + return Task.FromResult(state.Apps.Values.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs new file mode 100644 index 000000000..3327fc914 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByUserIndexCommandMiddleware : ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public AppsByUserIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateApp createApp: + await Index(GetUserId(createApp)).AddAppAsync(createApp.AppId); + break; + case AssignContributor assignContributor: + await Index(GetUserId(context)).AddAppAsync(assignContributor.AppId); + break; + case RemoveContributor removeContributor: + await Index(GetUserId(removeContributor)).RemoveAppAsync(removeContributor.AppId); + break; + case ArchiveApp archiveApp: + { + var appState = await grainFactory.GetGrain(archiveApp.AppId).GetStateAsync(); + + foreach (var contributorId in appState.Value.Contributors.Keys) + { + await Index(contributorId).RemoveAppAsync(archiveApp.AppId); + } + + break; + } + } + } + + await next(); + } + + private static string GetUserId(RemoveContributor removeContributor) + { + return removeContributor.ContributorId; + } + + private static string GetUserId(CreateApp createApp) + { + return createApp.Actor.Identifier; + } + + private static string GetUserId(CommandContext context) + { + return context.Result>().IdOrValue; + } + + private IAppsByUserIndex Index(string id) + { + return grainFactory.GetGrain(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs new file mode 100644 index 000000000..8ab4e0d36 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// 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 Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Apps.Indexes +{ + public sealed class AppsByUserIndexGrain : GrainOfString, IAppsByUserIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_AppsByUser")] + private sealed class State + { + public HashSet Apps { get; set; } = new HashSet(); + } + + public AppsByUserIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(string key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(HashSet apps) + { + state = new State { Apps = apps }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddAppAsync(Guid appId) + { + state.Apps.Add(appId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveAppAsync(Guid appId) + { + state.Apps.Remove(appId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task> GetAppIdsAsync() + { + return Task.FromResult(state.Apps.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs new file mode 100644 index 000000000..8af4a5fbb --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// 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 +{ + public interface IAppsByNameIndex : IGrainWithStringKey + { + Task AddAppAsync(Guid appId, string name); + + Task RemoveAppAsync(Guid appId); + + Task RebuildAsync(Dictionary apps); + + Task GetAppIdAsync(string name); + + Task> GetAppIdAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs similarity index 55% rename from src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs index c8f1a339e..8b960b13d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs @@ -1,22 +1,25 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// 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.Repositories +namespace Squidex.Domain.Apps.Entities.Apps { - public interface IAppRepository + public interface IAppsByUserIndex : IGrainWithStringKey { - Task FindAppIdByNameAsync(string name); + Task AddAppAsync(Guid appId); - Task> QueryAppIdsAsync(); + Task RemoveAppAsync(Guid appId); - Task> QueryUserAppIdsAsync(string userId); + Task RebuildAsync(HashSet apps); + + Task> GetAppIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 4662de274..9a60918c9 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -12,11 +12,12 @@ using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Apps.State { - public class AppState : DomainObjectState, - IAppEntity + [CollectionName("Apps")] + public class AppState : DomainObjectState, IAppEntity { [JsonProperty] public string Name { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index 3ed7714a7..d1cf6df53 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -17,9 +17,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets.State { - public class AssetState : DomainObjectState, - IAssetEntity, - IAssetInfo + public class AssetState : DomainObjectState, IAssetEntity, IAssetInfo { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 489a03832..f2c6b07c5 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -17,8 +17,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Contents.State { - public class ContentState : DomainObjectState, - IContentEntity + public class ContentState : DomainObjectState, IContentEntity { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentStateScheduleItem.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentStateScheduleItem.cs deleted file mode 100644 index a73f6f0a4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentStateScheduleItem.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.State -{ - public sealed class ContentStateScheduleItem : IContentScheduleItem - { - [JsonProperty] - public Instant ScheduledAt { get; set; } - - [JsonProperty] - public RefToken ScheduledBy { get; set; } - - [JsonProperty] - public Status ScheduledTo { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs similarity index 54% rename from src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs rename to src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs index 53294cef3..dd169a045 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs @@ -1,20 +1,25 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// 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.Schemas.Repositories +namespace Squidex.Domain.Apps.Entities.Rules { - public interface ISchemaRepository + public interface IRulesByAppIndex : IGrainWithGuidKey { - Task FindSchemaIdAsync(Guid appId, string name); + Task AddRuleAsync(Guid ruleId); - Task> QuerySchemaIdsAsync(Guid appId); + Task RemoveRuleAsync(Guid ruleId); + + Task RebuildAsync(HashSet rules); + + Task> GetRuleIdsAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs new file mode 100644 index 000000000..faee7b695 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesByAppIndexCommandMiddleware : ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public RulesByAppIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateRule createRule: + await Index(createRule.AppId.Id).AddRuleAsync(createRule.RuleId); + break; + case DeleteRule deleteRule: + { + var schema = await grainFactory.GetGrain(deleteRule.RuleId).GetStateAsync(); + + await Index(schema.Value.AppId.Id).RemoveRuleAsync(deleteRule.RuleId); + + break; + } + } + } + + await next(); + } + + private IRulesByAppIndex Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs new file mode 100644 index 000000000..022c0f6d1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// 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 Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public sealed class RulesByAppIndexGrain : GrainOfGuid, IRulesByAppIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_RulesByApp")] + private sealed class State + { + public HashSet Rules { get; set; } = new HashSet(); + } + + public RulesByAppIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(Guid key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(HashSet rules) + { + state = new State { Rules = rules }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddRuleAsync(Guid ruleId) + { + state.Rules.Add(ruleId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveRuleAsync(Guid ruleId) + { + state.Rules.Remove(ruleId); + + return persistence.WriteSnapshotAsync(state); + } + + public Task> GetRuleIdsAsync() + { + return Task.FromResult(state.Rules.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index fa87078da..80da5ed16 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -13,11 +13,12 @@ using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules.State { - public class RuleState : DomainObjectState, - IRuleEntity + [CollectionName("Rules")] + public class RuleState : DomainObjectState, IRuleEntity { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs new file mode 100644 index 000000000..38104d363 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// 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.Schemas +{ + public interface ISchemasByAppIndex : IGrainWithGuidKey + { + Task AddSchemaAsync(Guid schemaId, string name); + + Task RemoveSchemaAsync(Guid schemaId); + + Task RebuildAsync(Dictionary schemas); + + Task GetSchemaIdAsync(string name); + + Task> GetSchemaIdsAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs new file mode 100644 index 000000000..1090894d5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasByAppIndexCommandMiddleware : ICommandMiddleware + { + private readonly IGrainFactory grainFactory; + + public SchemasByAppIndexCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (context.IsCompleted) + { + switch (context.Command) + { + case CreateSchema createSchema: + await Index(createSchema.AppId.Id).AddSchemaAsync(createSchema.SchemaId, createSchema.Name); + break; + case DeleteSchema deleteSchema: + { + var schema = await grainFactory.GetGrain(deleteSchema.SchemaId).GetStateAsync(); + + await Index(schema.Value.AppId.Id).RemoveSchemaAsync(deleteSchema.SchemaId); + + break; + } + } + } + + await next(); + } + + private ISchemasByAppIndex Index(Guid appId) + { + return grainFactory.GetGrain(appId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs new file mode 100644 index 000000000..5463ed8f8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// 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 Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public sealed class SchemasByAppIndexGrain : GrainOfGuid, ISchemasByAppIndex + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_SchemasByApp")] + private sealed class State + { + public Dictionary Schemas { get; set; } = new Dictionary(); + } + + public SchemasByAppIndexGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(Guid key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public Task RebuildAsync(Dictionary schemas) + { + state = new State { Schemas = schemas }; + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddSchemaAsync(Guid schemaId, string name) + { + state.Schemas[name] = schemaId; + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveSchemaAsync(Guid schemaId) + { + state.Schemas.Remove(state.Schemas.FirstOrDefault(x => x.Value == schemaId).Key ?? string.Empty); + + return persistence.WriteSnapshotAsync(state); + } + + public Task GetSchemaIdAsync(string name) + { + state.Schemas.TryGetValue(name, out var schemaId); + + return Task.FromResult(schemaId); + } + + public Task> GetSchemaIdsAsync() + { + return Task.FromResult(state.Schemas.Values.ToList()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index bc148430b..5f09910e6 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -15,11 +15,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Schemas.State { - public class SchemaState : DomainObjectState, - ISchemaEntity + [CollectionName("Schemas")] + public class SchemaState : DomainObjectState, ISchemaEntity { [JsonProperty] public NamedId AppId { get; set; } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 19f641f38..873f49a1c 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -5,7 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson; using MongoDB.Driver; using Newtonsoft.Json; using Squidex.Infrastructure.MongoDb; @@ -26,7 +28,11 @@ namespace Squidex.Infrastructure.States protected override string CollectionName() { - return $"States_{typeof(T).Name}"; + var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); + + var name = attribute?.Name ?? typeof(T).Name; + + return $"States_{name}"; } public async Task<(T Value, long Version)> ReadAsync(TKey key) @@ -47,5 +53,10 @@ namespace Squidex.Infrastructure.States { return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); } + + public Task ReadAllAsync(System.Func callback) + { + return Collection.Find(new BsonDocument()).ForEachAsync(x => callback(x.Doc, x.Version)); + } } } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index f85bf59ab..bbea29936 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -11,6 +11,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { + [BsonIgnoreExtraElements] public sealed class MongoState : IVersionedEntity { [BsonId] diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs index adbca58e7..6e3da7063 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs @@ -7,6 +7,7 @@ using System; using Orleans; +using Squidex.Infrastructure.Orleans; namespace Squidex.Infrastructure.EventSourcing.Grains { @@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains eventConsumerManagerGrain = new Lazy(() => { - return factory.GetGrain("Default"); + return factory.GetGrain(SingleGrain.Id); }); } diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs index 254d31b49..5a3bb7a19 100644 --- a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs +++ b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.Orleans { try { - var grain = grainFactory.GetGrain("Default"); + var grain = grainFactory.GetGrain(SingleGrain.Id); await grain.ActivateAsync(); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Infrastructure/Orleans/SingleGrain.cs similarity index 53% rename from src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs rename to src/Squidex.Infrastructure/Orleans/SingleGrain.cs index 41cbfde78..66a9fb356 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs +++ b/src/Squidex.Infrastructure/Orleans/SingleGrain.cs @@ -1,18 +1,14 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Entities.Rules.Repositories +namespace Squidex.Infrastructure.Orleans { - public interface IRuleRepository + public static class SingleGrain { - Task> QueryRuleIdsAsync(Guid appId); + public const string Id = "Default"; } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs b/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs similarity index 57% rename from src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs rename to src/Squidex.Infrastructure/States/CollectionNameAttribute.cs index 0cd2e4257..647420a4b 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentScheduleItem.cs +++ b/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs @@ -5,18 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; +using System; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Infrastructure.States { - public interface IContentScheduleItem + [AttributeUsage(AttributeTargets.Class)] + public sealed class CollectionNameAttribute : Attribute { - Status ScheduledTo { get; } + public string Name { get; } - Instant ScheduledAt { get; } - - RefToken ScheduledBy { get; } + public CollectionNameAttribute(string name) + { + Name = name; + } } } diff --git a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs index 500a44dbf..b2c00d6b7 100644 --- a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs +++ b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs @@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States { public sealed class DefaultStreamNameResolver : IStreamNameResolver { - private static readonly string[] Suffixes = { "Grain", "DomainObject" }; + private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; public string GetStreamName(Type aggregateType, string id) { diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index d20fc7437..60b2ab5be 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Infrastructure.States @@ -16,5 +17,7 @@ namespace Squidex.Infrastructure.States Task<(T Value, long Version)> ReadAsync(TKey key); Task ClearAsync(); + + Task ReadAllAsync(Func callback); } } diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index 8265d5835..fd5abdeae 100644 --- a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -13,6 +13,7 @@ using Orleans; using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Orleans; using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.EventConsumers @@ -28,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory) : base(commandBus) { - eventConsumerManagerGrain = grainFactory.GetGrain("Default"); + eventConsumerManagerGrain = grainFactory.GetGrain(SingleGrain.Id); } [HttpGet] diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 689991790..e37d1b9ba 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Backup; @@ -28,7 +29,9 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; @@ -116,25 +119,22 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - - services.AddSingleton>(DomainObjectGrainFormatter.Format); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingleton>(DomainObjectGrainFormatter.Format); services.AddSingleton(c => { @@ -172,9 +172,15 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .As(); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index f4f8e56ba..b2592dae5 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -12,33 +12,22 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps.Repositories; -using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; -using Squidex.Domain.Apps.Entities.Backup.State; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Entities.MongoDb.Apps; using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.History; using Squidex.Domain.Apps.Entities.MongoDb.Rules; -using Squidex.Domain.Apps.Entities.MongoDb.Schemas; using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; -using Squidex.Domain.Apps.Entities.Schemas.State; using Squidex.Domain.Users; using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb.Infrastructure; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; @@ -62,63 +51,55 @@ namespace Squidex.Config.Domain var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName); - services.AddSingletonAs(c => new MongoXmlRepository(mongoDatabase)) - .As() - .As(); + services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); - services.AddSingletonAs(c => new MongoMigrationStatus(mongoDatabase)) - .As() - .As(); + services.AddSingletonAs(mongoDatabase) + .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As>() + services.AddSingletonAs() + .As() .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As>() + services.AddSingletonAs() + .As() .As(); - services.AddSingletonAs(c => new MongoPersistedGrantStore(mongoDatabase)) + services.AddSingletonAs() .As() .As(); - services.AddSingletonAs(c => new MongoUsageStore(mongoDatabase)) + services.AddSingletonAs() .As() .As(); - services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) + services.AddSingletonAs() .As() .As(); - services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) + services.AddSingletonAs() .As>() .As() .As() .As(); - services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase)) + services.AddSingletonAs() .As>() .As() .As(); - services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) - .As() - .As>() - .As(); - - services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) - .As() - .As>() + services.AddSingletonAs() + .As() + .As() .As(); - services.AddSingletonAs(c => new MongoRuleRepository(mongoDatabase)) - .As() - .As>() + services.AddSingletonAs() + .As() + .As() .As(); - services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) - .As() - .As>() + services.AddSingletonAs() + .As() + .As>() .As(); services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) @@ -126,16 +107,6 @@ namespace Squidex.Config.Domain .As>() .As() .As(); - - services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) - .As() - .As() - .As(); - - services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase)) - .As() - .As() - .As(); } }); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs index 4c3e54e11..16645f738 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs @@ -7,6 +7,7 @@ using FakeItEasy; using Orleans; +using Squidex.Infrastructure.Orleans; using Xunit; namespace Squidex.Infrastructure.EventSourcing.Grains @@ -20,7 +21,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { var factory = A.Fake(); - A.CallTo(() => factory.GetGrain("Default", null)) + A.CallTo(() => factory.GetGrain(SingleGrain.Id, null)) .Returns(manager); sut = new OrleansEventNotifier(factory); diff --git a/tools/Migrate_01/MigrationPath.cs b/tools/Migrate_01/MigrationPath.cs index 81eb97d0d..ce17926c9 100644 --- a/tools/Migrate_01/MigrationPath.cs +++ b/tools/Migrate_01/MigrationPath.cs @@ -15,7 +15,7 @@ namespace Migrate_01 { public sealed class MigrationPath : IMigrationPath { - private const int CurrentVersion = 8; + private const int CurrentVersion = 9; private readonly IServiceProvider serviceProvider; public MigrationPath(IServiceProvider serviceProvider) @@ -50,13 +50,24 @@ namespace Migrate_01 migrations.Add(serviceProvider.GetRequiredService()); } + // Version 9: Grain Indexes + if (version < 9) + { + migrations.Add(serviceProvider.GetRequiredService()); + migrations.Add(serviceProvider.GetRequiredService()); + } + // Version 1: Introduce App patterns. if (version <= 1) { migrations.Add(serviceProvider.GetRequiredService()); } - migrations.Add(serviceProvider.GetRequiredService()); + // Version 8: Introduce Archive collection. + if (version < 8) + { + migrations.Add(serviceProvider.GetRequiredService()); + } return (CurrentVersion, migrations); } diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs index f07b2d20f..523c90d59 100644 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -10,8 +10,8 @@ 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.Repositories; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Orleans; namespace Migrate_01.Migrations { @@ -19,18 +19,16 @@ namespace Migrate_01.Migrations { private readonly InitialPatterns initialPatterns; private readonly IGrainFactory grainFactory; - private readonly IAppRepository appRepository; - public AddPatterns(InitialPatterns initialPatterns, IAppRepository appRepository, IGrainFactory grainFactory) + public AddPatterns(InitialPatterns initialPatterns, IGrainFactory grainFactory) { this.initialPatterns = initialPatterns; - this.appRepository = appRepository; this.grainFactory = grainFactory; } public async Task UpdateAsync() { - var ids = await appRepository.QueryAppIdsAsync(); + var ids = await grainFactory.GetGrain(SingleGrain.Id).GetAppIdAsync(); foreach (var id in ids) { diff --git a/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs b/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs new file mode 100644 index 000000000..4528f8e5b --- /dev/null +++ b/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.Migrations; + +namespace Migrate_01.Migrations +{ + public sealed class ConvertOldSnapshotStores : IMigration + { + private readonly IMongoDatabase database; + + public ConvertOldSnapshotStores(IMongoDatabase database) + { + this.database = database; + } + + public Task UpdateAsync() + { + var collections = new[] + { + "States_Apps", + "States_Rules", + "States_Schemas" + }; + + var update = Builders.Update.Rename("State", "Doc"); + + var filter = new BsonDocument(); + + return Task.WhenAll( + collections + .Select(x => database.GetCollection(x)) + .Select(x => x.UpdateManyAsync(filter, update))); + } + } +} diff --git a/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs new file mode 100644 index 000000000..49431c8aa --- /dev/null +++ b/tools/Migrate_01/Migrations/PopulateGrainIndexes.cs @@ -0,0 +1,127 @@ +// ========================================================================== +// 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; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Entities.Schemas; +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; + +namespace Migrate_01.Migrations +{ + public class PopulateGrainIndexes : IMigration + { + private readonly IGrainFactory grainFactory; + private readonly ISnapshotStore statesForApps; + private readonly ISnapshotStore statesForRules; + private readonly ISnapshotStore statesForSchemas; + + public PopulateGrainIndexes( + IGrainFactory grainFactory, + ISnapshotStore statesForApps, + ISnapshotStore statesForRules, + ISnapshotStore statesForSchemas) + { + this.grainFactory = grainFactory; + this.statesForApps = statesForApps; + this.statesForRules = statesForRules; + this.statesForSchemas = statesForSchemas; + } + + public Task UpdateAsync() + { + return Task.WhenAll( + RebuildAppIndexes(), + RebuildRuleIndexes(), + RebuildSchemaIndexes()); + } + + private async Task RebuildAppIndexes() + { + var appsByName = new Dictionary(); + var appsByUser = new Dictionary>(); + + await statesForApps.ReadAllAsync((app, version) => + { + if (!app.IsArchived) + { + appsByName[app.Name] = app.Id; + + foreach (var contributor in app.Contributors.Keys) + { + appsByUser.GetOrAddNew(contributor).Add(app.Id); + } + } + + return TaskHelper.Done; + }); + + var tasks = + appsByUser.Select(x => + grainFactory.GetGrain(x.Key).RebuildAsync(x.Value)) + .Union(new[] + { + grainFactory.GetGrain(SingleGrain.Id).RebuildAsync(appsByName) + }); + + await Task.WhenAll(tasks); + } + + private async Task RebuildRuleIndexes() + { + var schemasByApp = new Dictionary>(); + + await statesForRules.ReadAllAsync((schema, version) => + { + if (!schema.IsDeleted) + { + schemasByApp.GetOrAddNew(schema.AppId.Id).Add(schema.Id); + } + + return TaskHelper.Done; + }); + + var tasks = + schemasByApp.Select(x => + grainFactory.GetGrain(x.Key).RebuildAsync(x.Value)); + + await Task.WhenAll(tasks); + } + + private async Task RebuildSchemaIndexes() + { + var schemasByApp = new Dictionary>(); + + await statesForSchemas.ReadAllAsync((schema, version) => + { + if (!schema.IsDeleted) + { + schemasByApp.GetOrAddNew(schema.AppId.Id).Add(schema.Name, schema.Id); + } + + return TaskHelper.Done; + }); + + var tasks = + schemasByApp.Select(x => + grainFactory.GetGrain(x.Key).RebuildAsync(x.Value)); + + await Task.WhenAll(tasks); + } + } +} diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index 7d27b1861..ea14482cc 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.State; @@ -138,22 +139,13 @@ namespace Migrate_01 { var @event = ParseKnownEvent(storedEvent); - if (@event.Payload is ContentEvent contentEvent) + if (@event.Payload is ContentEvent contentEvent && handledIds.Add(contentEvent.ContentId)) { try { - var (content, version) = await snapshotContentStore.ReadAsync(contentEvent.ContentId); + var content = grainFactory.GetGrain(contentEvent.ContentId); - if (content == null) - { - version = EtagVersion.Empty; - - content = new ContentState(); - } - - content = content.Apply(@event); - - await snapshotContentStore.WriteAsync(contentEvent.ContentId, content, version, version + 1); + await content.WriteSnapshotAsync(); } catch (DomainObjectNotFoundException) {