From 206566efa4c8461515b3056b4287795c1d966f0f Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 4 Dec 2017 22:33:02 +0100 Subject: [PATCH 01/30] Domain objects moved. --- Squidex.sln | 15 + .../AppAggregateCommand.cs | 21 ++ .../AppCommand.cs | 18 ++ .../Apps/AppCommandMiddleware.cs | 173 ++++++++++++ .../Apps/AppDomainObject.cs | 229 +++++++++++++++ .../Apps/AppEntityExtensions.cs | 20 ++ .../Apps/AppHistoryEventsCreator.cs | 145 ++++++++++ .../Apps/Commands/AddLanguage.cs | 17 ++ .../Apps/Commands/AssignContributor.cs | 19 ++ .../Apps/Commands/AttachClient.cs | 19 ++ .../Apps/Commands/ChangePlan.cs | 17 ++ .../Apps/Commands/CreateApp.cs | 30 ++ .../Apps/Commands/RemoveContributor.cs | 15 + .../Apps/Commands/RemoveLanguage.cs | 17 ++ .../Apps/Commands/RevokeClient.cs | 15 + .../Apps/Commands/UpdateClient.cs | 21 ++ .../Apps/Commands/UpdateLanguage.cs | 24 ++ .../Apps/Guards/GuardApp.cs | 65 +++++ .../Apps/Guards/GuardAppClients.cs | 102 +++++++ .../Apps/Guards/GuardAppContributors.cs | 82 ++++++ .../Apps/Guards/GuardAppLanguages.cs | 100 +++++++ .../Apps/IAppEntity.cs | 25 ++ .../Apps/Services/IAppLimitsPlan.cs | 25 ++ .../Apps/Services/IAppPlanBillingManager.cs | 22 ++ .../Apps/Services/IAppPlansProvider.cs | 27 ++ .../Apps/Services/IChangePlanResult.cs | 14 + .../Implementations/ConfigAppLimitsPlan.cs | 30 ++ .../Implementations/ConfigAppPlansProvider.cs | 86 ++++++ .../NoopAppPlanBillingManager.cs | 31 +++ .../Apps/Services/PlanChangeAsyncResult.cs | 19 ++ .../Apps/Services/PlanChangedResult.cs | 19 ++ .../Apps/Services/RedirectToCheckoutResult.cs | 25 ++ .../Apps/State/AppState.cs | 31 +++ .../Assets/AssetCommandMiddleware.cs | 117 ++++++++ .../Assets/AssetDomainObject.cs | 107 +++++++ .../Assets/AssetSavedResult.cs | 23 ++ .../Assets/Commands/AssetAggregateCommand.cs | 23 ++ .../Assets/Commands/CreateAsset.cs | 25 ++ .../Assets/Commands/DeleteAsset.cs | 14 + .../Assets/Commands/RenameAsset.cs | 15 + .../Assets/Commands/UpdateAsset.cs | 19 ++ .../Assets/Guards/GuardAsset.cs | 49 ++++ .../Assets/IAssetEntity.cs | 25 ++ .../Assets/IAssetEventConsumer.cs | 16 ++ .../Assets/IAssetStatsEntity.cs | 21 ++ .../Assets/Repositories/IAssetRepository.cs | 23 ++ .../Repositories/IAssetStatsRepository.cs | 21 ++ .../Assets/State/AssetState.cs | 55 ++++ .../DomainObjectState.cs | 45 +++ .../EntityMapper.cs | 77 ++++++ .../History/HistoryEventToStore.cs | 44 +++ .../History/HistoryEventsCreatorBase.cs | 66 +++++ .../History/IHistoryEventEntity.cs | 24 ++ .../History/IHistoryEventsCreator.cs | 21 ++ .../Repositories/IHistoryEventRepository.cs | 19 ++ .../IAppProvider.cs | 34 +++ src/Squidex.Domain.Apps.Entities/IEntity.cs | 22 ++ .../IEntityWithAppRef.cs | 17 ++ .../IEntityWithCreatedBy.cs | 17 ++ .../IEntityWithLastModifiedBy.cs | 17 ++ .../IEntityWithVersion.cs | 15 + .../IUpdateableEntityWithAppRef.cs | 17 ++ .../IUpdateableEntityWithCreatedBy.cs | 17 ++ .../IUpdateableEntityWithLastModifiedBy.cs | 17 ++ .../IUpdateableEntityWithVersion.cs | 15 + .../Rules/Commands/CreateRule.cs | 20 ++ .../Rules/Commands/DeleteRule.cs | 14 + .../Rules/Commands/DisableRule.cs | 14 + .../Rules/Commands/EnableRule.cs | 14 + .../Rules/Commands/RuleAggregateCommand.cs | 23 ++ .../Rules/Commands/RuleEditCommand.cs | 19 ++ .../Rules/Commands/UpdateRule.cs | 14 + .../Rules/Guards/GuardRule.cs | 107 +++++++ .../Rules/Guards/RuleActionValidator.cs | 40 +++ .../Rules/Guards/RuleTriggerValidator.cs | 56 ++++ .../Rules/IRuleEntity.cs | 22 ++ .../Rules/IRuleEventEntity.cs | 29 ++ .../Repositories/IRuleEventRepository.cs | 35 +++ .../Rules/RuleCommandMiddleware.cs | 92 ++++++ .../Rules/RuleDequeuer.cs | 157 +++++++++++ .../Rules/RuleDomainObject.cs | 93 +++++++ .../Rules/RuleEnqueuer.cs | 73 +++++ .../Rules/RuleJobResult.cs | 18 ++ .../Rules/State/RuleState.cs | 26 ++ .../SchemaAggregateCommand.cs | 21 ++ .../SchemaCommand.cs | 18 ++ .../Schemas/Commands/AddField.cs | 21 ++ .../Schemas/Commands/ConfigureScripts.cs | 23 ++ .../Schemas/Commands/CreateSchema.cs | 36 +++ .../Schemas/Commands/CreateSchemaField.cs | 27 ++ .../Schemas/Commands/DeleteField.cs | 14 + .../Schemas/Commands/DeleteSchema.cs | 14 + .../Schemas/Commands/DisableField.cs | 14 + .../Schemas/Commands/EnableField.cs | 14 + .../Schemas/Commands/FieldCommand.cs | 15 + .../Schemas/Commands/HideField.cs | 14 + .../Schemas/Commands/LockField.cs | 14 + .../Schemas/Commands/PublishSchema.cs | 14 + .../Schemas/Commands/ReorderFields.cs | 17 ++ .../Schemas/Commands/ShowField.cs | 14 + .../Schemas/Commands/UnpublishSchema.cs | 14 + .../Schemas/Commands/UpdateField.cs | 17 ++ .../Schemas/Commands/UpdateSchema.cs | 17 ++ .../Guards/FieldPropertiesValidator.cs | 232 ++++++++++++++++ .../Schemas/Guards/GuardSchema.cs | 128 +++++++++ .../Schemas/Guards/GuardSchemaField.cs | 160 +++++++++++ .../Schemas/ISchemaEntity.cs | 38 +++ .../Schemas/SchemaCommandMiddleware.cs | 198 +++++++++++++ .../Schemas/SchemaDomainObject.cs | 261 ++++++++++++++++++ .../Schemas/SchemaHistoryEventsCreator.cs | 88 ++++++ .../Schemas/State/SchemaState.cs | 57 ++++ .../Squidex.Domain.Apps.Entities.csproj | 25 ++ .../SquidexCommand.cs | 23 ++ .../SquidexEvent.cs | 2 + .../Apps/Services/PlanChangeAsyncResult.cs | 1 + src/Squidex.Domain.Apps.Read/EntityMapper.cs | 2 +- .../Commands/AggregateHandler.cs | 27 +- .../Commands/CommandExtensions.cs | 4 +- .../Commands/DomainObjectBase.cs | 77 +++--- .../Commands/DomainObjectWrapper.cs | 55 ---- .../Commands/IAggregateHandler.cs | 4 +- .../Commands/IDomainObject.cs | 21 ++ 122 files changed, 5044 insertions(+), 118 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/AppCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs create mode 100644 src/Squidex.Domain.Apps.Entities/DomainObjectState.cs create mode 100644 src/Squidex.Domain.Apps.Entities/EntityMapper.cs create mode 100644 src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs create mode 100644 src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs create mode 100644 src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IAppProvider.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs create mode 100644 src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs create mode 100644 src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/SchemaCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj create mode 100644 src/Squidex.Domain.Apps.Entities/SquidexCommand.cs delete mode 100644 src/Squidex.Infrastructure/Commands/DomainObjectWrapper.cs create mode 100644 src/Squidex.Infrastructure/Commands/IDomainObject.cs diff --git a/Squidex.sln b/Squidex.sln index 21ac0f2e4..67e85c17a 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Op EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities", "src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj", "{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -315,6 +317,18 @@ Global {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.Build.0 = Release|Any CPU {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.ActiveCfg = Release|Any CPU {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.Build.0 = Release|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|x64.Build.0 = Debug|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|x86.Build.0 = Debug|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|Any CPU.Build.0 = Release|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x64.ActiveCfg = Release|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x64.Build.0 = Release|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x86.ActiveCfg = Release|Any CPU + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -344,6 +358,7 @@ Global {7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892} {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892} + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs new file mode 100644 index 000000000..1bd5d3a75 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// AppAggregateCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities +{ + public class AppAggregateCommand : AppCommand, IAggregateCommand + { + Guid IAggregateCommand.AggregateId + { + get { return AppId.Id; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/AppCommand.cs b/src/Squidex.Domain.Apps.Entities/AppCommand.cs new file mode 100644 index 000000000..9ab07df33 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/AppCommand.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// AppCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class AppCommand : SquidexCommand + { + public NamedId AppId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs new file mode 100644 index 000000000..ae6a2e3ce --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -0,0 +1,173 @@ +// ========================================================================== +// AppCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Guards; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Dispatching; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly IAppProvider appProvider; + private readonly IAppPlansProvider appPlansProvider; + private readonly IAppPlanBillingManager appPlansBillingManager; + private readonly IUserResolver userResolver; + + public AppCommandMiddleware( + IAggregateHandler handler, + IAppProvider appProvider, + IAppPlansProvider appPlansProvider, + IAppPlanBillingManager appPlansBillingManager, + IUserResolver userResolver) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(userResolver, nameof(userResolver)); + Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); + Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); + + this.handler = handler; + this.userResolver = userResolver; + this.appProvider = appProvider; + this.appPlansProvider = appPlansProvider; + this.appPlansBillingManager = appPlansBillingManager; + } + + protected Task On(CreateApp command, CommandContext context) + { + return handler.CreateAsync(context, async a => + { + await GuardApp.CanCreate(command, appProvider); + + a.Create(command); + + context.Complete(EntityCreatedResult.Create(a.State.Id, a.Version)); + }); + } + + protected Task On(AssignContributor command, CommandContext context) + { + return handler.UpdateAsync(context, async a => + { + await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan.PlanId)); + + a.AssignContributor(command); + }); + } + + protected Task On(RemoveContributor command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppContributors.CanRemove(a.State.Contributors, command); + + a.RemoveContributor(command); + }); + } + + protected Task On(AttachClient command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppClients.CanAttach(a.State.Clients, command); + + a.AttachClient(command); + }); + } + + protected Task On(UpdateClient command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppClients.CanUpdate(a.State.Clients, command); + + a.UpdateClient(command); + }); + } + + protected Task On(RevokeClient command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppClients.CanRevoke(a.State.Clients, command); + + a.RevokeClient(command); + }); + } + + protected Task On(AddLanguage command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppLanguages.CanAdd(a.State.LanguagesConfig, command); + + a.AddLanguage(command); + }); + } + + protected Task On(RemoveLanguage command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppLanguages.CanRemove(a.State.LanguagesConfig, command); + + a.RemoveLanguage(command); + }); + } + + protected Task On(UpdateLanguage command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppLanguages.CanUpdate(a.State.LanguagesConfig, command); + + a.UpdateLanguage(command); + }); + } + + protected Task On(ChangePlan command, CommandContext context) + { + return handler.UpdateAsync(context, async a => + { + GuardApp.CanChangePlan(command, a.State.Plan, appPlansProvider); + + if (command.FromCallback) + { + a.ChangePlan(command); + } + else + { + var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.State.Id, a.State.Name, command.PlanId); + + if (result is PlanChangedResult) + { + a.ChangePlan(command); + } + + context.Complete(result); + } + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs new file mode 100644 index 000000000..e3c1ef9b7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -0,0 +1,229 @@ +// ========================================================================== +// AppDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppDomainObject : DomainObjectBase + { + public AppDomainObject Create(CreateApp command) + { + ThrowIfCreated(); + + var appId = new NamedId(command.AppId, command.Name); + + UpdateState(command, s => s.Name = command.Name); + + RaiseEvent(SimpleMapper.Map(command, CreateInitalEvent(appId))); + RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); + RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId))); + + return this; + } + + public AppDomainObject UpdateLanguage(UpdateLanguage command) + { + ThrowIfNotCreated(); + + UpdateLanguages(command, l => + { + var fallback = command.Fallback; + + if (fallback != null && fallback.Count > 0) + { + var existingLangauges = l.OfType().Select(x => x.Language); + + fallback = fallback.Intersect(existingLangauges).ToList(); + } + + l = l.Set(new LanguageConfig(command.Language, command.IsOptional, fallback)); + + if (command.IsMaster) + { + l = l.MakeMaster(command.Language); + } + + return l; + }); + + RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); + + return this; + } + + public AppDomainObject UpdateClient(UpdateClient command) + { + ThrowIfNotCreated(); + + if (!string.IsNullOrWhiteSpace(command.Name)) + { + UpdateClients(command, c => c.Rename(command.Id, command.Name)); + + RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); + } + + if (command.Permission.HasValue) + { + UpdateClients(command, c => c.Update(command.Id, command.Permission.Value)); + + RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value })); + } + + return this; + } + + public AppDomainObject AssignContributor(AssignContributor command) + { + ThrowIfNotCreated(); + + UpdateContributors(command, c => c.Assign(command.ContributorId, command.Permission)); + + RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); + + return this; + } + + public AppDomainObject RemoveContributor(RemoveContributor command) + { + ThrowIfNotCreated(); + + UpdateContributors(command, c => c.Remove(command.ContributorId)); + + RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); + + return this; + } + + public AppDomainObject AttachClient(AttachClient command) + { + ThrowIfNotCreated(); + + UpdateClients(command, c => c.Add(command.Id, command.Secret)); + + RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); + + return this; + } + + public AppDomainObject RevokeClient(RevokeClient command) + { + ThrowIfNotCreated(); + + UpdateClients(command, c => c.Revoke(command.Id)); + + RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); + + return this; + } + + public AppDomainObject AddLanguage(AddLanguage command) + { + ThrowIfNotCreated(); + + UpdateLanguages(command, l => l.Set(new LanguageConfig(command.Language))); + + RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); + + return this; + } + + public AppDomainObject RemoveLanguage(RemoveLanguage command) + { + ThrowIfNotCreated(); + + UpdateLanguages(command, l => l.Remove(command.Language)); + + RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); + + return this; + } + + public AppDomainObject ChangePlan(ChangePlan command) + { + ThrowIfNotCreated(); + + UpdateState(command, s => s.Plan = new AppPlan(command.Actor, command.PlanId)); + + RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); + + return this; + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = new NamedId(State.Id, State.Name); + } + + RaiseEvent(Envelope.Create(@event)); + } + + private static AppCreated CreateInitalEvent(NamedId appId) + { + return new AppCreated { AppId = appId }; + } + + private static AppLanguageAdded CreateInitialLanguage(NamedId id) + { + return new AppLanguageAdded { AppId = id, Language = Language.EN }; + } + + private static AppContributorAssigned CreateInitialOwner(NamedId id, SquidexCommand command) + { + return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = AppContributorPermission.Owner }; + } + + private void ThrowIfNotCreated() + { + if (string.IsNullOrWhiteSpace(State.Name)) + { + throw new DomainException("App has not been created."); + } + } + + private void ThrowIfCreated() + { + if (!string.IsNullOrWhiteSpace(State.Name)) + { + throw new DomainException("App has already been created."); + } + } + + private void UpdateClients(ICommand command, Func updater) + { + UpdateState(command, s => s.Clients = updater(s.Clients)); + } + + private void UpdateContributors(ICommand command, Func updater) + { + UpdateState(command, s => s.Contributors = updater(s.Contributors)); + } + + private void UpdateLanguages(ICommand command, Func updater) + { + UpdateState(command, s => s.LanguagesConfig = updater(s.LanguagesConfig)); + } + + protected override AppState CloneState(ICommand command, Action updater) + { + return State.Clone().Update((SquidexCommand)command, updater); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs new file mode 100644 index 000000000..1b9013944 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// AppEntityExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public static class AppEntityExtensions + { + public static PartitionResolver PartitionResolver(this IAppEntity entity) + { + return entity.LanguagesConfig.ToResolver(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs new file mode 100644 index 000000000..2d2cdd628 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -0,0 +1,145 @@ +// ========================================================================== +// AppHistoryEventsCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppHistoryEventsCreator : HistoryEventsCreatorBase + { + public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "assigned {user:[Contributor]} as [Permission]"); + + AddEventMessage( + "removed {user:[Contributor]} from app"); + + AddEventMessage( + "added client {[Id]} to app"); + + AddEventMessage( + "revoked client {[Id]}"); + + AddEventMessage( + "updated client {[Id]}"); + + AddEventMessage( + "renamed client {[Id]} to {[Name]}"); + + AddEventMessage( + "added language {[Language]}"); + + AddEventMessage( + "removed language {[Language]}"); + + AddEventMessage( + "updated language {[Language]}"); + + AddEventMessage( + "changed master language to {[Language]}"); + } + + protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) + { + const string channel = "settings.contributors"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Contributor", @event.ContributorId)); + } + + protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers) + { + const string channel = "settings.contributors"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Contributor", @event.ContributorId).AddParameter("Permission", @event.Permission)); + } + + protected Task On(AppClientAttached @event, EnvelopeHeaders headers) + { + const string channel = "settings.clients"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Id", @event.Id)); + } + + protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) + { + const string channel = "settings.clients"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Id", @event.Id)); + } + + protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) + { + const string channel = "settings.clients"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Id", @event.Id).AddParameter("Name", ClientName(@event))); + } + + protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Language", @event.Language)); + } + + protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Language", @event.Language)); + } + + protected Task On(AppLanguageUpdated @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Language", @event.Language)); + } + + protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers) + { + const string channel = "settings.languages"; + + return Task.FromResult( + ForEvent(@event, channel) + .AddParameter("Language", @event.Language)); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null); + } + + private static string ClientName(AppClientRenamed @event) + { + return !string.IsNullOrWhiteSpace(@event.Name) ? @event.Name : @event.Id; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs new file mode 100644 index 000000000..486f8d0aa --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// AddLanguage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AddLanguage : AppAggregateCommand + { + public Language Language { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs new file mode 100644 index 000000000..0dad40b86 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// AssignContributor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AssignContributor : AppAggregateCommand + { + public string ContributorId { get; set; } + + public AppContributorPermission Permission { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs new file mode 100644 index 000000000..2cf0bdf39 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// AttachClient.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AttachClient : AppAggregateCommand + { + public string Id { get; set; } + + public string Secret { get; } = RandomHash.New(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs new file mode 100644 index 000000000..eab67f42e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ChangePlan.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class ChangePlan : AppAggregateCommand + { + public bool FromCallback { get; set; } + + public string PlanId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs new file mode 100644 index 000000000..eb173e4f5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// CreateApp.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class CreateApp : SquidexCommand, IAggregateCommand + { + public string Name { get; set; } + + public Guid AppId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return AppId; } + } + + public CreateApp() + { + AppId = Guid.NewGuid(); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs new file mode 100644 index 000000000..5bda9002b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RemoveContributor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class RemoveContributor : AppAggregateCommand + { + public string ContributorId { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs new file mode 100644 index 000000000..1e7fbf499 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RemoveLanguage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class RemoveLanguage : AppAggregateCommand + { + public Language Language { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs new file mode 100644 index 000000000..cf48c425f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RevokeClient.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class RevokeClient : AppAggregateCommand + { + public string Id { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs new file mode 100644 index 000000000..82059fbcd --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// UpdateClient.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdateClient : AppAggregateCommand + { + public string Id { get; set; } + + public string Name { get; set; } + + public AppClientPermission? Permission { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs new file mode 100644 index 000000000..d8badb558 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// UpdateLanguage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdateLanguage : AppAggregateCommand + { + public Language Language { get; set; } + + public bool IsOptional { get; set; } + + public bool IsMaster { get; set; } + + public List Fallback { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs new file mode 100644 index 000000000..ced905013 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// GuardApp.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardApp + { + public static Task CanCreate(CreateApp command, IAppProvider appProvider) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create app.", async error => + { + if (await appProvider.GetAppAsync(command.Name) != null) + { + error(new ValidationError($"An app with name '{command.Name}' already exists", nameof(command.Name))); + } + + if (!command.Name.IsSlug()) + { + error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); + } + }); + } + + public static void CanChangePlan(ChangePlan command, AppPlan plan, IAppPlansProvider appPlans) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot change plan.", error => + { + if (string.IsNullOrWhiteSpace(command.PlanId)) + { + error(new ValidationError("PlanId is not defined.", nameof(command.PlanId))); + } + else if (appPlans.GetPlan(command.PlanId) == null) + { + error(new ValidationError("Plan id not available.", nameof(command.PlanId))); + } + + if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) + { + error(new ValidationError("Plan can only be changed from current user.")); + } + + if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) + { + error(new ValidationError("App has already this plan.")); + } + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs new file mode 100644 index 000000000..1d48aa8d4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// GuardAppClients.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppClients + { + public static void CanAttach(AppClients clients, AttachClient command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot attach client.", error => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + error(new ValidationError("Client id must be defined.", nameof(command.Id))); + } + else if (clients.ContainsKey(command.Id)) + { + error(new ValidationError("Client id already added.", nameof(command.Id))); + } + }); + } + + public static void CanRevoke(AppClients clients, RevokeClient command) + { + Guard.NotNull(command, nameof(command)); + + GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot revoke client.", error => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + error(new ValidationError("Client id must be defined.", nameof(command.Id))); + } + }); + } + + public static void CanUpdate(AppClients clients, UpdateClient command) + { + Guard.NotNull(command, nameof(command)); + + var client = GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot revoke client.", error => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + error(new ValidationError("Client id must be defined.", nameof(command.Id))); + } + + if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null) + { + error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission))); + } + + if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue()) + { + error(new ValidationError("Permission is not valid.", nameof(command.Permission))); + } + + if (client != null) + { + if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) + { + error(new ValidationError("Client already has this name.", nameof(command.Permission))); + } + + if (command.Permission == client.Permission) + { + error(new ValidationError("Client already has this permission.", nameof(command.Permission))); + } + } + }); + } + + private static AppClient GetClientOrThrow(AppClients clients, string id) + { + if (id == null) + { + return null; + } + + if (!clients.TryGetValue(id, out var client)) + { + throw new DomainObjectNotFoundException(id, "Clients", typeof(AppDomainObject)); + } + + return client; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs new file mode 100644 index 000000000..bd0304d22 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// GuardAppContributors.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppContributors + { + public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot assign contributor.", async error => + { + if (!command.Permission.IsEnumValue()) + { + error(new ValidationError("Permission is not valid.", nameof(command.Permission))); + } + + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); + } + else + { + if (await users.FindByIdAsync(command.ContributorId) == null) + { + error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId))); + } + else if (contributors.TryGetValue(command.ContributorId, out var existing)) + { + if (existing == command.Permission) + { + error(new ValidationError("Contributor has already this permission.", nameof(command.Permission))); + } + } + else if (plan.MaxContributors == contributors.Count) + { + error(new ValidationError("You have reached the maximum number of contributors for your plan.")); + } + } + }); + } + + public static void CanRemove(AppContributors contributors, RemoveContributor command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot remove contributor.", error => + { + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); + } + + var ownerIds = contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList(); + + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) + { + error(new ValidationError("Cannot remove the only owner.", nameof(command.ContributorId))); + } + }); + + if (!contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppDomainObject)); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs new file mode 100644 index 000000000..a31fa3413 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// GuardAppLanguages.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppLanguages + { + public static void CanAdd(LanguagesConfig languages, AddLanguage command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add language.", error => + { + if (command.Language == null) + { + error(new ValidationError("Language cannot be null.", nameof(command.Language))); + } + else if (languages.Contains(command.Language)) + { + error(new ValidationError("Language already added.", nameof(command.Language))); + } + }); + } + + public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) + { + Guard.NotNull(command, nameof(command)); + + var languageConfig = GetLanguageConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot remove language.", error => + { + if (command.Language == null) + { + error(new ValidationError("Language cannot be null.", nameof(command.Language))); + } + + if (languages.Master == languageConfig) + { + error(new ValidationError("Language config is master.", nameof(command.Language))); + } + }); + } + + public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) + { + Guard.NotNull(command, nameof(command)); + + var languageConfig = GetLanguageConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot update language.", error => + { + if (command.Language == null) + { + error(new ValidationError("Language cannot be null.", nameof(command.Language))); + } + + if ((languages.Master == languageConfig || command.IsMaster) && command.IsOptional) + { + error(new ValidationError("Cannot make master language optional.", nameof(command.IsMaster))); + } + + if (command.Fallback != null) + { + foreach (var fallback in command.Fallback) + { + if (!languages.Contains(fallback)) + { + error(new ValidationError($"Config does not contain fallback language {fallback}.", nameof(command.Fallback))); + } + } + } + }); + } + + private static LanguageConfig GetLanguageConfigOrThrow(LanguagesConfig languages, Language language) + { + if (language == null) + { + return null; + } + + if (!languages.TryGetConfig(language, out var languageConfig)) + { + throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject)); + } + + return languageConfig; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs new file mode 100644 index 000000000..154478f1e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// IAppEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppEntity : IEntity, IEntityWithVersion + { + string Name { get; } + + AppPlan Plan { get; } + + AppClients Clients { get; } + + AppContributors Contributors { get; } + + LanguagesConfig LanguagesConfig { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs new file mode 100644 index 000000000..623d71f26 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// IAppLimitsPlan.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppLimitsPlan + { + string Id { get; } + + string Name { get; } + + string Costs { get; } + + long MaxApiCalls { get; } + + long MaxAssetSize { get; } + + int MaxContributors { get; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs new file mode 100644 index 000000000..d0c601d06 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IAppPlanBillingManager.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppPlanBillingManager + { + bool HasPortal { get; } + + Task ChangePlanAsync(string userId, Guid appId, string appName, string planId); + + Task GetPortalLinkAsync(string userId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs new file mode 100644 index 000000000..53dea5201 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// IAppPlansProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IAppPlansProvider + { + IEnumerable GetAvailablePlans(); + + bool IsConfiguredPlan(string planId); + + IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app); + + IAppLimitsPlan GetPlanUpgrade(string planId); + + IAppLimitsPlan GetPlanForApp(IAppEntity app); + + IAppLimitsPlan GetPlan(string planId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs new file mode 100644 index 000000000..c8cde7963 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// IChangePlanResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public interface IChangePlanResult + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs new file mode 100644 index 000000000..165c11bd2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// ConfigAppLimitsPlan.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class ConfigAppLimitsPlan : IAppLimitsPlan + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Costs { get; set; } + + public long MaxApiCalls { get; set; } + + public long MaxAssetSize { get; set; } + + public int MaxContributors { get; set; } + + public ConfigAppLimitsPlan Clone() + { + return (ConfigAppLimitsPlan)MemberwiseClone(); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs new file mode 100644 index 000000000..d27f8c189 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// ConfigAppPlansProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class ConfigAppPlansProvider : IAppPlansProvider + { + private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1 + }; + + private readonly Dictionary plansById; + private readonly List plansList; + + public ConfigAppPlansProvider(IEnumerable config) + { + Guard.NotNull(config, nameof(config)); + + plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList(); + plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable GetAvailablePlans() + { + return plansList; + } + + public bool IsConfiguredPlan(string planId) + { + return planId != null && plansById.ContainsKey(planId); + } + + public IAppLimitsPlan GetPlanForApp(IAppEntity app) + { + Guard.NotNull(app, nameof(app)); + + return GetPlan(app.Plan?.PlanId); + } + + public IAppLimitsPlan GetPlan(string planId) + { + return GetPlanCore(planId); + } + + public IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app) + { + Guard.NotNull(app, nameof(app)); + + return GetPlanUpgrade(app.Plan?.PlanId); + } + + public IAppLimitsPlan GetPlanUpgrade(string planId) + { + var plan = GetPlanCore(planId); + + var nextPlanIndex = plansList.IndexOf(plan); + + if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) + { + return plansList[nextPlanIndex + 1]; + } + + return null; + } + + private ConfigAppLimitsPlan GetPlanCore(string planId) + { + return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs new file mode 100644 index 000000000..6a053c294 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// NoopAppPlanBillingManager.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations +{ + public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager + { + public bool HasPortal + { + get { return false; } + } + + public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId) + { + return Task.FromResult(PlanChangedResult.Instance); + } + + public Task GetPortalLinkAsync(string userId) + { + return Task.FromResult(string.Empty); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs new file mode 100644 index 000000000..6c0c15865 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// PlanChangeAsyncResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public sealed class PlanChangeAsyncResult : IChangePlanResult + { + public static readonly PlanChangeAsyncResult Instance = new PlanChangeAsyncResult(); + + private PlanChangeAsyncResult() + { + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs new file mode 100644 index 000000000..efcc95b3d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// PlanChangedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public sealed class PlanChangedResult : IChangePlanResult + { + public static readonly PlanChangedResult Instance = new PlanChangedResult(); + + private PlanChangedResult() + { + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs new file mode 100644 index 000000000..5d1d2a441 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// RedirectToCheckoutResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Services +{ + public sealed class RedirectToCheckoutResult : IChangePlanResult + { + public Uri Url { get; } + + public RedirectToCheckoutResult(Uri url) + { + Guard.NotNull(url, nameof(url)); + + Url = url; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs new file mode 100644 index 000000000..b9c40344e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// AppState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Domain.Apps.Entities.Apps.State +{ + public sealed class AppState : DomainObjectState, IAppEntity + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public AppPlan Plan { get; set; } + + [JsonProperty] + public AppClients Clients { get; set; } = AppClients.Empty; + + [JsonProperty] + public AppContributors Contributors { get; set; } = AppContributors.Empty; + + [JsonProperty] + public LanguagesConfig LanguagesConfig { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs new file mode 100644 index 000000000..5f1fe9459 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// AssetCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Guards; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AssetCommandMiddleware( + IAggregateHandler handler, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(assetStore, nameof(assetStore)); + Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); + + this.handler = handler; + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + protected async Task On(CreateAsset command, CommandContext context) + { + command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); + try + { + var asset = await handler.CreateAsync(context, async a => + { + GuardAsset.CanCreate(command); + + a.Create(command); + + await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); + + context.Complete(EntityCreatedResult.Create(a.State.Id, a.Version)); + }); + + await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.State.Id.ToString(), asset.State.FileVersion, null); + } + finally + { + await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); + } + } + + protected async Task On(UpdateAsset command, CommandContext context) + { + command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); + + try + { + var asset = await handler.UpdateAsync(context, async a => + { + GuardAsset.CanUpdate(command); + + a.Update(command); + + await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); + + context.Complete(new AssetSavedResult(a.Version, a.State.FileVersion)); + }); + + await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.State.Id.ToString(), asset.State.FileVersion, null); + } + finally + { + await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); + } + } + + protected Task On(RenameAsset command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAsset.CanRename(command, a.State.FileName); + + a.Rename(command); + }); + } + + protected Task On(DeleteAsset command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAsset.CanDelete(command); + + a.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs new file mode 100644 index 000000000..b38838e99 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// AssetDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetDomainObject : DomainObjectBase + { + public AssetDomainObject Create(CreateAsset command) + { + VerifyNotCreated(); + + var @event = SimpleMapper.Map(command, new AssetCreated + { + FileName = command.File.FileName, + FileSize = command.File.FileSize, + FileVersion = State.FileVersion + 1, + MimeType = command.File.MimeType, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight, + IsImage = command.ImageInfo != null + }); + + UpdateState(command, s => SimpleMapper.Map(@event, s)); + + RaiseEvent(@event); + + return this; + } + + public AssetDomainObject Update(UpdateAsset command) + { + VerifyCreatedAndNotDeleted(); + + var @event = SimpleMapper.Map(command, new AssetUpdated + { + FileVersion = State.FileVersion + 1, + FileSize = command.File.FileSize, + MimeType = command.File.MimeType, + PixelWidth = command.ImageInfo?.PixelWidth, + PixelHeight = command.ImageInfo?.PixelHeight, + IsImage = command.ImageInfo != null + }); + + UpdateState(command, s => SimpleMapper.Map(@event, s)); + + RaiseEvent(@event); + + return this; + } + + public AssetDomainObject Delete(DeleteAsset command) + { + VerifyCreatedAndNotDeleted(); + + UpdateState(command, s => s.IsDeleted = true); + + RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = State.TotalSize })); + + return this; + } + + public AssetDomainObject Rename(RenameAsset command) + { + VerifyCreatedAndNotDeleted(); + + UpdateState(command, s => s.FileName = command.FileName); + + RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); + + return this; + } + + private void VerifyNotCreated() + { + if (!string.IsNullOrWhiteSpace(State.FileName)) + { + throw new DomainException("Asset has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (State.IsDeleted || string.IsNullOrWhiteSpace(State.FileName)) + { + throw new DomainException("Asset has already been deleted or not created yet."); + } + } + + protected override AssetState CloneState(ICommand command, Action updater) + { + return State.Clone().Update((SquidexCommand)command, updater); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs new file mode 100644 index 000000000..129bc8379 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// AssetSavedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetSavedResult : EntitySavedResult + { + public long FileVersion { get; } + + public AssetSavedResult(long version, long fileVersion) + : base(version) + { + FileVersion = fileVersion; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs new file mode 100644 index 000000000..001cf574b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// AssetAggregateCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public abstract class AssetAggregateCommand : AppCommand, IAggregateCommand + { + public Guid AssetId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return AssetId; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs new file mode 100644 index 000000000..7ac46f525 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// CreateAsset.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class CreateAsset : AssetAggregateCommand + { + public AssetFile File { get; set; } + + public ImageInfo ImageInfo { get; set; } + + public CreateAsset() + { + AssetId = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs new file mode 100644 index 000000000..1dca53261 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteAsset.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class DeleteAsset : AssetAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs new file mode 100644 index 000000000..bc5a278f8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RenameAsset.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class RenameAsset : AssetAggregateCommand + { + public string FileName { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs new file mode 100644 index 000000000..750e0641c --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// UpdateAsset.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class UpdateAsset : AssetAggregateCommand + { + public AssetFile File { get; set; } + + public ImageInfo ImageInfo { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs new file mode 100644 index 000000000..2ac0d086d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// GuardAsset.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public static class GuardAsset + { + public static void CanRename(RenameAsset command, string oldName) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot rename asset.", error => + { + if (string.IsNullOrWhiteSpace(command.FileName)) + { + error(new ValidationError("Name must be defined.", nameof(command.FileName))); + } + + if (string.Equals(command.FileName, oldName)) + { + error(new ValidationError("Name is equal to old name.", nameof(command.FileName))); + } + }); + } + + public static void CanCreate(CreateAsset command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanUpdate(UpdateAsset command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanDelete(DeleteAsset command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs new file mode 100644 index 000000000..8563d3931 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// IAssetEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.ValidateContent; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetEntity : + IEntity, + IEntityWithAppRef, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion, + IAssetInfo + { + string MimeType { get; } + + long FileVersion { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs new file mode 100644 index 000000000..a188fead4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// IAssetEventConsumer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetEventConsumer : IEventConsumer + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs new file mode 100644 index 000000000..7edf5ddb8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IAssetStatsEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetStatsEntity + { + DateTime Date { get; } + + long TotalSize { get; } + + long TotalCount { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs new file mode 100644 index 000000000..10a16b6e3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// IAssetRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets.Repositories +{ + public interface IAssetRepository + { + Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0); + + Task FindAssetAsync(Guid id); + + Task CountAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs new file mode 100644 index 000000000..5980dc6c1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IAssetStatsRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets.Repositories +{ + public interface IAssetStatsRepository + { + Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate); + + Task GetTotalSizeAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs new file mode 100644 index 000000000..77e6ca5c9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// AssetState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.ValidateContent; + +namespace Squidex.Domain.Apps.Entities.Assets.State +{ + public sealed class AssetState : DomainObjectState, + IAssetEntity, + IAssetInfo, + IUpdateableEntityWithAppRef + { + [JsonProperty] + public Guid AppId { get; set; } + + [JsonProperty] + public string FileName { get; set; } + + [JsonProperty] + public string MimeType { get; set; } + + [JsonProperty] + public long FileVersion { get; set; } + + [JsonProperty] + public long FileSize { get; set; } + + [JsonProperty] + public long TotalSize { get; set; } + + [JsonProperty] + public bool IsImage { get; set; } + + [JsonProperty] + public int? PixelWidth { get; set; } + + [JsonProperty] + public int? PixelHeight { get; set; } + + [JsonProperty] + public bool IsDeleted { get; set; } + + Guid IAssetInfo.AssetId + { + get { return Id; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs new file mode 100644 index 000000000..c13e058d7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// DomainObjectState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class DomainObjectState : Cloneable, + IUpdateableEntityWithCreatedBy, + IUpdateableEntityWithLastModifiedBy, + IUpdateableEntityWithVersion + where T : Cloneable + { + [JsonProperty] + public Guid Id { get; set; } + + [JsonProperty] + public RefToken CreatedBy { get; set; } + + [JsonProperty] + public RefToken LastModifiedBy { get; set; } + + [JsonProperty] + public Instant Created { get; set; } + + [JsonProperty] + public Instant LastModified { get; set; } + + [JsonProperty] + public long Version { get; set; } + + public T Clone() + { + return Clone(x => { }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs new file mode 100644 index 000000000..d100f596a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// EntityMapper.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Domain.Apps.Entities +{ + public static class EntityMapper + { + public static T Update(this T entity, SquidexCommand command, Action updater = null) where T : IEntity + { + var timestamp = SystemClock.Instance.GetCurrentInstant(); + + SetAppId(entity, command); + SetVersion(entity); + SetCreated(entity, timestamp); + SetCreatedBy(entity, command); + SetLastModified(entity, timestamp); + SetLastModifiedBy(entity, command); + + updater?.Invoke(entity); + + return entity; + } + + private static void SetLastModified(IEntity entity, Instant timestamp) + { + entity.LastModified = timestamp; + } + + private static void SetCreated(IEntity entity, Instant timestamp) + { + if (entity.Created == default(Instant)) + { + entity.Created = timestamp; + } + } + + private static void SetVersion(IEntity entity) + { + if (entity is IUpdateableEntityWithVersion withVersion) + { + withVersion.Version++; + } + } + + private static void SetCreatedBy(IEntity entity, SquidexCommand command) + { + if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) + { + withCreatedBy.CreatedBy = command.Actor; + } + } + + private static void SetLastModifiedBy(IEntity entity, SquidexCommand command) + { + if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) + { + withModifiedBy.LastModifiedBy = command.Actor; + } + } + + private static void SetAppId(IEntity entity, SquidexCommand command) + { + if (entity is IUpdateableEntityWithAppRef appEntity && command is AppCommand appCommand) + { + appEntity.AppId = appCommand.AppId.Id; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs new file mode 100644 index 000000000..aeda6bc40 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// HistoryEventToStore.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryEventToStore + { + private readonly Dictionary parameters = new Dictionary(); + + public string Channel { get; } + + public string Message { get; } + + public IReadOnlyDictionary Parameters + { + get { return parameters; } + } + + public HistoryEventToStore(string channel, string message) + { + Guard.NotNullOrEmpty(channel, nameof(channel)); + Guard.NotNullOrEmpty(message, nameof(message)); + + Channel = channel; + + Message = message; + } + + public HistoryEventToStore AddParameter(string key, object value) + { + parameters[key] = value.ToString(); + + return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs new file mode 100644 index 000000000..e3a59eecd --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// HistoryEventsCreatorBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator + { + private readonly Dictionary texts = new Dictionary(); + private readonly TypeNameRegistry typeNameRegistry; + + public IReadOnlyDictionary Texts + { + get { return texts; } + } + + protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); + + this.typeNameRegistry = typeNameRegistry; + } + + protected void AddEventMessage(string message) where TEvent : IEvent + { + Guard.NotNullOrEmpty(message, nameof(message)); + + texts[typeNameRegistry.GetName()] = message; + } + + protected bool HasEventText(IEvent @event) + { + var message = typeNameRegistry.GetName(@event.GetType()); + + return texts.ContainsKey(message); + } + + protected HistoryEventToStore ForEvent(IEvent @event, string channel) + { + var message = typeNameRegistry.GetName(@event.GetType()); + + return new HistoryEventToStore(channel, message); + } + + public Task CreateEventAsync(Envelope @event) + { + if (HasEventText(@event.Payload)) + { + return CreateEventCoreAsync(@event); + } + + return Task.FromResult(null); + } + + protected abstract Task CreateEventCoreAsync(Envelope @event); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs new file mode 100644 index 000000000..ae201227e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// IHistoryEventEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public interface IHistoryEventEntity : IEntity + { + Guid EventId { get; } + + RefToken Actor { get; } + + string Message { get; } + + long Version { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs new file mode 100644 index 000000000..38fadac6b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IHistoryEventsCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public interface IHistoryEventsCreator + { + IReadOnlyDictionary Texts { get; } + + Task CreateEventAsync(Envelope @event); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs new file mode 100644 index 000000000..7652fbd28 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// IHistoryEventRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.History.Repositories +{ + public interface IHistoryEventRepository + { + Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs new file mode 100644 index 000000000..404a62e78 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// IAppProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IAppProvider + { + Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id); + + Task GetAppAsync(string appName); + + Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false); + + Task GetSchemaAsync(string appName, string name, bool provideDeleted = false); + + Task> GetSchemasAsync(string appName); + + Task> GetRulesAsync(string appName); + + Task> GetUserApps(string userId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IEntity.cs b/src/Squidex.Domain.Apps.Entities/IEntity.cs new file mode 100644 index 000000000..9aa8ea2b0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntity.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntity + { + Guid Id { get; set; } + + Instant Created { get; set; } + + Instant LastModified { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs new file mode 100644 index 000000000..2b7549767 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IEntityWithAppRef.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithAppRef + { + Guid AppId { get; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs new file mode 100644 index 000000000..a8ff4bb95 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IEntityWithCreatedBy.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithCreatedBy + { + RefToken CreatedBy { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs new file mode 100644 index 000000000..d5704ac7a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IEntityWithLastModifiedBy.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithLastModifiedBy + { + RefToken LastModifiedBy { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs new file mode 100644 index 000000000..9ec4e7fa9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IEntityWithVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithVersion + { + long Version { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs new file mode 100644 index 000000000..aa3b58226 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IUpdateableEntityWithAppRef.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IUpdateableEntityWithAppRef + { + Guid AppId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs new file mode 100644 index 000000000..8f07b421d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IUpdateableEntityWithCreatedBy.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IUpdateableEntityWithCreatedBy + { + RefToken CreatedBy { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs new file mode 100644 index 000000000..c57cf3d75 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IUpdateableEntityWithLastModifiedBy.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IUpdateableEntityWithLastModifiedBy + { + RefToken LastModifiedBy { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs new file mode 100644 index 000000000..229f7ea2d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IUpdateableEntityWithVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities +{ + public interface IUpdateableEntityWithVersion + { + long Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs new file mode 100644 index 000000000..1018ea503 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// CreateRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public sealed class CreateRule : RuleEditCommand + { + public CreateRule() + { + RuleId = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs new file mode 100644 index 000000000..96edbb03b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public sealed class DeleteRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs new file mode 100644 index 000000000..c4419b680 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DisableRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public sealed class DisableRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs new file mode 100644 index 000000000..8a4f9a746 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// EnableRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public sealed class EnableRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs new file mode 100644 index 000000000..2b28578ad --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// RuleAggregateCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public abstract class RuleAggregateCommand : AppCommand, IAggregateCommand + { + public Guid RuleId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return RuleId; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs new file mode 100644 index 000000000..548e34a9e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// RuleEditCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public abstract class RuleEditCommand : RuleAggregateCommand + { + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs new file mode 100644 index 000000000..841248dcc --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UpdateRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Rules.Commands +{ + public sealed class UpdateRule : RuleEditCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs new file mode 100644 index 000000000..ccf3023f1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// GuardRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public static class GuardRule + { + public static Task CanCreate(CreateRule command, IAppProvider appProvider) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create rule.", async error => + { + if (command.Trigger == null) + { + error(new ValidationError("Trigger must be defined.", nameof(command.Trigger))); + } + else + { + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); + + errors.Foreach(error); + } + + if (command.Action == null) + { + error(new ValidationError("Trigger must be defined.", nameof(command.Action))); + } + else + { + var errors = await RuleActionValidator.ValidateAsync(command.Action); + + errors.Foreach(error); + } + }); + } + + public static Task CanUpdate(UpdateRule command, IAppProvider appProvider) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot update rule.", async error => + { + if (command.Trigger == null && command.Action == null) + { + error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action))); + } + + if (command.Trigger != null) + { + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); + + errors.Foreach(error); + } + + if (command.Action != null) + { + var errors = await RuleActionValidator.ValidateAsync(command.Action); + + errors.Foreach(error); + } + }); + } + + public static void CanEnable(EnableRule command, Rule rule) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot enable rule.", error => + { + if (rule.IsEnabled) + { + error(new ValidationError("Rule is already enabled.")); + } + }); + } + + public static void CanDisable(DisableRule command, Rule rule) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot disable rule.", error => + { + if (!rule.IsEnabled) + { + error(new ValidationError("Rule is already disabled.")); + } + }); + } + + public static void CanDelete(DeleteRule command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs new file mode 100644 index 000000000..c1a9578d3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// RuleActionValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public sealed class RuleActionValidator : IRuleActionVisitor>> + { + public static Task> ValidateAsync(RuleAction action) + { + Guard.NotNull(action, nameof(action)); + + var visitor = new RuleActionValidator(); + + return action.Accept(visitor); + } + + public Task> Visit(WebhookAction action) + { + var errors = new List(); + + if (action.Url == null || !action.Url.IsAbsoluteUri) + { + errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); + } + + return Task.FromResult>(errors); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs new file mode 100644 index 000000000..dbe01af6f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -0,0 +1,56 @@ +// ========================================================================== +// RuleTriggerValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> + { + public Func> SchemaProvider { get; } + + public RuleTriggerValidator(Func> schemaProvider) + { + SchemaProvider = schemaProvider; + } + + public static Task> ValidateAsync(string appName, RuleTrigger action, IAppProvider appProvider) + { + Guard.NotNull(action, nameof(action)); + Guard.NotNull(appProvider, nameof(appProvider)); + + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appName, x)); + + return action.Accept(visitor); + } + + public async Task> Visit(ContentChangedTrigger trigger) + { + if (trigger.Schemas != null) + { + var schemaErrors = await Task.WhenAll( + trigger.Schemas.Select(async s => + await SchemaProvider(s.SchemaId) == null + ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) + : null)); + + return schemaErrors.Where(x => x != null).ToList(); + } + + return new List(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs new file mode 100644 index 000000000..a358e1c0e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public interface IRuleEntity : + IEntity, + IEntityWithAppRef, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + Rule RuleDef { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs new file mode 100644 index 000000000..7b499253f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// IRuleEventEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public interface IRuleEventEntity : IEntity + { + RuleJob Job { get; } + + Instant? NextAttempt { get; } + + RuleJobResult JobResult { get; } + + RuleResult Result { get; } + + int NumCalls { get; } + + string LastDump { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs new file mode 100644 index 000000000..7bf7acb45 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// IRuleEventRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules.Repositories +{ + public interface IRuleEventRepository + { + Task EnqueueAsync(RuleJob job, Instant nextAttempt); + + Task EnqueueAsync(Guid id, Instant nextAttempt); + + Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); + + Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)); + + Task CountByAppAsync(Guid appId); + + Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); + + Task FindAsync(Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs new file mode 100644 index 000000000..8fe0e34f7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// RuleCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Guards; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly IAppProvider appProvider; + + public RuleCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(appProvider, nameof(appProvider)); + + this.handler = handler; + + this.appProvider = appProvider; + } + + protected Task On(CreateRule command, CommandContext context) + { + return handler.CreateAsync(context, async w => + { + await GuardRule.CanCreate(command, appProvider); + + w.Create(command); + }); + } + + protected Task On(UpdateRule command, CommandContext context) + { + return handler.UpdateAsync(context, async c => + { + await GuardRule.CanUpdate(command, appProvider); + + c.Update(command); + }); + } + + protected Task On(EnableRule command, CommandContext context) + { + return handler.UpdateAsync(context, r => + { + GuardRule.CanEnable(command, r.State.RuleDef); + + r.Enable(command); + }); + } + + protected Task On(DisableRule command, CommandContext context) + { + return handler.UpdateAsync(context, r => + { + GuardRule.CanDisable(command, r.State.RuleDef); + + r.Disable(command); + }); + } + + protected Task On(DeleteRule command, CommandContext context) + { + return handler.UpdateAsync(context, c => + { + GuardRule.CanDelete(command); + + c.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs new file mode 100644 index 000000000..a3e07ea47 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs @@ -0,0 +1,157 @@ +// ========================================================================== +// RuleDequeuer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem + { + private readonly ActionBlock requestBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly RuleService ruleService; + private readonly CompletionTimer timer; + private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); + private readonly IClock clock; + private readonly ISemanticLog log; + + public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); + Guard.NotNull(clock, nameof(clock)); + Guard.NotNull(log, nameof(log)); + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + + this.clock = clock; + + this.log = log; + + requestBlock = + new ActionBlock(HandleAsync, + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); + + timer = new CompletionTimer(5000, QueryAsync); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + timer.StopAsync().Wait(); + + requestBlock.Complete(); + requestBlock.Completion.Wait(); + } + } + + public void Connect() + { + } + + public void Next() + { + timer.SkipCurrentDelay(); + } + + private async Task QueryAsync(CancellationToken cancellationToken) + { + try + { + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, cancellationToken); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "QueueWebhookEvents") + .WriteProperty("status", "Failed")); + } + } + + public async Task HandleAsync(IRuleEventEntity @event) + { + if (!executing.TryAdd(@event.Id, false)) + { + return; + } + + try + { + var job = @event.Job; + + var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData); + + var jobInvoke = ComputeJobInvoke(response.Result, @event, job); + var jobResult = ComputeJobResult(response.Result, jobInvoke); + + await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, jobInvoke); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "SendWebhookEvent") + .WriteProperty("status", "Failed")); + } + finally + { + executing.TryRemove(@event.Id, out var value); + } + } + + private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) + { + if (result != RuleResult.Success && !nextCall.HasValue) + { + return RuleJobResult.Failed; + } + else if (result != RuleResult.Success && nextCall.HasValue) + { + return RuleJobResult.Retry; + } + else + { + return RuleJobResult.Success; + } + } + + private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) + { + if (result != RuleResult.Success) + { + switch (@event.NumCalls) + { + case 0: + return job.Created.Plus(Duration.FromMinutes(5)); + case 1: + return job.Created.Plus(Duration.FromHours(1)); + case 2: + return job.Created.Plus(Duration.FromHours(6)); + case 3: + return job.Created.Plus(Duration.FromHours(12)); + } + } + + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs new file mode 100644 index 000000000..80522b6d5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// RuleDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleDomainObject : DomainObjectBase + { + public void Create(CreateRule command) + { + VerifyNotCreated(); + + UpdateRule(command, r => new Rule(command.Trigger, command.Action)); + + RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + } + + public void Update(UpdateRule command) + { + VerifyCreatedAndNotDeleted(); + + UpdateRule(command, r => r.Update(command.Trigger).Update(command.Action)); + + RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + } + + public void Enable(EnableRule command) + { + VerifyCreatedAndNotDeleted(); + + UpdateRule(command, r => r.Enable()); + + RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + } + + public void Disable(DisableRule command) + { + VerifyCreatedAndNotDeleted(); + + UpdateRule(command, r => r.Disable()); + + RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + } + + public void Delete(DeleteRule command) + { + VerifyCreatedAndNotDeleted(); + + UpdateState(command, s => s.IsDeleted = true); + + RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + } + + private void VerifyNotCreated() + { + if (State.RuleDef != null) + { + throw new DomainException("Webhook has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (State.IsDeleted || State.RuleDef == null) + { + throw new DomainException("Webhook has already been deleted or not created yet."); + } + } + + private void UpdateRule(ICommand command, Func updater) + { + UpdateState(command, s => s.RuleDef = updater(s.RuleDef)); + } + + protected override RuleState CloneState(ICommand command, Action updater) + { + return State.Clone().Update((SquidexCommand)command, updater); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs new file mode 100644 index 000000000..31caed52d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// RuleEnqueuer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleEnqueuer : IEventConsumer + { + private readonly IRuleEventRepository ruleEventRepository; + private readonly IAppProvider appProvider; + private readonly RuleService ruleService; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public RuleEnqueuer( + IRuleEventRepository ruleEventRepository, IAppProvider appProvider, + RuleService ruleService) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); + + Guard.NotNull(appProvider, nameof(appProvider)); + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + + this.appProvider = appProvider; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name); + + foreach (var ruleEntity in rules) + { + var job = ruleService.CreateJob(ruleEntity.RuleDef, @event); + + if (job != null) + { + await ruleEventRepository.EnqueueAsync(job, job.Created); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs new file mode 100644 index 000000000..79dde166a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// RuleJobResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public enum RuleJobResult + { + Pending, + Success, + Retry, + Failed + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs new file mode 100644 index 000000000..c41d1761d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// RuleState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Entities.Rules.State +{ + public sealed class RuleState : DomainObjectState, IRuleEntity + { + [JsonProperty] + public Guid AppId { get; set; } + + [JsonProperty] + public Rule RuleDef { get; set; } + + [JsonProperty] + public bool IsDeleted { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs new file mode 100644 index 000000000..72bc2990a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// SchemaAggregateCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class SchemaAggregateCommand : SchemaCommand, IAggregateCommand + { + Guid IAggregateCommand.AggregateId + { + get { return SchemaId.Id; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/SchemaCommand.cs b/src/Squidex.Domain.Apps.Entities/SchemaCommand.cs new file mode 100644 index 000000000..d962f9931 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/SchemaCommand.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// SchemaCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class SchemaCommand : AppCommand + { + public NamedId SchemaId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs new file mode 100644 index 000000000..40fea2376 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// AddField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class AddField : SchemaAggregateCommand + { + public string Name { get; set; } + + public string Partitioning { get; set; } + + public FieldProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs new file mode 100644 index 000000000..685006d55 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// ConfigureScripts.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class ConfigureScripts : SchemaAggregateCommand + { + public string ScriptQuery { get; set; } + + public string ScriptCreate { get; set; } + + public string ScriptUpdate { get; set; } + + public string ScriptDelete { get; set; } + + public string ScriptChange { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs new file mode 100644 index 000000000..ec389bc7b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// CreateSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Commands; +using SchemaFields = System.Collections.Generic.List; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class CreateSchema : AppCommand, IAggregateCommand + { + public Guid SchemaId { get; set; } + + public SchemaFields Fields { get; set; } + + public SchemaProperties Properties { get; set; } + + public string Name { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return SchemaId; } + } + + public CreateSchema() + { + SchemaId = Guid.NewGuid(); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs new file mode 100644 index 000000000..49c84f3ea --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// CreateSchemaField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class CreateSchemaField + { + public string Partitioning { get; set; } + + public string Name { get; set; } + + public bool IsHidden { get; set; } + + public bool IsLocked { get; set; } + + public bool IsDisabled { get; set; } + + public FieldProperties Properties { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs new file mode 100644 index 000000000..cc8e5f572 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class DeleteField : FieldCommand + { + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs new file mode 100644 index 000000000..c5d61e764 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class DeleteSchema : SchemaAggregateCommand + { + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs new file mode 100644 index 000000000..7ab6a58df --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DisableField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class DisableField : FieldCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs new file mode 100644 index 000000000..7905de7d8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// EnableField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class EnableField : FieldCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs new file mode 100644 index 000000000..54396bec6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// FieldCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public class FieldCommand : SchemaAggregateCommand + { + public long FieldId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs new file mode 100644 index 000000000..96a65bc88 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// HideField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class HideField : FieldCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs new file mode 100644 index 000000000..705221a9a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// LockField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class LockField : FieldCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs new file mode 100644 index 000000000..4bf22c436 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// PublishSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class PublishSchema : SchemaAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs new file mode 100644 index 000000000..af23659f6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ReorderFields.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class ReorderFields : SchemaAggregateCommand + { + public List FieldIds { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs new file mode 100644 index 000000000..7e93cb85f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// ShowField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class ShowField : FieldCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs new file mode 100644 index 000000000..ace984eb1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UnpublishSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class UnpublishSchema : SchemaAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs new file mode 100644 index 000000000..6756cd4ba --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// UpdateField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class UpdateField : FieldCommand + { + public FieldProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs new file mode 100644 index 000000000..0f0f49252 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// UpdateSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class UpdateSchema : SchemaAggregateCommand + { + public SchemaProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs new file mode 100644 index 000000000..0102e441f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -0,0 +1,232 @@ +// ========================================================================== +// FieldpropertiesValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public sealed class FieldPropertiesValidator : IFieldPropertiesVisitor> + { + private static readonly FieldPropertiesValidator Instance = new FieldPropertiesValidator(); + + private FieldPropertiesValidator() + { + } + + public static IEnumerable Validate(FieldProperties properties) + { + return properties?.Accept(Instance) ?? Enumerable.Empty(); + } + + public IEnumerable Visit(AssetsFieldProperties properties) + { + if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value) + { + yield return new ValidationError("Max items must be greater than min items.", + nameof(properties.MinItems), + nameof(properties.MaxItems)); + } + + if (properties.MaxHeight.HasValue && properties.MinHeight.HasValue && properties.MinHeight.Value >= properties.MaxHeight.Value) + { + yield return new ValidationError("Max height must be greater than min height.", + nameof(properties.MaxHeight), + nameof(properties.MinHeight)); + } + + if (properties.MaxWidth.HasValue && properties.MinWidth.HasValue && properties.MinWidth.Value >= properties.MaxWidth.Value) + { + yield return new ValidationError("Max width must be greater than min width.", + nameof(properties.MaxWidth), + nameof(properties.MinWidth)); + } + + if (properties.MaxSize.HasValue && properties.MinSize.HasValue && properties.MinSize.Value >= properties.MaxSize.Value) + { + yield return new ValidationError("Max size must be greater than min size.", + nameof(properties.MaxSize), + nameof(properties.MinSize)); + } + + if (properties.AspectWidth.HasValue != properties.AspectHeight.HasValue) + { + yield return new ValidationError("Aspect width and height must be defined.", + nameof(properties.AspectWidth), + nameof(properties.AspectHeight)); + } + } + + public IEnumerable Visit(BooleanFieldProperties properties) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError("Editor is not a valid value.", + nameof(properties.Editor)); + } + } + + public IEnumerable Visit(DateTimeFieldProperties properties) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError("Editor is not a valid value.", + nameof(properties.Editor)); + } + + if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) + { + yield return new ValidationError("Default value must be greater than min value.", + nameof(properties.DefaultValue)); + } + + if (properties.DefaultValue.HasValue && properties.MaxValue.HasValue && properties.DefaultValue.Value > properties.MaxValue.Value) + { + yield return new ValidationError("Default value must be less than max value.", + nameof(properties.DefaultValue)); + } + + if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue.Value >= properties.MaxValue.Value) + { + yield return new ValidationError("Max value must be greater than min value.", + nameof(properties.MinValue), + nameof(properties.MaxValue)); + } + + if (properties.CalculatedDefaultValue.HasValue) + { + if (!properties.CalculatedDefaultValue.Value.IsEnumValue()) + { + yield return new ValidationError("Calculated default value is not valid.", + nameof(properties.CalculatedDefaultValue)); + } + + if (properties.DefaultValue.HasValue) + { + yield return new ValidationError("Calculated default value and default value cannot be used together.", + nameof(properties.CalculatedDefaultValue), + nameof(properties.DefaultValue)); + } + } + } + + public IEnumerable Visit(GeolocationFieldProperties properties) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError("Editor is not a valid value.", + nameof(properties.Editor)); + } + } + + public IEnumerable Visit(JsonFieldProperties properties) + { + yield break; + } + + public IEnumerable Visit(NumberFieldProperties properties) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError("Editor is not a valid value.", + nameof(properties.Editor)); + } + + if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + { + yield return new ValidationError("Radio buttons or dropdown list need allowed values.", + nameof(properties.AllowedValues)); + } + + if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) + { + yield return new ValidationError("Default value must be greater than min value.", + nameof(properties.DefaultValue)); + } + + if (properties.DefaultValue.HasValue && properties.MaxValue.HasValue && properties.DefaultValue.Value > properties.MaxValue.Value) + { + yield return new ValidationError("Default value must be less than max value.", + nameof(properties.DefaultValue)); + } + + if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue.Value >= properties.MaxValue.Value) + { + yield return new ValidationError("Max value must be greater than min value.", + nameof(properties.MinValue), + nameof(properties.MaxValue)); + } + + if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) + { + yield return new ValidationError("Either allowed values or min and max value can be defined.", + nameof(properties.AllowedValues), + nameof(properties.MinValue), + nameof(properties.MaxValue)); + } + } + + public IEnumerable Visit(ReferencesFieldProperties properties) + { + if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value) + { + yield return new ValidationError("Max items must be greater than min items.", + nameof(properties.MinItems), + nameof(properties.MaxItems)); + } + } + + public IEnumerable Visit(StringFieldProperties properties) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError("Editor is not a valid value.", + nameof(properties.Editor)); + } + + if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + { + yield return new ValidationError("Radio buttons or dropdown list need allowed values.", + nameof(properties.AllowedValues)); + } + + if (properties.Pattern != null && !properties.Pattern.IsValidRegex()) + { + yield return new ValidationError("Pattern is not a valid expression.", + nameof(properties.Pattern)); + } + + if (properties.MaxLength.HasValue && properties.MinLength.HasValue && properties.MinLength.Value >= properties.MaxLength.Value) + { + yield return new ValidationError("Max length must be greater than min length.", + nameof(properties.MinLength), + nameof(properties.MaxLength)); + } + + if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) + { + yield return new ValidationError("Either allowed values or min and max length can be defined.", + nameof(properties.AllowedValues), + nameof(properties.MinLength), + nameof(properties.MaxLength)); + } + } + + public IEnumerable Visit(TagsFieldProperties properties) + { + if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value) + { + yield return new ValidationError("Max items must be greater than min items.", + nameof(properties.MinItems), + nameof(properties.MaxItems)); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs new file mode 100644 index 000000000..70670d74d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -0,0 +1,128 @@ +// ========================================================================== +// GuardSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +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; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public static class GuardSchema + { + public static Task CanCreate(CreateSchema command, IAppProvider appProvider) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create schema.", async error => + { + if (!command.Name.IsSlug()) + { + error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); + } + + if (await appProvider.GetSchemaAsync(command.AppId.Name, command.Name) != null) + { + error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name))); + } + + if (command.Fields != null && command.Fields.Any()) + { + var index = 0; + + foreach (var field in command.Fields) + { + var prefix = $"Fields.{index}"; + + if (!field.Partitioning.IsValidPartitioning()) + { + error(new ValidationError("Partitioning is not valid.", $"{prefix}.{nameof(field.Partitioning)}")); + } + + if (!field.Name.IsPropertyName()) + { + error(new ValidationError("Name must be a valid property name.", $"{prefix}.{nameof(field.Name)}")); + } + + if (field.Properties == null) + { + error(new ValidationError("Properties must be defined.", $"{prefix}.{nameof(field.Properties)}")); + } + + var propertyErrors = FieldPropertiesValidator.Validate(field.Properties); + + foreach (var propertyError in propertyErrors) + { + error(propertyError); + } + } + + if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count) + { + error(new ValidationError("Fields cannot have duplicate names.", nameof(command.Fields))); + } + } + }); + } + + public static void CanReorder(Schema schema, ReorderFields command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot reorder schema fields.", error => + { + if (command.FieldIds == null) + { + error(new ValidationError("Field ids must be specified.", nameof(command.FieldIds))); + } + + if (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x))) + { + error(new ValidationError("Ids must cover all fields.", nameof(command.FieldIds))); + } + }); + } + + public static void CanPublish(Schema schema, PublishSchema command) + { + Guard.NotNull(command, nameof(command)); + + if (schema.IsPublished) + { + throw new DomainException("Schema is already published."); + } + } + + public static void CanUnpublish(Schema schema, UnpublishSchema command) + { + Guard.NotNull(command, nameof(command)); + + if (!schema.IsPublished) + { + throw new DomainException("Schema is not published."); + } + } + + public static void CanUpdate(Schema schema, UpdateSchema command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanConfigureScripts(Schema schema, ConfigureScripts command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanDelete(Schema schema, DeleteSchema command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs new file mode 100644 index 000000000..dfa761112 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs @@ -0,0 +1,160 @@ +// ========================================================================== +// SchemaFieldGuard.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public static class GuardSchemaField + { + public static void CanAdd(Schema schema, AddField command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add a new field.", error => + { + if (!command.Partitioning.IsValidPartitioning()) + { + error(new ValidationError("Partitioning is not valid.", nameof(command.Partitioning))); + } + + if (!command.Name.IsPropertyName()) + { + error(new ValidationError("Name must be a valid property name.", nameof(command.Name))); + } + + if (command.Properties == null) + { + error(new ValidationError("Properties must be defined.", nameof(command.Properties))); + } + + var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); + + foreach (var propertyError in propertyErrors) + { + error(propertyError); + } + + if (schema.FieldsByName.ContainsKey(command.Name)) + { + error(new ValidationError($"There is already a field with name '{command.Name}'", nameof(command.Name))); + } + }); + } + + public static void CanUpdate(Schema schema, UpdateField command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot update field.", error => + { + if (command.Properties == null) + { + error(new ValidationError("Properties must be defined.", nameof(command.Properties))); + } + + var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); + + foreach (var propertyError in propertyErrors) + { + error(propertyError); + } + }); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsLocked) + { + throw new DomainException("Schema field is already locked."); + } + } + + public static void CanDelete(Schema schema, DeleteField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsLocked) + { + throw new DomainException("Schema field is locked."); + } + } + + public static void CanHide(Schema schema, HideField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsHidden) + { + throw new DomainException("Schema field is already hidden."); + } + } + + public static void CanShow(Schema schema, ShowField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (!field.IsHidden) + { + throw new DomainException("Schema field is already visible."); + } + } + + public static void CanDisable(Schema schema, DisableField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsDisabled) + { + throw new DomainException("Schema field is already disabled."); + } + } + + public static void CanEnable(Schema schema, EnableField command) + { + var field = GetFieldOrThrow(schema, command.FieldId); + + if (!field.IsDisabled) + { + throw new DomainException("Schema field is already enabled."); + } + } + + public static void CanLock(Schema schema, LockField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsLocked) + { + throw new DomainException("Schema field is already locked."); + } + } + + private static Field GetFieldOrThrow(Schema schema, long fieldId) + { + if (!schema.FieldsById.TryGetValue(fieldId, out var field)) + { + throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema)); + } + + return field; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs new file mode 100644 index 000000000..34bcc8d9a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// ISchemaEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public interface ISchemaEntity : + IEntity, + IEntityWithAppRef, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + string Name { get; } + + bool IsPublished { get; } + + bool IsDeleted { get; } + + string ScriptQuery { get; } + + string ScriptCreate { get; } + + string ScriptUpdate { get; } + + string ScriptDelete { get; } + + string ScriptChange { get; } + + Schema SchemaDef { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs new file mode 100644 index 000000000..f22b1a599 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs @@ -0,0 +1,198 @@ +// ========================================================================== +// SchemaCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Guards; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Entities.State.SchemaDefs +{ + public class SchemaCommandMiddleware : ICommandMiddleware + { + private readonly IAppProvider appProvider; + private readonly IAggregateHandler handler; + + public SchemaCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(appProvider, nameof(appProvider)); + + this.handler = handler; + + this.appProvider = appProvider; + } + + protected Task On(CreateSchema command, CommandContext context) + { + return handler.CreateAsync(context, async s => + { + await GuardSchema.CanCreate(command, appProvider); + + s.Create(command); + + context.Complete(EntityCreatedResult.Create(s.State.Id, s.Version)); + }); + } + + protected Task On(AddField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanAdd(s.State.SchemaDef, command); + + s.Add(command); + + context.Complete(EntityCreatedResult.Create(s.State.SchemaDef.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version)); + }); + } + + protected Task On(DeleteField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanDelete(s.State.SchemaDef, command); + + s.DeleteField(command); + }); + } + + protected Task On(LockField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanLock(s.State.SchemaDef, command); + + s.LockField(command); + }); + } + + protected Task On(HideField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanHide(s.State.SchemaDef, command); + + s.HideField(command); + }); + } + + protected Task On(ShowField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanShow(s.State.SchemaDef, command); + + s.ShowField(command); + }); + } + + protected Task On(DisableField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanDisable(s.State.SchemaDef, command); + + s.DisableField(command); + }); + } + + protected Task On(EnableField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanEnable(s.State.SchemaDef, command); + + s.EnableField(command); + }); + } + + protected Task On(UpdateField command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanUpdate(s.State.SchemaDef, command); + + s.UpdateField(command); + }); + } + + protected Task On(ReorderFields command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanReorder(s.State.SchemaDef, command); + + s.Reorder(command); + }); + } + + protected Task On(UpdateSchema command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanUpdate(s.State.SchemaDef, command); + + s.Update(command); + }); + } + + protected Task On(PublishSchema command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanPublish(s.State.SchemaDef, command); + + s.Publish(command); + }); + } + + protected Task On(UnpublishSchema command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanUnpublish(s.State.SchemaDef, command); + + s.Unpublish(command); + }); + } + + protected Task On(ConfigureScripts command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanConfigureScripts(s.State.SchemaDef, command); + + s.ConfigureScripts(command); + }); + } + + protected Task On(DeleteSchema command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanDelete(s.State.SchemaDef, command); + + s.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs new file mode 100644 index 000000000..6d3eb5277 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -0,0 +1,261 @@ +// ========================================================================== +// SchemaDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaDomainObject : DomainObjectBase + { + private readonly FieldRegistry registry; + + public SchemaDomainObject(FieldRegistry registry) + { + Guard.NotNull(registry, nameof(registry)); + + this.registry = registry; + } + + public SchemaDomainObject Create(CreateSchema command) + { + VerifyNotCreated(); + + var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(State.Id, command.Name) }); + + if (command.Fields != null) + { + @event.Fields = new List(); + + foreach (var commandField in command.Fields) + { + var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField()); + + @event.Fields.Add(eventField); + } + } + + RaiseEvent(@event); + + return this; + } + + public SchemaDomainObject Add(AddField command) + { + VerifyCreatedAndNotDeleted(); + + var partitioning = + string.Equals(command.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? + Partitioning.Language : + Partitioning.Invariant; + + var fieldId = State.TotalFields; + + var field = registry.CreateField(fieldId, command.Name, partitioning, command.Properties); + + UpdateState(command, state => + { + state.SchemaDef = state.SchemaDef.AddField(field); + state.TotalFields = fieldId + 1; + }); + + RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(fieldId + 1, command.Name) })); + + return this; + } + + public SchemaDomainObject UpdateField(UpdateField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.UpdateField(command.FieldId, command.Properties)); + + RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated())); + + return this; + } + + public SchemaDomainObject LockField(LockField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.LockField(command.FieldId)); + + RaiseEvent(command, new FieldLocked()); + + return this; + } + + public SchemaDomainObject HideField(HideField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.HideField(command.FieldId)); + + RaiseEvent(command, new FieldHidden()); + + return this; + } + + public SchemaDomainObject ShowField(ShowField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.ShowField(command.FieldId)); + + RaiseEvent(command, new FieldShown()); + + return this; + } + + public SchemaDomainObject DisableField(DisableField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.DisableField(command.FieldId)); + + RaiseEvent(command, new FieldDisabled()); + + return this; + } + + public SchemaDomainObject EnableField(EnableField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.EnableField(command.FieldId)); + + RaiseEvent(command, new FieldEnabled()); + + return this; + } + + public SchemaDomainObject DeleteField(DeleteField command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.DeleteField(command.FieldId)); + + RaiseEvent(command, new FieldDeleted()); + + return this; + } + + public SchemaDomainObject Reorder(ReorderFields command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.ReorderFields(command.FieldIds)); + + RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); + + return this; + } + + public SchemaDomainObject Publish(PublishSchema command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.Publish()); + + RaiseEvent(SimpleMapper.Map(command, new SchemaPublished())); + + return this; + } + + public SchemaDomainObject Unpublish(UnpublishSchema command) + { + VerifyCreatedAndNotDeleted(); + + UpdateSchema(command, s => s.Unpublish()); + + RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished())); + + return this; + } + + public SchemaDomainObject ConfigureScripts(ConfigureScripts command) + { + VerifyCreatedAndNotDeleted(); + + UpdateState(command, s => SimpleMapper.Map(command, s)); + + RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured())); + + return this; + } + + public SchemaDomainObject Delete(DeleteSchema command) + { + VerifyCreatedAndNotDeleted(); + + UpdateState(command, s => s.IsDeleted = true); + + RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted())); + + return this; + } + + public SchemaDomainObject Update(UpdateSchema command) + { + VerifyCreatedAndNotDeleted(); + + UpdateState(command, s => SimpleMapper.Map(command, s)); + + RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated())); + + return this; + } + + protected void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event) + { + SimpleMapper.Map(fieldCommand, @event); + + if (State.SchemaDef.FieldsById.TryGetValue(fieldCommand.FieldId, out var field)) + { + @event.FieldId = new NamedId(field.Id, field.Name); + } + + RaiseEvent(@event); + } + + private void VerifyNotCreated() + { + if (State.SchemaDef != null) + { + throw new DomainException("Schema has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (State.IsDeleted || State.SchemaDef == null) + { + throw new DomainException("Schema has already been deleted or not created yet."); + } + } + + private void UpdateSchema(ICommand command, Func updater) + { + UpdateState(command, s => s.SchemaDef = updater(s.SchemaDef)); + } + + protected override SchemaState CloneState(ICommand command, Action updater) + { + return State.Clone().Update((SquidexCommand)command, updater); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs new file mode 100644 index 000000000..43e1a2328 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// SchemaHistoryEventsCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase + { + public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "created schema {[Name]}"); + + AddEventMessage( + "updated schema {[Name]}"); + + AddEventMessage( + "deleted schema {[Name]}"); + + AddEventMessage( + "published schema {[Name]}"); + + AddEventMessage( + "unpublished schema {[Name]}"); + + AddEventMessage( + "reordered fields of schema {[Name]}"); + + AddEventMessage( + "added field {[Field]} to schema {[Name]}"); + + AddEventMessage( + "deleted field {[Field]} from schema {[Name]}"); + + AddEventMessage( + "has locked field {[Field]} of schema {[Name]}"); + + AddEventMessage( + "has hidden field {[Field]} of schema {[Name]}"); + + AddEventMessage( + "has shown field {[Field]} of schema {[Name]}"); + + AddEventMessage( + "disabled field {[Field]} of schema {[Name]}"); + + AddEventMessage( + "disabled field {[Field]} of schema {[Name]}"); + + AddEventMessage( + "has updated field {[Field]} of schema {[Name]}"); + + AddEventMessage( + "deleted field {[Field]} of schema {[Name]}"); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + if (@event.Payload is SchemaEvent schemaEvent) + { + var channel = $"schemas.{schemaEvent.SchemaId.Name}"; + + var result = ForEvent(@event.Payload, channel).AddParameter("Name", schemaEvent.SchemaId.Name); + + if (schemaEvent is FieldEvent fieldEvent) + { + result.AddParameter("Field", fieldEvent.FieldId.Name); + } + + return Task.FromResult(result); + } + + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs new file mode 100644 index 000000000..9fd80f57b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// JsonSchemaEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.State +{ + public sealed class SchemaState : DomainObjectState, + ISchemaEntity, + IUpdateableEntityWithAppRef, + IUpdateableEntityWithCreatedBy, + IUpdateableEntityWithLastModifiedBy + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public Guid AppId { get; set; } + + [JsonProperty] + public int TotalFields { get; set; } + + [JsonProperty] + public bool IsDeleted { get; set; } + + [JsonProperty] + public string ScriptQuery { get; set; } + + [JsonProperty] + public string ScriptCreate { get; set; } + + [JsonProperty] + public string ScriptUpdate { get; set; } + + [JsonProperty] + public string ScriptDelete { get; set; } + + [JsonProperty] + public string ScriptChange { get; set; } + + [JsonProperty] + public Schema SchemaDef { get; set; } + + [JsonIgnore] + public bool IsPublished + { + get { return SchemaDef.IsPublished; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj new file mode 100644 index 000000000..d273d2afb --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -0,0 +1,25 @@ + + + netstandard2.0 + + + full + True + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + diff --git a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs new file mode 100644 index 000000000..58e1087a0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// SquidexCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Security.Claims; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class SquidexCommand : ICommand + { + public ClaimsPrincipal Principal { get; set; } + + public RefToken Actor { get; set; } + + public long? ExpectedVersion { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/src/Squidex.Domain.Apps.Events/SquidexEvent.cs index 729022e8c..2eb7f3119 100644 --- a/src/Squidex.Domain.Apps.Events/SquidexEvent.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexEvent.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Events { public abstract class SquidexEvent : IEvent { + public string Username { get; set; } + public RefToken Actor { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs index 3a4ec4f9e..4f57733e8 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs @@ -5,6 +5,7 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Domain.Apps.Read.Apps.Services { public sealed class PlanChangeAsyncResult : IChangePlanResult diff --git a/src/Squidex.Domain.Apps.Read/EntityMapper.cs b/src/Squidex.Domain.Apps.Read/EntityMapper.cs index 829ea277c..9dfe40a6d 100644 --- a/src/Squidex.Domain.Apps.Read/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Read/EntityMapper.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Read private static void SetCreatedBy(SquidexEvent @event, IEntity entity) { - if (entity is IUpdateableEntityWithCreatedBy withCreatedBy) + if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy) { withCreatedBy.CreatedBy = @event.Actor; } diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index 6badd2691..10f17b85b 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; +using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands @@ -15,44 +16,44 @@ namespace Squidex.Infrastructure.Commands public sealed class AggregateHandler : IAggregateHandler { private readonly IStateFactory stateFactory; + private readonly ISemanticLog log; private readonly IServiceProvider serviceProvider; - public AggregateHandler(IStateFactory stateFactory, IServiceProvider serviceProvider) + public AggregateHandler(IStateFactory stateFactory, IServiceProvider serviceProvider, ISemanticLog log) { Guard.NotNull(stateFactory, nameof(stateFactory)); Guard.NotNull(serviceProvider, nameof(serviceProvider)); + Guard.NotNull(log, nameof(log)); this.stateFactory = stateFactory; this.serviceProvider = serviceProvider; + + this.log = log; } - public Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate + public Task CreateAsync(CommandContext context, Func creator) where T : class, IDomainObject { Guard.NotNull(creator, nameof(creator)); return InvokeAsync(context, creator, false); } - public Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate + public Task UpdateAsync(CommandContext context, Func updater) where T : class, IDomainObject { Guard.NotNull(updater, nameof(updater)); return InvokeAsync(context, updater, true); } - private async Task InvokeAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IAggregate + private async Task InvokeAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject { Guard.NotNull(context, nameof(context)); - var aggregateCommand = GetCommand(context); - var aggregateFactory = (DomainObjectFactoryFunction)serviceProvider.GetService(typeof(DomainObjectFactoryFunction)); - - var wrapper = await stateFactory.GetDetachedAsync>(aggregateCommand.AggregateId.ToString()); - - var domainObject = aggregateFactory(aggregateCommand.AggregateId); + var domainObjectCommand = GetCommand(context); + var domainObjectId = domainObjectCommand.AggregateId; + var domainObject = await stateFactory.GetDetachedAsync(domainObjectId.ToString()); - await wrapper.LoadAsync(domainObject, isUpdate ? aggregateCommand.ExpectedVersion : -1); - await wrapper.UpdateAsync(handler); + await domainObject.WriteAsync(log); if (!context.IsCompleted) { @@ -62,7 +63,7 @@ namespace Squidex.Infrastructure.Commands } else { - context.Complete(EntityCreatedResult.Create(domainObject.Id, domainObject.Version)); + context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version)); } } diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 86c7640cd..5eb115cd6 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Commands { public static class CommandExtensions { - public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IAggregate + public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject { return handler.CreateAsync(context, x => { @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Commands }); } - public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IAggregate + public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject { return handler.UpdateAsync(context, x => { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index bd7595cce..caf135179 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -8,41 +8,35 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectBase : IAggregate, IEquatable + public abstract class DomainObjectBase : IDomainObject { private readonly List> uncomittedEvents = new List>(); - private readonly Guid id; - private int version; + private int version = -1; + private TState state; + private IPersistence persistence; - public int Version + public TState State { - get { return version; } + get { return state; } } - public Guid Id + public int Version { - get { return id; } + get { return version; } } - protected DomainObjectBase(Guid id, int version) + public Task ActivateAsync(string key, IStore store) { - Guard.NotEmpty(id, nameof(id)); - Guard.GreaterEquals(version, -1, nameof(version)); - - this.id = id; + persistence = store.WithSnapshots(key, s => state = s); - this.version = version; - } - - protected abstract void DispatchEvent(Envelope @event); - - private void ApplyEventCore(Envelope @event) - { - DispatchEvent(@event); version++; + return persistence.ReadAsync(); } protected void RaiseEvent(IEvent @event) @@ -55,38 +49,31 @@ namespace Squidex.Infrastructure.Commands Guard.NotNull(@event, nameof(@event)); uncomittedEvents.Add(@event.To()); - - ApplyEventCore(@event.To()); } - void IAggregate.ApplyEvent(Envelope @event) + public void UpdateState(ICommand command, Action updater) { - ApplyEventCore(@event); + state = CloneState(command, updater); } - void IAggregate.ClearUncommittedEvents() - { - uncomittedEvents.Clear(); - } - - public ICollection> GetUncomittedEvents() - { - return uncomittedEvents; - } - - public override int GetHashCode() - { - return id.GetHashCode(); - } - - public override bool Equals(object obj) - { - return Equals(obj as IAggregate); - } + protected abstract TState CloneState(ICommand command, Action updater); - public bool Equals(IAggregate other) + public async Task WriteAsync(ISemanticLog log) { - return other != null && other.Id.Equals(id); + await persistence.WriteSnapshotAsync(state); + + try + { + await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + } + catch (Exception ex) + { + log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); + } + finally + { + uncomittedEvents.Clear(); + } } } } diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectWrapper.cs b/src/Squidex.Infrastructure/Commands/DomainObjectWrapper.cs deleted file mode 100644 index 9eb8a7303..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectWrapper.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// DomainObjectWrapper.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public delegate T DomainObjectFactoryFunction(Guid id) where T : IAggregate; - - public sealed class DomainObjectWrapper : IStatefulObject where T : IAggregate - { - private IPersistence persistence; - private T domainObject; - - public Task ActivateAsync(string key, IStore store) - { - persistence = store.WithEventSourcing(key, e => domainObject.ApplyEvent(e)); - - return TaskHelper.Done; - } - - public Task LoadAsync(T domainObject, long? expectedVersion) - { - this.domainObject = domainObject; - - return persistence.ReadAsync(expectedVersion); - } - - public async Task UpdateAsync(Func handler) - { - await handler(domainObject); - - var events = domainObject.GetUncomittedEvents(); - - foreach (var @event in events) - { - @event.SetAggregateId(domainObject.Id); - } - - await persistence.WriteEventsAsync(events.ToArray()); - - domainObject.ClearUncommittedEvents(); - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs index 77196fe19..c3a01f38d 100644 --- a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs @@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.Commands { public interface IAggregateHandler { - Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate; + Task CreateAsync(CommandContext context, Func creator) where T : class, IDomainObject; - Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate; + Task UpdateAsync(CommandContext context, Func updater) where T : class, IDomainObject; } } diff --git a/src/Squidex.Infrastructure/Commands/IDomainObject.cs b/src/Squidex.Infrastructure/Commands/IDomainObject.cs new file mode 100644 index 000000000..971382c81 --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/IDomainObject.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IDomainObjectBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public interface IDomainObject : IStatefulObject + { + int Version { get; } + + Task WriteAsync(ISemanticLog log); + } +} \ No newline at end of file From cd264df6c8901d4c116a65210e48b02ead48c231 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 4 Dec 2017 23:08:21 +0100 Subject: [PATCH 02/30] Tests for read models and apps. --- Squidex.sln | 15 + .../Apps/AppCommandMiddleware.cs | 2 +- .../Apps/AppDomainObject.cs | 6 +- .../Apps/State/AppState.cs | 5 +- .../CachingProviderBase.cs | 30 + .../Contents/ContentHistoryEventsCreator.cs | 49 ++ .../Contents/ContentQueryService.cs | 216 ++++++ .../Contents/Edm/EdmModelBuilder.cs | 74 ++ .../Contents/Edm/EdmModelExtensions.cs | 39 + .../Contents/GraphQL/CachingGraphQLService.cs | 85 +++ .../Contents/GraphQL/GraphQLModel.cs | 229 ++++++ .../Contents/GraphQL/GraphQLQuery.cs | 23 + .../Contents/GraphQL/GraphQLQueryContext.cs | 67 ++ .../Contents/GraphQL/IGraphQLContext.cs | 38 + .../Contents/GraphQL/IGraphQLService.cs | 19 + .../Contents/GraphQL/IGraphQLUrlGenerator.cs | 27 + .../Contents/GraphQL/Types/AssetGraphType.cs | 170 +++++ .../GraphQL/Types/ContentDataGraphType.cs | 68 ++ .../GraphQL/Types/ContentGraphType.cs | 112 +++ .../GraphQL/Types/ContentQueryGraphType.cs | 192 +++++ .../Contents/GraphQL/Types/NoopGraphType.cs | 37 + .../Contents/IContentEntity.cs | 25 + .../Contents/IContentQueryService.cs | 28 + .../Contents/QueryContext.cs | 146 ++++ .../Repositories/IContentRepository.cs | 33 + .../Rules/Guards/GuardRule.cs | 1 - .../Rules/Guards/RuleTriggerValidator.cs | 1 - .../Squidex.Domain.Apps.Events.csproj | 3 + .../EventSourcing/Events/GetEventStore.cs | 1 - .../Commands/DomainObjectBase.cs | 14 +- .../Apps/AppCommandMiddlewareTests.cs | 244 +++++++ .../Apps/AppDomainObjectTests.cs | 284 ++++++++ .../Apps/AppEventTests.cs | 51 ++ .../Apps/ConfigAppLimitsProviderTests.cs | 159 ++++ .../Apps/Guards/GuardAppClientsTests.cs | 142 ++++ .../Apps/Guards/GuardAppContributorsTests.cs | 158 ++++ .../Apps/Guards/GuardAppLanguagesTests.cs | 131 ++++ .../Apps/Guards/GuardAppTests.cs | 118 +++ .../Apps/NoopAppPlanBillingManagerTests.cs | 38 + .../Contents/ContentQueryServiceTests.cs | 220 ++++++ .../Contents/GraphQLTests.cs | 689 ++++++++++++++++++ .../Contents/ODataQueryTests.cs | 394 ++++++++++ .../Contents/TestData/FakeAssetEntity.cs | 50 ++ .../Contents/TestData/FakeContentEntity.cs | 36 + .../Contents/TestData/FakeUrlGenerator.cs | 40 + .../Rules/RuleDequeuerTests.cs | 99 +++ .../Rules/RuleEnqueuerTests.cs | 102 +++ .../Squidex.Domain.Apps.Entities.Tests.csproj | 36 + .../TestHelpers/AssertHelper.cs | 45 ++ .../TestHelpers/HandlerTestBase.cs | 162 ++++ 50 files changed, 4944 insertions(+), 9 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NoopGraphType.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs diff --git a/Squidex.sln b/Squidex.sln index 67e85c17a..fb24873ad 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -67,6 +67,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchma EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities", "src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj", "{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.Tests", "tests\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj", "{AA003372-CD8D-4DBC-962C-F61E0C93CF05}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -329,6 +331,18 @@ Global {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x64.Build.0 = Release|Any CPU {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x86.ActiveCfg = Release|Any CPU {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x86.Build.0 = Release|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x64.Build.0 = Debug|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x86.Build.0 = Debug|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|Any CPU.Build.0 = Release|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x64.ActiveCfg = Release|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x64.Build.0 = Release|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.ActiveCfg = Release|Any CPU + {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -359,6 +373,7 @@ Global {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892} {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892} {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892} + {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index ae6a2e3ce..584471e4d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { return handler.UpdateAsync(context, async a => { - await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan.PlanId)); + await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan?.PlanId)); a.AssignContributor(command); }); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index e3c1ef9b7..40593cc2c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -28,7 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Apps var appId = new NamedId(command.AppId, command.Name); - UpdateState(command, s => s.Name = command.Name); + UpdateState(command, s => { s.Id = appId.Id; s.Name = command.Name; }); + + UpdateContributors(command, c => c.Assign(command.Actor.Identifier, AppContributorPermission.Owner)); RaiseEvent(SimpleMapper.Map(command, CreateInitalEvent(appId))); RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); @@ -158,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateState(command, s => s.Plan = new AppPlan(command.Actor, command.PlanId)); + UpdateState(command, s => s.Plan = command.PlanId != null ? new AppPlan(command.Actor, command.PlanId) : null); RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index b9c40344e..857f66678 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -8,11 +8,14 @@ using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.State { public sealed class AppState : DomainObjectState, IAppEntity { + private static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN); + [JsonProperty] public string Name { get; set; } @@ -26,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.State public AppContributors Contributors { get; set; } = AppContributors.Empty; [JsonProperty] - public LanguagesConfig LanguagesConfig { get; set; } + public LanguagesConfig LanguagesConfig { get; set; } = English; } } diff --git a/src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs b/src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs new file mode 100644 index 000000000..e625dc23d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// CachingProviderBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public abstract class CachingProviderBase + { + private readonly IMemoryCache cache; + + protected IMemoryCache Cache + { + get { return cache; } + } + + protected CachingProviderBase(IMemoryCache cache) + { + Guard.NotNull(cache, nameof(cache)); + + this.cache = cache; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs new file mode 100644 index 000000000..fc0d29266 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// ContentHistoryEventsCreator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase + { + public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) + { + AddEventMessage( + "created content item."); + + AddEventMessage( + "updated content item."); + + AddEventMessage( + "deleted content item."); + + AddEventMessage( + "changed status of content item to {[Status]}."); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + var channel = $"contents.{@event.Headers.AggregateId()}"; + + var result = ForEvent(@event.Payload, channel); + + if (@event.Payload is ContentStatusChanged contentStatusChanged) + { + result = result.AddParameter("Status", contentStatusChanged.Status); + } + + return Task.FromResult(result); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs new file mode 100644 index 000000000..d04adaa1c --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -0,0 +1,216 @@ +// ========================================================================== +// ContentQueryService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.OData; +using Microsoft.OData.UriParser; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Edm; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentQueryService : IContentQueryService + { + private readonly IContentRepository contentRepository; + private readonly IAppProvider appProvider; + private readonly IScriptEngine scriptEngine; + private readonly EdmModelBuilder modelBuilder; + + public ContentQueryService( + IContentRepository contentRepository, + IAppProvider appProvider, + IScriptEngine scriptEngine, + EdmModelBuilder modelBuilder) + { + Guard.NotNull(contentRepository, nameof(contentRepository)); + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(modelBuilder, nameof(modelBuilder)); + Guard.NotNull(appProvider, nameof(appProvider)); + + this.contentRepository = contentRepository; + this.appProvider = appProvider; + this.scriptEngine = scriptEngine; + this.modelBuilder = modelBuilder; + } + + public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) + { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(user, nameof(user)); + Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); + + var isFrontendClient = user.IsInClient("squidex-frontend"); + + var schema = await FindSchemaAsync(app, schemaIdOrName); + + var content = await contentRepository.FindContentAsync(app, schema, id); + + if (content == null || (content.Status != Status.Published && !isFrontendClient)) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); + } + + content = TransformContent(user, schema, new List { content })[0]; + + return (schema, content); + } + + public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query) + { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(user, nameof(user)); + Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); + + var schema = await FindSchemaAsync(app, schemaIdOrName); + + var parsedQuery = ParseQuery(app, query, schema); + + var status = ParseStatus(user, archived); + + var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), parsedQuery); + var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), parsedQuery); + + await Task.WhenAll(taskForItems, taskForCount); + + var list = TransformContent(user, schema, taskForItems.Result.ToList()); + + return (schema, taskForCount.Result, list); + } + + public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet ids) + { + Guard.NotNull(ids, nameof(ids)); + Guard.NotNull(app, nameof(app)); + Guard.NotNull(user, nameof(user)); + Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); + + var schema = await FindSchemaAsync(app, schemaIdOrName); + + var status = ParseStatus(user, archived); + + var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), ids); + var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), ids); + + await Task.WhenAll(taskForItems, taskForCount); + + var list = TransformContent(user, schema, taskForItems.Result.ToList()); + + return (schema, taskForCount.Result, list); + } + + private List TransformContent(ClaimsPrincipal user, ISchemaEntity schema, List contents) + { + var scriptText = schema.ScriptQuery; + + if (!string.IsNullOrWhiteSpace(scriptText)) + { + for (var i = 0; i < contents.Count; i++) + { + var content = contents[i]; + var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText); + + contents[i] = SimpleMapper.Map(content, new Content { Data = contentData }); + } + } + + return contents; + } + + private ODataUriParser ParseQuery(IAppEntity app, string query, ISchemaEntity schema) + { + try + { + var model = modelBuilder.BuildEdmModel(schema, app); + + return model.ParseQuery(query); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + + public async Task FindSchemaAsync(IAppEntity app, string schemaIdOrName) + { + Guard.NotNull(app, nameof(app)); + + ISchemaEntity schema = null; + + if (Guid.TryParse(schemaIdOrName, out var id)) + { + schema = await appProvider.GetSchemaAsync(app.Name, id); + } + + if (schema == null) + { + schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); + } + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); + } + + return schema; + } + + private static List ParseStatus(ClaimsPrincipal user, bool archived) + { + var status = new List(); + + if (user.IsInClient("squidex-frontend")) + { + if (archived) + { + status.Add(Status.Archived); + } + else + { + status.Add(Status.Draft); + status.Add(Status.Published); + } + } + else + { + status.Add(Status.Published); + } + + return status; + } + + private sealed class Content : IContentEntity + { + public Guid Id { get; set; } + public Guid AppId { get; set; } + + public long Version { get; set; } + + public Instant Created { get; set; } + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + public RefToken LastModifiedBy { get; set; } + + public NamedContentData Data { get; set; } + + public Status Status { get; set; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs new file mode 100644 index 000000000..bf67aa1f6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// EdmModelBuilder.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.GenerateEdmSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Edm +{ + public class EdmModelBuilder : CachingProviderBase + { + public EdmModelBuilder(IMemoryCache cache) + : base(cache) + { + } + + public virtual IEdmModel BuildEdmModel(ISchemaEntity schema, IAppEntity app) + { + Guard.NotNull(schema, nameof(schema)); + + var cacheKey = $"{schema.Id}_{schema.Version}_{app.Id}_{app.Version}"; + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); + + return BuildEdmModel(schema.SchemaDef, app.PartitionResolver()); + }); + + return result; + } + + private static EdmModel BuildEdmModel(Schema schema, PartitionResolver partitionResolver) + { + var model = new EdmModel(); + + var container = new EdmEntityContainer("Squidex", "Container"); + + var schemaType = schema.BuildEdmType(partitionResolver, x => + { + model.AddElement(x); + + return x; + }); + + var entityType = new EdmEntityType("Squidex", schema.Name); + entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); + entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String); + + model.AddElement(container); + model.AddElement(schemaType); + model.AddElement(entityType); + + container.AddEntitySet("ContentSet", entityType); + + return model; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelExtensions.cs b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelExtensions.cs new file mode 100644 index 000000000..4e414d746 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelExtensions.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// EdmModelExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Squidex.Domain.Apps.Entities.Contents.Edm +{ + public static class EdmModelExtensions + { + public static ODataUriParser ParseQuery(this IEdmModel model, string query) + { + if (!model.EntityContainer.EntitySets().Any()) + { + return null; + } + + query = query ?? string.Empty; + + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); + + if (query.StartsWith("?", StringComparison.Ordinal)) + { + query = query.Substring(1); + } + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs new file mode 100644 index 000000000..1a7f208e1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// CachingGraphQLService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private readonly IContentQueryService contentQuery; + private readonly IGraphQLUrlGenerator urlGenerator; + private readonly IAssetRepository assetRepository; + private readonly IAppProvider appProvider; + + public CachingGraphQLService(IMemoryCache cache, + IAppProvider appProvider, + IAssetRepository assetRepository, + IContentQueryService contentQuery, + IGraphQLUrlGenerator urlGenerator) + : base(cache) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentQuery, nameof(urlGenerator)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + + this.appProvider = appProvider; + this.assetRepository = assetRepository; + this.contentQuery = contentQuery; + this.urlGenerator = urlGenerator; + } + + public async Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query) + { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(query, nameof(query)); + + if (string.IsNullOrWhiteSpace(query.Query)) + { + return (new object(), new object[0]); + } + + var modelContext = await GetModelAsync(app); + var queryContext = new GraphQLQueryContext(app, assetRepository, contentQuery, user, urlGenerator); + + return await modelContext.ExecuteAsync(queryContext, query); + } + + private async Task GetModelAsync(IAppEntity app) + { + var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); + + var modelContext = Cache.Get(cacheKey); + + if (modelContext == null) + { + var allSchemas = await appProvider.GetSchemasAsync(app.Name); + + modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); + + Cache.Set(cacheKey, modelContext, CacheDuration); + } + + return modelContext; + } + + private static object CreateCacheKey(Guid appId, string etag) + { + return $"GraphQLModel_{appId}_{etag}"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs new file mode 100644 index 000000000..05921a5ca --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -0,0 +1,229 @@ +// ========================================================================== +// GraphQLContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using GraphQLSchema = GraphQL.Types.Schema; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLModel : IGraphQLContext + { + private readonly Dictionary> fieldInfos; + private readonly Dictionary schemaTypes = new Dictionary(); + private readonly Dictionary schemas; + private readonly PartitionResolver partitionResolver; + private readonly IAppEntity app; + private readonly IGraphType assetType; + private readonly IGraphType assetListType; + private readonly GraphQLSchema graphQLSchema; + + public bool CanGenerateAssetSourceUrl { get; } + + public GraphQLModel(IAppEntity app, IEnumerable schemas, IGraphQLUrlGenerator urlGenerator) + { + this.app = app; + + CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; + + partitionResolver = app.PartitionResolver(); + + assetType = new AssetGraphType(this); + assetListType = new ListGraphType(new NonNullGraphType(assetType)); + + fieldInfos = new Dictionary> + { + { + typeof(StringField), + field => ResolveDefault("String") + }, + { + typeof(BooleanField), + field => ResolveDefault("Boolean") + }, + { + typeof(NumberField), + field => ResolveDefault("Float") + }, + { + typeof(DateTimeField), + field => ResolveDefault("Date") + }, + { + typeof(JsonField), + field => ResolveDefault("Json") + }, + { + typeof(TagsField), + field => ResolveDefault("String") + }, + { + typeof(GeolocationField), + field => ResolveDefault("Geolocation") + }, + { + typeof(AssetsField), + field => ResolveAssets(assetListType) + }, + { + typeof(ReferencesField), + field => ResolveReferences(field) + } + }; + + this.schemas = schemas.ToDictionary(x => x.Id); + + graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) }; + + foreach (var schemaType in schemaTypes.Values) + { + schemaType.Initialize(); + } + } + + private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(string name) + { + return (new NoopGraphType(name), new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName))); + } + + public IFieldResolver ResolveAssetUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveAssetSourceUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveAssetThumbnailUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); + }); + + return resolver; + } + + public IFieldResolver ResolveContentUrl(ISchemaEntity schema) + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); + }); + + return resolver; + } + + private static ValueTuple ResolveAssets(IGraphType assetListType) + { + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentIds = c.Source.GetOrDefault(c.FieldName); + + return context.GetReferencedAssetsAsync(contentIds); + }); + + return (assetListType, resolver); + } + + private ValueTuple ResolveReferences(Field field) + { + var schemaId = ((ReferencesField)field).Properties.SchemaId; + var schemaType = GetSchemaType(schemaId); + + if (schemaType == null) + { + return (null, null); + } + + var resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentIds = c.Source.GetOrDefault(c.FieldName); + + return context.GetReferencedContentsAsync(schemaId, contentIds); + }); + + var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); + + return (schemaFieldType, resolver); + } + + public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLQueryContext context, GraphQLQuery query) + { + Guard.NotNull(context, nameof(context)); + + var result = await new DocumentExecuter().ExecuteAsync(options => + { + options.Query = query.Query; + options.Schema = graphQLSchema; + options.Inputs = query.Variables?.ToInputs() ?? new Inputs(); + options.UserContext = context; + options.OperationName = query.OperationName; + }).ConfigureAwait(false); + + return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); + } + + public IFieldPartitioning ResolvePartition(Partitioning key) + { + return partitionResolver(key); + } + + public IGraphType GetAssetType() + { + return assetType; + } + + public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) + { + return fieldInfos[field.GetType()](field); + } + + public IGraphType GetSchemaType(Guid schemaId) + { + var schema = schemas.GetOrDefault(schemaId); + + return schema != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schema, this)) : null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs new file mode 100644 index 000000000..6d4163321 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// GraphQLQuery.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json.Linq; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLQuery + { + public string OperationName { get; set; } + + public string NamedQuery { get; set; } + + public string Query { get; set; } + + public JObject Variables { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs new file mode 100644 index 000000000..8ed50cea9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// GraphQLQueryContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLQueryContext : QueryContext + { + public IGraphQLUrlGenerator UrlGenerator { get; } + + public GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user, + IGraphQLUrlGenerator urlGenerator) + : base(app, assetRepository, contentQuery, user) + { + UrlGenerator = urlGenerator; + } + + public Task> GetReferencedAssetsAsync(JToken value) + { + var ids = ParseIds(value); + + return GetReferencedAssetsAsync(ids); + } + + public Task> GetReferencedContentsAsync(Guid schemaId, JToken value) + { + var ids = ParseIds(value); + + return GetReferencedContentsAsync(schemaId, ids); + } + + private static ICollection ParseIds(JToken value) + { + try + { + var result = new List(); + + if (value is JArray) + { + foreach (var id in value) + { + result.Add(Guid.Parse(id.ToString())); + } + } + + return result; + } + catch + { + return new List(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs new file mode 100644 index 000000000..1bd15fb37 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// SchemaGraphType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphQLContext + { + bool CanGenerateAssetSourceUrl { get; } + + IFieldPartitioning ResolvePartition(Partitioning key); + + IGraphType GetAssetType(); + + IGraphType GetSchemaType(Guid schemaId); + + IFieldResolver ResolveAssetUrl(); + + IFieldResolver ResolveAssetSourceUrl(); + + IFieldResolver ResolveAssetThumbnailUrl(); + + IFieldResolver ResolveContentUrl(ISchemaEntity schema); + + (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs new file mode 100644 index 000000000..8b0ce31b2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// IGraphQLService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphQLService + { + Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs new file mode 100644 index 000000000..983778efc --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// IGraphQLUrlGenerator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public interface IGraphQLUrlGenerator + { + bool CanGenerateAssetSourceUrl { get; } + + string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); + + string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); + + string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); + + string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs new file mode 100644 index 000000000..c75d1c80b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -0,0 +1,170 @@ +// ========================================================================== +// AssetGraphType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class AssetGraphType : ObjectGraphType + { + public AssetGraphType(IGraphQLContext context) + { + Name = "AssetDto"; + + AddField(new FieldType + { + Name = "id", + Resolver = Resolver(x => x.Id.ToString()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The id of the asset." + }); + + AddField(new FieldType + { + Name = "version", + Resolver = Resolver(x => x.Version), + ResolvedType = new NonNullGraphType(new IntGraphType()), + Description = "The version of the asset." + }); + + AddField(new FieldType + { + Name = "created", + Resolver = Resolver(x => x.Created.ToDateTimeUtc()), + ResolvedType = new NonNullGraphType(new DateGraphType()), + Description = "The date and time when the asset has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + Resolver = Resolver(x => x.CreatedBy.ToString()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The user that has created the asset." + }); + + AddField(new FieldType + { + Name = "lastModified", + Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), + ResolvedType = new NonNullGraphType(new DateGraphType()), + Description = "The date and time when the asset has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + Resolver = Resolver(x => x.LastModifiedBy.ToString()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The user that has updated the asset last." + }); + + AddField(new FieldType + { + Name = "mimeType", + Resolver = Resolver(x => x.MimeType), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The mime type." + }); + + AddField(new FieldType + { + Name = "url", + Resolver = context.ResolveAssetUrl(), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The url to the asset." + }); + + AddField(new FieldType + { + Name = "thumbnailUrl", + Resolver = context.ResolveAssetThumbnailUrl(), + ResolvedType = new StringGraphType(), + Description = "The thumbnail url to the asset." + }); + + AddField(new FieldType + { + Name = "fileName", + Resolver = Resolver(x => x.FileName), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The file name." + }); + + AddField(new FieldType + { + Name = "fileType", + Resolver = Resolver(x => x.FileName.FileType()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = "The file type." + }); + + AddField(new FieldType + { + Name = "fileSize", + Resolver = Resolver(x => x.FileSize), + ResolvedType = new NonNullGraphType(new IntGraphType()), + Description = "The size of the file in bytes." + }); + + AddField(new FieldType + { + Name = "fileVersion", + Resolver = Resolver(x => x.FileVersion), + ResolvedType = new NonNullGraphType(new IntGraphType()), + Description = "The version of the file." + }); + + AddField(new FieldType + { + Name = "isImage", + Resolver = Resolver(x => x.IsImage), + ResolvedType = new NonNullGraphType(new BooleanGraphType()), + Description = "Determines of the created file is an image." + }); + + AddField(new FieldType + { + Name = "pixelWidth", + Resolver = Resolver(x => x.PixelWidth), + ResolvedType = new IntGraphType(), + Description = "The width of the image in pixels if the asset is an image." + }); + + AddField(new FieldType + { + Name = "pixelHeight", + Resolver = Resolver(x => x.PixelHeight), + ResolvedType = new IntGraphType(), + Description = "The height of the image in pixels if the asset is an image." + }); + + if (context.CanGenerateAssetSourceUrl) + { + AddField(new FieldType + { + Name = "sourceUrl", + Resolver = context.ResolveAssetSourceUrl(), + ResolvedType = new StringGraphType(), + Description = "The source url of the asset." + }); + } + + Description = "An asset"; + } + + private static IFieldResolver Resolver(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs new file mode 100644 index 000000000..af48d1087 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// ContentDataGraphType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentDataGraphType : ObjectGraphType + { + public ContentDataGraphType(Schema schema, IGraphQLContext context) + { + var schemaName = schema.Properties.Label.WithFallback(schema.Name); + + Name = $"{schema.Name.ToPascalCase()}DataDto"; + + foreach (var field in schema.Fields.Where(x => !x.IsHidden)) + { + var fieldInfo = context.GetGraphType(field); + + if (fieldInfo.ResolveType != null) + { + var fieldName = field.RawProperties.Label.WithFallback(field.Name); + + var fieldGraphType = new ObjectGraphType + { + Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" + }; + + var partition = context.ResolvePartition(field.Partitioning); + + foreach (var partitionItem in partition) + { + fieldGraphType.AddField(new FieldType + { + Name = partitionItem.Key, + Resolver = fieldInfo.Resolver, + ResolvedType = fieldInfo.ResolveType, + Description = field.RawProperties.Hints + }); + } + + fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; + + var fieldResolver = new FuncFieldResolver(c => c.Source.GetOrDefault(field.Name)); + + AddField(new FieldType + { + Name = field.Name.ToCamelCase(), + Resolver = fieldResolver, + ResolvedType = fieldGraphType + }); + } + } + + Description = $"The structure of a {schemaName} content type."; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs new file mode 100644 index 000000000..0ef578e8a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -0,0 +1,112 @@ +// ========================================================================== +// SchemaGraphType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentGraphType : ObjectGraphType + { + private readonly ISchemaEntity schema; + private readonly IGraphQLContext context; + + public ContentGraphType(ISchemaEntity schema, IGraphQLContext context) + { + this.context = context; + this.schema = schema; + + Name = $"{schema.Name.ToPascalCase()}Dto"; + } + + public void Initialize() + { + var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.Name); + + AddField(new FieldType + { + Name = "id", + Resolver = Resolver(x => x.Id.ToString()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = $"The id of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "version", + Resolver = Resolver(x => x.Version), + ResolvedType = new NonNullGraphType(new IntGraphType()), + Description = $"The version of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "created", + Resolver = Resolver(x => x.Created.ToDateTimeUtc()), + ResolvedType = new NonNullGraphType(new DateGraphType()), + Description = $"The date and time when the {schemaName} content has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + Resolver = Resolver(x => x.CreatedBy.ToString()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = $"The user that has created the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "lastModified", + Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), + ResolvedType = new NonNullGraphType(new DateGraphType()), + Description = $"The date and time when the {schemaName} content has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + Resolver = Resolver(x => x.LastModifiedBy.ToString()), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = $"The user that has updated the {schemaName} content last." + }); + + AddField(new FieldType + { + Name = "url", + Resolver = context.ResolveContentUrl(schema), + ResolvedType = new NonNullGraphType(new StringGraphType()), + Description = $"The url to the the {schemaName} content." + }); + + var dataType = new ContentDataGraphType(schema.SchemaDef, context); + + if (dataType.Fields.Any()) + { + AddField(new FieldType + { + Name = "data", + Resolver = Resolver(x => x.Data), + ResolvedType = new NonNullGraphType(dataType), + Description = $"The data of the {schemaName} content." + }); + } + + Description = $"The structure of a {schemaName} content type."; + } + + private static IFieldResolver Resolver(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs new file mode 100644 index 000000000..4d980761d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs @@ -0,0 +1,192 @@ +// ========================================================================== +// GraphModelType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentQueryGraphType : ObjectGraphType + { + public ContentQueryGraphType(IGraphQLContext graphQLContext, IEnumerable schemas) + { + AddAssetFind(graphQLContext); + AddAssetsQuery(graphQLContext); + + foreach (var schema in schemas) + { + var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); + var schemaType = graphQLContext.GetSchemaType(schema.Id); + + AddContentFind(schema, schemaType, schemaName); + AddContentQuery(schema, schemaType, schemaName); + } + + Description = "The app queries."; + } + + private void AddAssetFind(IGraphQLContext graphQLContext) + { + AddField(new FieldType + { + Name = "findAsset", + Arguments = new QueryArguments + { + new QueryArgument(typeof(StringGraphType)) + { + Name = "id", + Description = "The id of the asset.", + DefaultValue = string.Empty + } + }, + ResolvedType = graphQLContext.GetAssetType(), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); + + return context.FindAssetAsync(contentId); + }), + Description = "Find an asset by id." + }); + } + + private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) + { + AddField(new FieldType + { + Name = $"find{schema.Name.ToPascalCase()}Content", + Arguments = new QueryArguments + { + new QueryArgument(typeof(StringGraphType)) + { + Name = "id", + Description = $"The id of the {schemaName} content.", + DefaultValue = string.Empty + } + }, + ResolvedType = schemaType, + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); + + return context.FindContentAsync(schema.Id, contentId); + }), + Description = $"Find an {schemaName} content by id." + }); + } + + private void AddAssetsQuery(IGraphQLContext graphQLContext) + { + AddField(new FieldType + { + Name = "queryAssets", + Arguments = new QueryArguments + { + new QueryArgument(typeof(IntGraphType)) + { + Name = "top", + Description = "Optional number of assets to take.", + DefaultValue = 20 + }, + new QueryArgument(typeof(IntGraphType)) + { + Name = "skip", + Description = "Optional number of assets to skip.", + DefaultValue = 0 + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "search", + Description = "Optional query.", + DefaultValue = string.Empty + } + }, + ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + var argTop = c.GetArgument("top", 20); + var argSkip = c.GetArgument("skip", 0); + var argQuery = c.GetArgument("search", string.Empty); + + return context.QueryAssetsAsync(argQuery, argSkip, argTop); + }), + Description = "Query assets items." + }); + } + + private void AddContentQuery(ISchemaEntity schema, IGraphType schemaType, string schemaName) + { + AddField(new FieldType + { + Name = $"query{schema.Name.ToPascalCase()}Contents", + Arguments = new QueryArguments + { + new QueryArgument(typeof(IntGraphType)) + { + Name = "top", + Description = "Optional number of contents to take.", + DefaultValue = 20 + }, + new QueryArgument(typeof(IntGraphType)) + { + Name = "skip", + Description = "Optional number of contents to skip.", + DefaultValue = 0 + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "filter", + Description = "Optional OData filter.", + DefaultValue = string.Empty + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "search", + Description = "Optional OData full text search.", + DefaultValue = string.Empty + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "orderby", + Description = "Optional OData order definition.", + DefaultValue = string.Empty + } + }, + ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentQuery = BuildODataQuery(c); + + return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); + }), + Description = $"Query {schemaName} content items." + }); + } + + private static string BuildODataQuery(ResolveFieldContext c) + { + var odataQuery = "?" + + string.Join("&", + c.Arguments + .Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) + .Select(x => $"${x.Key}={x.Value}")); + + return odataQuery; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NoopGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NoopGraphType.cs new file mode 100644 index 000000000..90c87258c --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NoopGraphType.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// NoopGraphType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using GraphQL.Language.AST; +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class NoopGraphType : ScalarGraphType + { + public NoopGraphType(string name) + { + Name = name; + } + + public override object Serialize(object value) + { + return value; + } + + public override object ParseValue(object value) + { + return value; + } + + public override object ParseLiteral(IValue value) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs new file mode 100644 index 000000000..7e3ae5451 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// IContentEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentEntity : + IEntity, + IEntityWithAppRef, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion + { + Status Status { get; } + + NamedContentData Data { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs new file mode 100644 index 000000000..99517290d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// IContentQueryService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentQueryService + { + Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet ids); + + Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); + + Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); + + Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs new file mode 100644 index 000000000..fc5ccd4ea --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// QueryContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class QueryContext + { + private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); + private readonly IContentQueryService contentQuery; + private readonly IAssetRepository assetRepository; + private readonly IAppEntity app; + private readonly ClaimsPrincipal user; + + public QueryContext( + IAppEntity app, + IAssetRepository assetRepository, + IContentQueryService contentQuery, + ClaimsPrincipal user) + { + Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + Guard.NotNull(app, nameof(app)); + Guard.NotNull(user, nameof(user)); + + this.assetRepository = assetRepository; + this.contentQuery = contentQuery; + + this.user = user; + + this.app = app; + } + + public async Task FindAssetAsync(Guid id) + { + var asset = cachedAssets.GetOrDefault(id); + + if (asset == null) + { + asset = await assetRepository.FindAssetAsync(id); + + if (asset != null) + { + cachedAssets[asset.Id] = asset; + } + } + + return asset; + } + + public async Task FindContentAsync(Guid schemaId, Guid id) + { + var content = cachedContents.GetOrDefault(id); + + if (content == null) + { + content = (await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id)).Content; + + if (content != null) + { + cachedContents[content.Id] = content; + } + } + + return content; + } + + public async Task> QueryAssetsAsync(string query, int skip = 0, int take = 10) + { + var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip); + + foreach (var asset in assets) + { + cachedAssets[asset.Id] = asset; + } + + return assets; + } + + public async Task> QueryContentsAsync(string schemaIdOrName, string query) + { + var contents = await contentQuery.QueryWithCountAsync(app, schemaIdOrName, user, false, query); + + foreach (var content in contents.Items) + { + cachedContents[content.Id] = content; + } + + return contents.Items; + } + + public async Task> GetReferencedAssetsAsync(ICollection ids) + { + Guard.NotNull(ids, nameof(ids)); + + var notLoadedAssets = new HashSet(ids.Where(id => !cachedAssets.ContainsKey(id))); + + if (notLoadedAssets.Count > 0) + { + var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue); + + foreach (var asset in assets) + { + cachedAssets[asset.Id] = asset; + } + } + + return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); + } + + public async Task> GetReferencedContentsAsync(Guid schemaId, ICollection ids) + { + Guard.NotNull(ids, nameof(ids)); + + var notLoadedContents = new HashSet(ids.Where(id => !cachedContents.ContainsKey(id))); + + if (notLoadedContents.Count > 0) + { + var contents = await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, notLoadedContents); + + foreach (var content in contents.Items) + { + cachedContents[content.Id] = content; + } + } + + return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs new file mode 100644 index 000000000..79de0dbf0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// IContentRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.OData.UriParser; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.Repositories +{ + public interface IContentRepository + { + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); + + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); + + Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds); + + Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); + + Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); + + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs index ccf3023f1..5a06c7b0c 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure; diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index dbe01af6f..febb77364 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index 9e69912e3..de665699c 100644 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -11,9 +11,12 @@ + + + diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs index bc7cda891..cd65c5e43 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using EventStore.ClientAPI; -using EventStore.ClientAPI.Exceptions; namespace Squidex.Infrastructure.EventSourcing { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index caf135179..40c1a0c03 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -15,11 +15,11 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectBase : IDomainObject + public abstract class DomainObjectBase : IDomainObject where TState : new() { private readonly List> uncomittedEvents = new List>(); private int version = -1; - private TState state; + private TState state = new TState(); private IPersistence persistence; public TState State @@ -32,6 +32,16 @@ namespace Squidex.Infrastructure.Commands get { return version; } } + public IReadOnlyList> GetUncomittedEvents() + { + return uncomittedEvents; + } + + public void ClearUncommittedEvents() + { + uncomittedEvents.Clear(); + } + public Task ActivateAsync(string key, IStore store) { persistence = store.WithSnapshots(key, s => state = s); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs new file mode 100644 index 000000000..05b7b1069 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -0,0 +1,244 @@ +// ========================================================================== +// AppCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppCommandMiddlewareTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly AppCommandMiddleware sut; + private readonly AppDomainObject app; + private readonly Language language = Language.DE; + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientName = "client"; + + public AppCommandMiddlewareTests() + { + app = new AppDomainObject(); + + A.CallTo(() => appProvider.GetAppAsync(AppName)) + .Returns((IAppEntity)null); + + A.CallTo(() => userResolver.FindByIdAsync(contributorId)) + .Returns(A.Fake()); + + sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); + + await TestCreate(app, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(AppId, context.Result>().IdOrValue); + } + + [Fact] + public async Task AssignContributor_should_update_domain_object_if_user_found() + { + A.CallTo(() => appPlansProvider.GetPlan(null)) + .Returns(new ConfigAppLimitsPlan { MaxContributors = -1 }); + + CreateApp(); + + var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task RemoveContributor_should_update_domain_object() + { + CreateApp() + .AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); + + var context = CreateContextForCommand(new RemoveContributor { ContributorId = contributorId }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task AttachClient_should_update_domain_object() + { + CreateApp(); + + var context = CreateContextForCommand(new AttachClient { Id = clientName }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task RenameClient_should_update_domain_object() + { + CreateApp() + .AttachClient(CreateCommand(new AttachClient { Id = clientName })); + + var context = CreateContextForCommand(new UpdateClient { Id = clientName, Name = "New Name" }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task RevokeClient_should_update_domain_object() + { + CreateApp() + .AttachClient(CreateCommand(new AttachClient { Id = clientName })); + + var context = CreateContextForCommand(new RevokeClient { Id = clientName }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task ChangePlan_should_update_domain_object() + { + A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) + .Returns(true); + + CreateApp(); + + var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_result() + { + A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) + .Returns(true); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) + .Returns(CreateRedirectResult()); + + CreateApp(); + + var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Null(app.State.Plan); + } + + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) + .Returns(true); + + CreateApp(); + + var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task AddLanguage_should_update_domain_object() + { + CreateApp(); + + var context = CreateContextForCommand(new AddLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task RemoveLanguage_should_update_domain_object() + { + CreateApp() + .AddLanguage(CreateCommand(new AddLanguage { Language = language })); + + var context = CreateContextForCommand(new RemoveLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task UpdateLanguage_should_update_domain_object() + { + CreateApp() + .AddLanguage(CreateCommand(new AddLanguage { Language = language })); + + var context = CreateContextForCommand(new UpdateLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + private AppDomainObject CreateApp() + { + app.Create(CreateCommand(new CreateApp { AppId = AppId, Name = AppName })); + + return app; + } + + private static Task CreateRedirectResult() + { + return Task.FromResult(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs new file mode 100644 index 000000000..c94358368 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -0,0 +1,284 @@ +// ========================================================================== +// AppDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppDomainObjectTests : HandlerTestBase + { + private readonly AppDomainObject sut; + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientId = "client"; + private readonly string clientNewName = "My Client"; + private readonly string planId = "premium"; + + public AppDomainObjectTests() + { + sut = new AppDomainObject(); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + CreateApp(); + + Assert.Throws(() => + { + sut.Create(CreateCommand(new CreateApp { Name = AppName })); + }); + } + + [Fact] + public void Create_should_specify_name_and_owner() + { + sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); + + Assert.Equal(AppName, sut.State.Name); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }), + CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), + CreateEvent(new AppLanguageAdded { Language = Language.EN }) + ); + } + + [Fact] + public void ChangePlan_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); + }); + } + + [Fact] + public void ChangePlan_should_create_events() + { + CreateApp(); + + sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planId }) + ); + } + + [Fact] + public void AssignContributor_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); + }); + } + + [Fact] + public void AssignContributor_should_create_events() + { + CreateApp(); + + sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) + ); + } + + [Fact] + public void RemoveContributor_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); + }); + } + + [Fact] + public void RemoveContributor_should_create_events_and_remove_contributor() + { + CreateApp(); + + sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); + sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); + + sut.GetUncomittedEvents().Skip(1) + .ShouldHaveSameEvents( + CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) + ); + } + + [Fact] + public void AttachClient_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); + }); + } + + [Fact] + public void AttachClient_should_create_events() + { + var command = new AttachClient { Id = clientId }; + + CreateApp(); + + sut.AttachClient(CreateCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) + ); + } + + [Fact] + public void RevokeClient_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); + }); + } + + [Fact] + public void RevokeClient_should_create_events() + { + CreateApp(); + CreateClient(); + + sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppClientRevoked { Id = clientId }) + ); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); + }); + } + + [Fact] + public void UpdateClient_should_create_events() + { + CreateApp(); + CreateClient(); + + sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), + CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) + ); + } + + [Fact] + public void AddLanguage_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); + }); + } + + [Fact] + public void AddLanguage_should_create_events() + { + CreateApp(); + + sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageAdded { Language = Language.DE }) + ); + } + + [Fact] + public void RemoveLanguage_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); + }); + } + + [Fact] + public void RemoveLanguage_should_create_events() + { + CreateApp(); + CreateLanguage(Language.DE); + + sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageRemoved { Language = Language.DE }) + ); + } + + [Fact] + public void UpdateLanguage_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); + }); + } + + [Fact] + public void UpdateLanguage_should_create_events() + { + CreateApp(); + CreateLanguage(Language.DE); + + sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) + ); + } + + private void CreateApp() + { + sut.Create(CreateCommand(new CreateApp { Name = AppName })); + sut.ClearUncommittedEvents(); + } + + private void CreateClient() + { + sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); + sut.ClearUncommittedEvents(); + } + + private void CreateLanguage(Language language) + { + sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); + sut.ClearUncommittedEvents(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs new file mode 100644 index 000000000..7e590590e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// AppEventTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Apps.Old; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable CS0612 // Type or member is obsolete + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppEventTests + { + private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + + [Fact] + public void Should_migrate_client_changed_as_reader_to_client_updated() + { + var source = CreateEvent(new AppClientChanged { IsReader = true }); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); + } + + [Fact] + public void Should_migrate_client_changed_as_writer_to_client_updated() + { + var source = CreateEvent(new AppClientChanged { IsReader = false }); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); + } + + private T CreateEvent(T contentEvent) where T : AppEvent + { + contentEvent.Actor = actor; + contentEvent.AppId = appId; + + return contentEvent; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs new file mode 100644 index 000000000..f6a6066fb --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// ConfigAppLimitsProviderTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class ConfigAppLimitsProviderTests + { + private static readonly ConfigAppLimitsPlan InfinitePlan = new ConfigAppLimitsPlan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1 + }; + + private static readonly ConfigAppLimitsPlan FreePlan = new ConfigAppLimitsPlan + { + Id = "free", + Name = "Free", + MaxApiCalls = 50000, + MaxAssetSize = 1024 * 1024 * 10, + MaxContributors = 2 + }; + + private static readonly ConfigAppLimitsPlan BasicPlan = new ConfigAppLimitsPlan + { + Id = "basic", + Name = "Basic", + MaxApiCalls = 150000, + MaxAssetSize = 1024 * 1024 * 2, + MaxContributors = 5 + }; + + private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; + + [Fact] + public void Should_return_plans() + { + var sut = new ConfigAppPlansProvider(Plans); + + Plans.OrderBy(x => x.MaxApiCalls).ShouldBeEquivalentTo(sut.GetAvailablePlans()); + } + + [Theory] + [InlineData(null)] + [InlineData("my-plan")] + public void Should_return_infinite_if_nothing_configured(string planId) + { + var sut = new ConfigAppPlansProvider(Enumerable.Empty()); + + var plan = sut.GetPlanForApp(CreateApp(planId)); + + plan.ShouldBeEquivalentTo(InfinitePlan); + } + + [Fact] + public void Should_return_fitting_app_plan() + { + var sut = new ConfigAppPlansProvider(Plans); + + var plan = sut.GetPlanForApp(CreateApp("basic")); + + plan.ShouldBeEquivalentTo(BasicPlan); + } + + [Fact] + public void Should_smallest_plan_if_none_fits() + { + var sut = new ConfigAppPlansProvider(Plans); + + var plan = sut.GetPlanForApp(CreateApp("enterprise")); + + plan.ShouldBeEquivalentTo(FreePlan); + } + + [Fact] + public void Should_return_second_plan_for_upgrade_if_plan_is_null() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgrade(null); + + upgradePlan.ShouldBeEquivalentTo(BasicPlan); + } + + [Fact] + public void Should_return_second_plan_for_upgrade_if_plan_not_found() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("enterprise")); + + upgradePlan.ShouldBeEquivalentTo(BasicPlan); + } + + [Fact] + public void Should_not_return_plan_for_upgrade_if_plan_is_highest_plan() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("basic")); + + Assert.Null(upgradePlan); + } + + [Fact] + public void Should_return_next_plan_if_plan_is_upgradeable() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("free")); + + upgradePlan.ShouldBeEquivalentTo(BasicPlan); + } + + [Fact] + public void Should_check_plan_exists() + { + var sut = new ConfigAppPlansProvider(Plans); + + Assert.True(sut.IsConfiguredPlan("basic")); + Assert.True(sut.IsConfiguredPlan("free")); + + Assert.False(sut.IsConfiguredPlan("infinite")); + Assert.False(sut.IsConfiguredPlan("invalid")); + Assert.False(sut.IsConfiguredPlan(null)); + } + + private static IAppEntity CreateApp(string plan) + { + var app = A.Dummy(); + + if (plan != null) + { + A.CallTo(() => app.Plan).Returns(new AppPlan(new RefToken("user", "me"), plan)); + } + else + { + A.CallTo(() => app.Plan).Returns(null); + } + + return app; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs new file mode 100644 index 000000000..5db4d3687 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// GuardAppClientsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppClientsTests + { + private readonly AppClients clients_0 = AppClients.Empty; + + [Fact] + public void CanAttach_should_throw_execption_if_client_id_is_null() + { + var command = new AttachClient(); + + Assert.Throws(() => GuardAppClients.CanAttach(clients_0, command)); + } + + [Fact] + public void CanAttach_should_throw_exception_if_client_already_exists() + { + var command = new AttachClient { Id = "android" }; + + var clients_1 = clients_0.Add("android", "secret"); + + Assert.Throws(() => GuardAppClients.CanAttach(clients_1, command)); + } + + [Fact] + public void CanAttach_should_not_throw_exception_if_client_is_free() + { + var command = new AttachClient { Id = "ios" }; + + var clients_1 = clients_0.Add("android", "secret"); + + GuardAppClients.CanAttach(clients_1, command); + } + + [Fact] + public void CanRevoke_should_throw_execption_if_client_id_is_null() + { + var command = new RevokeClient(); + + Assert.Throws(() => GuardAppClients.CanRevoke(clients_0, command)); + } + + [Fact] + public void CanRevoke_should_throw_exception_if_client_is_not_found() + { + var command = new RevokeClient { Id = "ios" }; + + Assert.Throws(() => GuardAppClients.CanRevoke(clients_0, command)); + } + + [Fact] + public void CanRevoke_should_not_throw_exception_if_client_is_found() + { + var command = new RevokeClient { Id = "ios" }; + + var clients_1 = clients_0.Add("ios", "secret"); + + GuardAppClients.CanRevoke(clients_1, command); + } + + [Fact] + public void CanUpdate_should_throw_execption_if_client_id_is_null() + { + var command = new UpdateClient(); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients_0, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_is_not_found() + { + var command = new UpdateClient { Id = "ios", Name = "iOS" }; + + Assert.Throws(() => GuardAppClients.CanUpdate(clients_0, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_no_name_and_permission() + { + var command = new UpdateClient { Id = "ios" }; + + var clients_1 = clients_0.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_invalid_permission() + { + var command = new UpdateClient { Id = "ios", Permission = (AppClientPermission)10 }; + + var clients_1 = clients_0.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_same_name() + { + var command = new UpdateClient { Id = "ios", Name = "ios" }; + + var clients_1 = clients_0.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_same_permission() + { + var command = new UpdateClient { Id = "ios", Permission = AppClientPermission.Editor }; + + var clients_1 = clients_0.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); + } + + [Fact] + public void UpdateClient_should_not_throw_exception_if_command_is_valid() + { + var command = new UpdateClient { Id = "ios", Name = "iOS", Permission = AppClientPermission.Reader }; + + var clients_1 = clients_0.Add("ios", "secret"); + + GuardAppClients.CanUpdate(clients_1, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs new file mode 100644 index 000000000..6bdfb21c7 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// GuardAppContributorsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Shared.Users; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppContributorsTests + { + private readonly IUserResolver users = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly AppContributors contributors_0 = AppContributors.Empty; + + public GuardAppContributorsTests() + { + A.CallTo(() => users.FindByIdAsync(A.Ignored)) + .Returns(A.Fake()); + + A.CallTo(() => appPlan.MaxContributors) + .Returns(10); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_permission_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_permission() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_user_not_found() + { + A.CallTo(() => users.FindByIdAsync(A.Ignored)) + .Returns(Task.FromResult(null)); + + var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_contributor_max_reached() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "3" }; + + var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); + var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_not_throw_exception_if_user_found() + { + var command = new AssignContributor { ContributorId = "1" }; + + return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan); + } + + [Fact] + public Task CanAssign_should_not_throw_exception_if_contributor_has_another_permission() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); + + return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan); + } + + [Fact] + public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_permission_changed() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); + var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); + + return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); + var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command)); + } + + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); + var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Owner); + + GuardAppContributors.CanRemove(contributors_2, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs new file mode 100644 index 000000000..a3ae0baaf --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// GuardAppLanguagesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppLanguagesTests + { + private readonly LanguagesConfig languages_0 = LanguagesConfig.Build(Language.DE); + + [Fact] + public void CanAddLanguage_should_throw_exception_if_language_is_null() + { + var command = new AddLanguage(); + + Assert.Throws(() => GuardAppLanguages.CanAdd(languages_0, command)); + } + + [Fact] + public void CanAddLanguage_should_throw_exception_if_language_already_added() + { + var command = new AddLanguage { Language = Language.DE }; + + Assert.Throws(() => GuardAppLanguages.CanAdd(languages_0, command)); + } + + [Fact] + public void CanAddLanguage_should_not_throw_exception_if_language_valid() + { + var command = new AddLanguage { Language = Language.EN }; + + GuardAppLanguages.CanAdd(languages_0, command); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_is_null() + { + var command = new RemoveLanguage(); + + Assert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command)); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_not_found() + { + var command = new RemoveLanguage { Language = Language.EN }; + + Assert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command)); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_is_master() + { + var command = new RemoveLanguage { Language = Language.DE }; + + Assert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command)); + } + + [Fact] + public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() + { + var command = new RemoveLanguage { Language = Language.EN }; + + var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); + + GuardAppLanguages.CanRemove(languages_1, command); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_is_null() + { + var command = new UpdateLanguage(); + + var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() + { + var command = new UpdateLanguage { Language = Language.DE, IsOptional = true }; + + var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.IT } }; + + var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_not_found() + { + var command = new UpdateLanguage { Language = Language.IT }; + + var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); + } + + [Fact] + public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + + var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); + + GuardAppLanguages.CanUpdate(languages_1, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs new file mode 100644 index 000000000..d653a3635 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// GuardAppTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public class GuardAppTests + { + private readonly IAppProvider apps = A.Fake(); + private readonly IUserResolver users = A.Fake(); + private readonly IAppPlansProvider appPlans = A.Fake(); + + public GuardAppTests() + { + A.CallTo(() => apps.GetAppAsync("new-app")) + .Returns(Task.FromResult(null)); + + A.CallTo(() => users.FindByIdAsync(A.Ignored)) + .Returns(A.Fake()); + + A.CallTo(() => appPlans.GetPlan("free")) + .Returns(A.Fake()); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_name_already_in_use() + { + A.CallTo(() => apps.GetAppAsync("new-app")) + .Returns(A.Fake()); + + var command = new CreateApp { Name = "new-app" }; + + return Assert.ThrowsAsync(() => GuardApp.CanCreate(command, apps)); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateApp { Name = "INVALID NAME" }; + + return Assert.ThrowsAsync(() => GuardApp.CanCreate(command, apps)); + } + + [Fact] + public Task CanCreate_should_not_throw_exception_if_app_name_is_free() + { + var command = new CreateApp { Name = "new-app" }; + + return GuardApp.CanCreate(command, apps); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_null() + { + var command = new ChangePlan { Actor = new RefToken("user", "me") }; + + AppPlan plan = null; + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + A.CallTo(() => appPlans.GetPlan("free")) + .Returns(null); + + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + AppPlan plan = null; + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() + { + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "other"), "premium"); + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_is_the_same() + { + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "me"), "free"); + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() + { + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "me"), "premium"); + + GuardApp.CanChangePlan(command, plan, appPlans); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs new file mode 100644 index 000000000..76162f128 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// NoopAppPlanBillingManagerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class NoopAppPlanBillingManagerTests + { + private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); + + [Fact] + public void Should_not_have_portal() + { + Assert.False(sut.HasPortal); + } + + [Fact] + public async Task Should_do_nothing_when_changing_plan() + { + await sut.ChangePlanAsync(null, Guid.Empty, null, null); + } + + [Fact] + public async Task Should_not_return_portal_link() + { + Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs new file mode 100644 index 000000000..90fddc2c4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -0,0 +1,220 @@ +// ========================================================================== +// ContentQueryServiceTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.OData.UriParser; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Edm; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentQueryServiceTests + { + private readonly IContentRepository contentRepository = A.Fake(); + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly ISchemaEntity schema = A.Fake(); + private readonly IContentEntity content = A.Fake(); + private readonly IAppEntity app = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid contentId = Guid.NewGuid(); + private readonly string appName = "my-app"; + private readonly NamedContentData data = new NamedContentData(); + private readonly NamedContentData transformedData = new NamedContentData(); + private readonly ClaimsPrincipal user; + private readonly ClaimsIdentity identity = new ClaimsIdentity(); + private readonly EdmModelBuilder modelBuilder = A.Fake(); + private readonly ContentQueryService sut; + + public ContentQueryServiceTests() + { + user = new ClaimsPrincipal(identity); + + A.CallTo(() => app.Id).Returns(appId); + A.CallTo(() => app.Name).Returns(appName); + + A.CallTo(() => content.Id).Returns(contentId); + A.CallTo(() => content.Data).Returns(data); + A.CallTo(() => content.Status).Returns(Status.Published); + + sut = new ContentQueryService(contentRepository, appProvider, scriptEngine, modelBuilder); + } + + [Fact] + public async Task Should_return_schema_from_id_if_string_is_guid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + .Returns(schema); + + var result = await sut.FindSchemaAsync(app, schemaId.ToString()); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_return_schema_from_name_if_string_not_guid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) + .Returns(schema); + + var result = await sut.FindSchemaAsync(app, "my-schema"); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_throw_if_schema_not_found() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) + .Returns((ISchemaEntity)null); + + await Assert.ThrowsAsync(() => sut.FindSchemaAsync(app, "my-schema")); + } + + [Fact] + public async Task Should_return_content_from_repository_and_transform() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + .Returns(schema); + A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) + .Returns(content); + + A.CallTo(() => schema.ScriptQuery) + .Returns(""); + + A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "")) + .Returns(transformedData); + + var result = await sut.FindContentAsync(app, schemaId.ToString(), user, contentId); + + Assert.Equal(schema, result.Schema); + Assert.Equal(data, result.Content.Data); + Assert.Equal(content.Id, result.Content.Id); + } + + [Fact] + public async Task Should_throw_if_content_to_find_does_not_exist() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + .Returns(schema); + A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) + .Returns((IContentEntity)null); + + await Assert.ThrowsAsync(async () => await sut.FindContentAsync(app, schemaId.ToString(), user, contentId)); + } + + [Fact] + public async Task Should_return_contents_with_ids_from_repository_and_transform() + { + await TestManyIdRequest(true, false, new HashSet { Guid.NewGuid() }, Status.Draft, Status.Published); + } + + [Fact] + public async Task Should_return_non_archived_contents_from_repository_and_transform() + { + await TestManyRequest(true, false, Status.Draft, Status.Published); + } + + [Fact] + public async Task Should_return_archived_contents_from_repository_and_transform() + { + await TestManyRequest(true, true, Status.Archived); + } + + [Fact] + public async Task Should_return_draft_contents_from_repository_and_transform() + { + await TestManyRequest(false, false, Status.Published); + } + + [Fact] + public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend() + { + await TestManyRequest(false, true, Status.Published); + } + + private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status) + { + SetupClaims(isFrontend); + + SetupFakeWithOdataQuery(status); + SetupFakeWithScripting(); + + var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, string.Empty); + + Assert.Equal(123, result.Total); + Assert.Equal(schema, result.Schema); + Assert.Equal(data, result.Items[0].Data); + Assert.Equal(content.Id, result.Items[0].Id); + } + + private async Task TestManyIdRequest(bool isFrontend, bool archive, HashSet ids, params Status[] status) + { + SetupClaims(isFrontend); + + SetupFakeWithIdQuery(status, ids); + SetupFakeWithScripting(); + + var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, ids); + + Assert.Equal(123, result.Total); + Assert.Equal(schema, result.Schema); + Assert.Equal(data, result.Items[0].Data); + Assert.Equal(content.Id, result.Items[0].Id); + } + + private void SetupClaims(bool isFrontend) + { + if (isFrontend) + { + identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend")); + } + } + + private void SetupFakeWithIdQuery(Status[] status, HashSet ids) + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + .Returns(schema); + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) + .Returns(new List { content }); + A.CallTo(() => contentRepository.CountAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) + .Returns(123); + } + + private void SetupFakeWithOdataQuery(Status[] status) + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + .Returns(schema); + A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) + .Returns(new List { content }); + A.CallTo(() => contentRepository.CountAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) + .Returns(123); + } + + private void SetupFakeWithScripting() + { + A.CallTo(() => schema.ScriptQuery) + .Returns(""); + + A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "")) + .Returns(transformedData); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs new file mode 100644 index 000000000..c4624d1ff --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs @@ -0,0 +1,689 @@ +// ========================================================================== +// GraphQLTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NodaTime.Extensions; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Contents.TestData; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class GraphQLTests + { + private static readonly Guid schemaId = Guid.NewGuid(); + private static readonly Guid appId = Guid.NewGuid(); + private static readonly string appName = "my-app"; + private readonly Schema schemaDef; + private readonly IContentQueryService contentQuery = A.Fake(); + private readonly IAssetRepository assetRepository = A.Fake(); + private readonly ISchemaEntity schema = A.Fake(); + private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppEntity app = A.Dummy(); + private readonly ClaimsPrincipal user = new ClaimsPrincipal(); + private readonly IGraphQLService sut; + + public GraphQLTests() + { + schemaDef = + new Schema("my-schema") + .AddField(new JsonField(1, "my-json", Partitioning.Invariant, + new JsonFieldProperties())) + .AddField(new StringField(2, "my-string", Partitioning.Language, + new StringFieldProperties())) + .AddField(new NumberField(3, "my-number", Partitioning.Invariant, + new NumberFieldProperties())) + .AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, + new AssetsFieldProperties())) + .AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties())) + .AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties())) + .AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId })) + .AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) + .AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties())) + .AddField(new TagsField(11, "my-tags", Partitioning.Invariant, + new TagsFieldProperties())); + + A.CallTo(() => app.Id).Returns(appId); + A.CallTo(() => app.Name).Returns(appName); + A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE)); + + A.CallTo(() => schema.Id).Returns(schemaId); + A.CallTo(() => schema.Name).Returns(schemaDef.Name); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.IsPublished).Returns(true); + A.CallTo(() => schema.ScriptQuery).Returns(""); + + var allSchemas = new List { schema }; + + A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas); + + sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_empty_object_for_empty_query(string query) + { + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_return_multiple_assets_when_querying_assets() + { + const string query = @" + query { + queryAssets(search: ""my-query"", top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + } + }"; + + var asset = CreateAsset(Guid.NewGuid()); + + var assets = new List { asset }; + + A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) + .Returns(assets); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssets = new dynamic[] + { + new + { + id = asset.Id, + version = 1, + created = asset.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = asset.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600 + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_return_single_asset_when_finding_asset() + { + var assetId = Guid.NewGuid(); + var asset = CreateAsset(Guid.NewGuid()); + + var query = $@" + query {{ + findAsset(id: ""{assetId}"") {{ + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + }} + }}"; + + A.CallTo(() => assetRepository.FindAssetAsync(assetId)) + .Returns(asset); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = new + { + id = asset.Id, + version = 1, + created = asset.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = asset.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600 + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_return_multiple_contents_when_querying_contents() + { + const string query = @" + query { + queryMySchemaContents(top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + var contents = new List { content }; + + A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, "?$top=30&$skip=5")) + .Returns((schema, 0L, (IReadOnlyList)contents)); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContents = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = content.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified.ToDateTimeUtc() + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + } + } + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_return_single_content_when_finding_content() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + version + created + createdBy + lastModified + lastModifiedBy + url + data {{ + myString {{ + de + }} + myNumber {{ + iv + }} + myBoolean {{ + iv + }} + myDatetime {{ + iv + }} + myJson {{ + iv + }} + myGeolocation {{ + iv + }} + myTags {{ + iv + }} + }} + }} + }}"; + + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + .Returns((schema, content)); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + version = 1, + created = content.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = content.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified.ToDateTimeUtc() + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + } + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = CreateContent(contentRefId, Guid.Empty, Guid.Empty); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + data {{ + myReferences {{ + iv {{ + id + }} + }} + }} + }} + }}"; + + var refContents = new List { contentRef }; + + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + .Returns((schema, content)); + + A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) + .Returns((schema, 0L, (IReadOnlyList)refContents)); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myReferences = new + { + iv = new[] + { + new + { + id = contentRefId + } + } + } + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() + { + var assetRefId = Guid.NewGuid(); + var assetRef = CreateAsset(assetRefId); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, assetRefId); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + data {{ + myAssets {{ + iv {{ + id + }} + }} + }} + }} + }}"; + + var refAssets = new List { assetRef }; + + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + .Returns((schema, content)); + + A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) + .Returns(refAssets); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myAssets = new + { + iv = new[] + { + new + { + id = assetRefId + } + } + } + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + + [Fact] + public async Task Should_not_return_data_when_field_not_part_of_content() + { + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + version + created + createdBy + lastModified + lastModifiedBy + url + data {{ + myInvalid {{ + iv + }} + }} + }} + }}"; + + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + .Returns((schema, content)); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = (object)null + }; + + AssertJson(expected, new { data = result.Data }); + } + + private static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null) + { + var now = DateTime.UtcNow.ToInstant(); + + data = data ?? + new NamedContentData() + .AddField("my-json", + new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 }))) + .AddField("my-string", + new ContentFieldData().AddValue("de", "value")) + .AddField("my-assets", + new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId }))) + .AddField("my-number", + new ContentFieldData().AddValue("iv", 1)) + .AddField("my-boolean", + new ContentFieldData().AddValue("iv", true)) + .AddField("my-datetime", + new ContentFieldData().AddValue("iv", now.ToDateTimeUtc())) + .AddField("my-tags", + new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" }))) + .AddField("my-references", + new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId }))) + .AddField("my-geolocation", + new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); + + var content = new FakeContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken("subject", "user1"), + LastModified = now, + LastModifiedBy = new RefToken("subject", "user2"), + Data = data + }; + + return content; + } + + private static IAssetEntity CreateAsset(Guid id) + { + var now = DateTime.UtcNow.ToInstant(); + + var asset = new FakeAssetEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken("subject", "user1"), + LastModified = now, + LastModifiedBy = new RefToken("subject", "user2"), + FileName = "MyFile.png", + FileSize = 1024, + FileVersion = 123, + MimeType = "image/png", + IsImage = true, + PixelWidth = 800, + PixelHeight = 600 + }; + + return asset; + } + + private static void AssertJson(object expected, object result) + { + var resultJson = JsonConvert.SerializeObject(result, Formatting.Indented); + var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented); + + Assert.Equal(expectJson, resultJson); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs new file mode 100644 index 000000000..bd00e634b --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs @@ -0,0 +1,394 @@ +// ========================================================================== +// ODataQueryTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +/* +using System; +using System.Collections.Immutable; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OData.Edm; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents.Edm; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ODataQueryTests + { + private readonly Schema schemaDef; + private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; + private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly IEdmModel edmModel; + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + static ODataQueryTests() + { + InstantSerializer.Register(); + } + + public ODataQueryTests() + { + schemaDef = + new Schema("user") + .AddField(new StringField(1, "firstName", Partitioning.Language, + new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })) + .AddField(new StringField(2, "lastName", Partitioning.Language, + new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })) + .AddField(new BooleanField(3, "isAdmin", Partitioning.Invariant, + new BooleanFieldProperties())) + .AddField(new NumberField(4, "age", Partitioning.Invariant, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) + .AddField(new DateTimeField(5, "birthday", Partitioning.Invariant, + new DateTimeFieldProperties())) + .AddField(new AssetsField(6, "pictures", Partitioning.Invariant, + new AssetsFieldProperties())) + .AddField(new ReferencesField(7, "friends", Partitioning.Invariant, + new ReferencesFieldProperties())) + .AddField(new StringField(8, "dashed-field", Partitioning.Invariant, + new StringFieldProperties())) + .Update(new SchemaProperties { Hints = "The User" }); + + var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); + + var schema = A.Dummy(); + A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); + A.CallTo(() => schema.Version).Returns(3); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + + var app = A.Dummy(); + A.CallTo(() => app.Id).Returns(Guid.NewGuid()); + A.CallTo(() => app.Version).Returns(3); + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + + edmModel = builder.BuildEdmModel(schema, app); + } + + [Fact] + public void Should_parse_query() + { + var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); + + Assert.NotNull(parser); + } + + [Fact] + public void Should_make_query_with_underscore_field() + { + var i = F("$filter=data/dashed_field/iv eq 'Value'"); + var o = C("{ 'do.8.iv' : 'Value' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_not_operator() + { + var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')"); + var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_starts_with_query() + { + var i = F("$filter=startswith(data/firstName/de, 'Sebastian')"); + var o = C("{ 'do.1.de' : /^Sebastian/i }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_ends_with_query() + { + var i = F("$filter=endswith(data/firstName/de, 'Sebastian')"); + var o = C("{ 'do.1.de' : /Sebastian$/i }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_contains_query() + { + var i = F("$filter=contains(data/firstName/de, 'Sebastian')"); + var o = C("{ 'do.1.de' : /Sebastian/i }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_contains_query_with_equals() + { + var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true"); + var o = C("{ 'do.1.de' : /Sebastian/i }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_negated_contains_query_with_equals() + { + var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false"); + var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_negated_contains_query_and_other() + { + var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true"); + var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_string_equals_query() + { + var i = F("$filter=data/firstName/de eq 'Sebastian'"); + var o = C("{ 'do.1.de' : 'Sebastian' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_datetime_equals_query() + { + var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z"); + var o = C("{ 'do.5.iv' : ISODate(\"1988-01-19T12:00:00Z\") }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_boolean_equals_query() + { + var i = F("$filter=data/isAdmin/iv eq true"); + var o = C("{ 'do.3.iv' : true }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_string_not_equals_query() + { + var i = F("$filter=data/firstName/de ne 'Sebastian'"); + var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_number_less_than_query() + { + var i = F("$filter=data/age/iv lt 1"); + var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_number_less_equals_query() + { + var i = F("$filter=data/age/iv le 1"); + var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_number_greater_than_query() + { + var i = F("$filter=data/age/iv gt 1"); + var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_number_greater_equals_query() + { + var i = F("$filter=data/age/iv ge 1"); + var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_equals_query_for_assets() + { + var i = F("$filter=data/pictures/iv eq 'guid'"); + var o = C("{ 'do.6.iv' : 'guid' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_equals_query_for_references() + { + var i = F("$filter=data/friends/iv eq 'guid'"); + var o = C("{ 'do.7.iv' : 'guid' }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_and_query() + { + var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2"); + var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_or_query() + { + var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2"); + var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_full_text_query() + { + var i = F("$search=Hello my World"); + var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_create_full_text_query_with_and() + { + var i = F("$search=A and B"); + var o = C("{ '$text' : { '$search' : 'A and B' } }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_convert_orderby_with_single_statements() + { + var i = S("$orderby=data/age/iv desc"); + var o = C("{ 'do.4.iv' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_convert_orderby_with_multiple_statements() + { + var i = S("$orderby=data/age/iv, data/firstName/en desc"); + var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_set_top() + { + var parser = edmModel.ParseQuery("$top=3"); + var cursor = A.Fake>(); + + cursor.Take(parser); + + A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); + } + + [Fact] + public void Should_set_max_top_if_larger() + { + var parser = edmModel.ParseQuery("$top=300"); + var cursor = A.Fake>(); + + cursor.Take(parser); + + A.CallTo(() => cursor.Limit(200)).MustHaveHappened(); + } + + [Fact] + public void Should_set_default_top() + { + var parser = edmModel.ParseQuery(string.Empty); + var cursor = A.Fake>(); + + cursor.Take(parser); + + A.CallTo(() => cursor.Limit(20)).MustHaveHappened(); + } + + [Fact] + public void Should_set_skip() + { + var parser = edmModel.ParseQuery("$skip=3"); + var cursor = A.Fake>(); + + cursor.Skip(parser); + + A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); + } + + [Fact] + public void Should_not_set_skip() + { + var parser = edmModel.ParseQuery(string.Empty); + var cursor = A.Fake>(); + + cursor.Take(parser); + + A.CallTo(() => cursor.Skip(A.Ignored)).MustNotHaveHappened(); + } + + private static string C(string value) + { + return value.Replace('\'', '"'); + } + + private string S(string value) + { + var parser = edmModel.ParseQuery(value); + var cursor = A.Fake>(); + + var i = string.Empty; + + A.CallTo(() => cursor.Sort(A>.Ignored)) + .Invokes((SortDefinition sortDefinition) => + { + i = sortDefinition.Render(serializer, registry).ToString(); + }); + + cursor.Sort(parser, schemaDef); + + return i; + } + + private string F(string value) + { + var parser = edmModel.ParseQuery(value); + + var query = FilterBuilder.Build(parser, schemaDef).Render(serializer, registry).ToString(); + + return query; + } + } +} +*/ \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs new file mode 100644 index 000000000..261c1d4f0 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// MockupAssetEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.TestData +{ + public sealed class FakeAssetEntity : IAssetEntity + { + public Guid Id { get; set; } + + public Guid AppId { get; set; } + + public Guid AssetId { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public long Version { get; set; } + + public string MimeType { get; set; } + + public string FileName { get; set; } + + public long FileSize { get; set; } + + public long FileVersion { get; set; } + + public bool IsImage { get; set; } + + public bool IsDeleted { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs new file mode 100644 index 000000000..fe06f5165 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// FakeContentEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.TestData +{ + public sealed class FakeContentEntity : IContentEntity + { + public Guid Id { get; set; } + + public Guid AppId { get; set; } + + public long Version { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public NamedContentData Data { get; set; } + + public Status Status { get; set; } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs new file mode 100644 index 000000000..900d01fc1 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// FakeUrlGenerator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.TestData +{ + public sealed class FakeUrlGenerator : IGraphQLUrlGenerator + { + public bool CanGenerateAssetSourceUrl { get; } = true; + + public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) + { + return $"assets/{asset.Id}"; + } + + public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) + { + return $"assets/{asset.Id}?width=100"; + } + + public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) + { + return $"assets/source/{asset.Id}"; + } + + public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) + { + return $"contents/{schema.Name}/{content.Id}"; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs new file mode 100644 index 000000000..288d66e22 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// RuleDequeuerGrainTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure.Log; +using Xunit; + +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleDequeuerTests + { + private readonly IClock clock = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly RuleService ruleService = A.Fake(); + private readonly RuleDequeuer sut; + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + + public RuleDequeuerTests() + { + A.CallTo(() => clock.GetCurrentInstant()).Returns(now); + + sut = new RuleDequeuer( + ruleService, + ruleEventRepository, + log, + clock); + } + + [Theory] + [InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] + [InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] + public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) + { + var actionData = new RuleJobData(); + var actionName = "MyAction"; + + var @event = CreateEvent(calls, actionName, actionData); + + var requestElapsed = TimeSpan.FromMinutes(1); + var requestDump = "Dump"; + + A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) + .Returns((requestDump, result, requestElapsed)); + + Instant? nextCall = null; + + if (minutes > 0) + { + nextCall = now.Plus(Duration.FromMinutes(minutes)); + } + + await sut.HandleAsync(@event); + + sut.Dispose(); + + A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, requestDump, result, jobResult, requestElapsed, nextCall)) + .MustHaveHappened(); + } + + private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) + { + var @event = A.Fake(); + + var job = new RuleJob + { + RuleId = Guid.NewGuid(), + ActionData = actionData, + ActionName = actionName, + Created = now + }; + + A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); + A.CallTo(() => @event.Job).Returns(job); + A.CallTo(() => @event.Created).Returns(now); + A.CallTo(() => @event.NumCalls).Returns(numCalls); + + return @event; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs new file mode 100644 index 000000000..3cd294d17 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// RuleEnqueuerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleEnqueuerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly RuleService ruleService = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly RuleEnqueuer sut; + + public RuleEnqueuerTests() + { + sut = new RuleEnqueuer( + ruleEventRepository, + appProvider, + ruleService); + } + + [Fact] + public void Should_return_contents_filter_for_events_filter() + { + Assert.Equal(".*", sut.EventsFilter); + } + + [Fact] + public void Should_return_type_name_for_name() + { + Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); + } + + [Fact] + public Task Should_do_nothing_on_clear() + { + return sut.ClearAsync(); + } + + [Fact] + public async Task Should_update_repositories_on_with_jobs_from_sender() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule1 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + var rule2 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + var rule3 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + + var job1 = new RuleJob { Created = now }; + var job2 = new RuleJob { Created = now }; + + var ruleEntity1 = A.Fake(); + var ruleEntity2 = A.Fake(); + var ruleEntity3 = A.Fake(); + + A.CallTo(() => ruleEntity1.RuleDef).Returns(rule1); + A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); + A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); + + A.CallTo(() => appProvider.GetRulesAsync(appId.Name)) + .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); + + A.CallTo(() => ruleService.CreateJob(rule1, @event)) + .Returns(job1); + + A.CallTo(() => ruleService.CreateJob(rule2, @event)) + .Returns(job2); + + A.CallTo(() => ruleService.CreateJob(rule3, @event)) + .Returns(null); + + await sut.On(@event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) + .MustHaveHappened(); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) + .MustHaveHappened(); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj new file mode 100644 index 000000000..eab4caa37 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -0,0 +1,36 @@ + + + Exe + netcoreapp2.0 + Squidex.Domain.Apps.Entities + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs new file mode 100644 index 000000000..6003f51c4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// AssertHelper.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public static class AssertHelper + { + public static void ShouldHaveSameEvents(this IEnumerable> events, params IEvent[] others) + { + var source = events.Select(x => x.Payload).ToArray(); + + source.Should().HaveSameCount(others); + + for (var i = 0; i < source.Length; i++) + { + var lhs = source[i]; + var rhs = others[i]; + + lhs.ShouldBeSameEvent(rhs); + } + } + + public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) + { + lhs.Should().BeOfType(rhs.GetType()); + + ((object)lhs).ShouldBeEquivalentTo(rhs, o => o.IncludingAllDeclaredProperties()); + } + + public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs) + { + lhs.Should().BeOfType(rhs.GetType()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs new file mode 100644 index 000000000..763b0db25 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -0,0 +1,162 @@ +// ========================================================================== +// HandlerTestBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +#pragma warning disable IDE0019 // Use pattern matching + +namespace Squidex.Domain.Apps.Entities.TestHelpers +{ + public abstract class HandlerTestBase where T : IDomainObject + { + private sealed class MockupHandler : IAggregateHandler + { + private T domainObject; + + public bool IsCreated { get; private set; } + public bool IsUpdated { get; private set; } + + public void Init(T newDomainObject) + { + domainObject = newDomainObject; + + IsCreated = false; + IsUpdated = false; + } + + public async Task CreateAsync(CommandContext context, Func creator) where V : class, IDomainObject + { + IsCreated = true; + + var @do = domainObject as V; + + await creator(domainObject as V); + + return @do; + } + + public async Task UpdateAsync(CommandContext context, Func updater) where V : class, IDomainObject + { + IsUpdated = true; + + var @do = domainObject as V; + + await updater(domainObject as V); + + return @do; + } + } + + private readonly MockupHandler handler = new MockupHandler(); + + protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString()); + + protected Guid AppId { get; } = Guid.NewGuid(); + + protected Guid SchemaId { get; } = Guid.NewGuid(); + + protected string AppName { get; } = "my-app"; + + protected string SchemaName { get; } = "my-schema"; + + protected NamedId AppNamedId + { + get { return new NamedId(AppId, AppName); } + } + + protected NamedId SchemaNamedId + { + get { return new NamedId(SchemaId, SchemaName); } + } + + protected IAggregateHandler Handler + { + get { return handler; } + } + + protected CommandContext CreateContextForCommand(TCommand command) where TCommand : SquidexCommand + { + return new CommandContext(CreateCommand(command)); + } + + protected async Task TestCreate(T domainObject, Func action, bool shouldCreate = true) + { + handler.Init(domainObject); + + await action(domainObject); + + if (!handler.IsCreated && shouldCreate) + { + throw new InvalidOperationException("Create not called."); + } + } + + protected async Task TestUpdate(T domainObject, Func action, bool shouldUpdate = true) + { + handler.Init(domainObject); + + await action(domainObject); + + if (!handler.IsUpdated && shouldUpdate) + { + throw new InvalidOperationException("Update not called."); + } + } + + protected TCommand CreateCommand(TCommand command) where TCommand : SquidexCommand + { + if (command.Actor == null) + { + command.Actor = User; + } + + var appCommand = command as AppCommand; + + if (appCommand != null && appCommand.AppId == null) + { + appCommand.AppId = AppNamedId; + } + + var schemaCommand = command as SchemaCommand; + + if (schemaCommand != null && schemaCommand.SchemaId == null) + { + schemaCommand.SchemaId = SchemaNamedId; + } + + return command; + } + + protected TEvent CreateEvent(TEvent @event) where TEvent : SquidexEvent + { + @event.Actor = User; + + var appEvent = @event as AppEvent; + + if (appEvent != null) + { + appEvent.AppId = AppNamedId; + } + + var schemaEvent = @event as SchemaEvent; + + if (schemaEvent != null) + { + schemaEvent.SchemaId = SchemaNamedId; + } + + return @event; + } + } +} + +#pragma warning restore IDE0019 // Use pattern matching \ No newline at end of file From 220947f6d2a62eea2498a9e55ce5b1e611073601 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 4 Dec 2017 23:13:03 +0100 Subject: [PATCH 03/30] Tests improved. --- .../Apps/AppCommandMiddlewareTests.cs | 2 +- .../Apps/AppDomainObjectTests.cs | 18 +++++++++++++++++- .../Rules/RuleDequeuerTests.cs | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index 05b7b1069..c095e4868 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -26,11 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); - private readonly AppCommandMiddleware sut; private readonly AppDomainObject app; private readonly Language language = Language.DE; private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientName = "client"; + private readonly AppCommandMiddleware sut; public AppCommandMiddlewareTests() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index c94358368..801feffba 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -20,11 +20,11 @@ namespace Squidex.Domain.Apps.Entities.Apps { public class AppDomainObjectTests : HandlerTestBase { - private readonly AppDomainObject sut; private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientId = "client"; private readonly string clientNewName = "My Client"; private readonly string planId = "premium"; + private readonly AppDomainObject sut; public AppDomainObjectTests() { @@ -73,6 +73,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); + Assert.Equal(planId, sut.State.Plan.PlanId); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppPlanChanged { PlanId = planId }) @@ -95,6 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); + Assert.Equal(AppContributorPermission.Editor, sut.State.Contributors[contributorId]); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) @@ -118,6 +122,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); + Assert.False(sut.State.Contributors.ContainsKey(contributorId)); + sut.GetUncomittedEvents().Skip(1) .ShouldHaveSameEvents( CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) @@ -142,6 +148,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.AttachClient(CreateCommand(command)); + Assert.True(sut.State.Clients.ContainsKey(clientId)); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) @@ -165,6 +173,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); + Assert.False(sut.State.Clients.ContainsKey(clientId)); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppClientRevoked { Id = clientId }) @@ -188,6 +198,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); + Assert.Equal(clientNewName, sut.State.Clients[clientId].Name); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), @@ -211,6 +223,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); + Assert.True(sut.State.LanguagesConfig.Contains(Language.DE)); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppLanguageAdded { Language = Language.DE }) @@ -234,6 +248,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); + Assert.False(sut.State.LanguagesConfig.Contains(Language.DE)); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppLanguageRemoved { Language = Language.DE }) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs index 288d66e22..3737294be 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs @@ -26,9 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly ISemanticLog log = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); private readonly RuleService ruleService = A.Fake(); private readonly RuleDequeuer sut; - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); public RuleDequeuerTests() { From 0b381df408445ea060005cff21dab2139279a1b9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 5 Dec 2017 18:22:03 +0100 Subject: [PATCH 04/30] Tests for rules. --- .../SquidexCommand.cs | 3 - .../Commands/IAggregate.cs | 27 -- .../Apps/AppDomainObjectTests.cs | 2 + .../Guards/Actions/WebhookActionTests.cs | 48 ++++ .../Rules/Guards/GuardRuleTests.cs | 167 ++++++++++++ .../Triggers/ContentChangedTriggerTests.cs | 84 ++++++ .../Rules/RuleCommandMiddlewareTests.cs | 116 ++++++++ .../Rules/RuleDomainObjectTests.cs | 249 ++++++++++++++++++ 8 files changed, 666 insertions(+), 30 deletions(-) delete mode 100644 src/Squidex.Infrastructure/Commands/IAggregate.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/WebhookActionTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs diff --git a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs index 58e1087a0..ae4c6cb8d 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System.Security.Claims; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -14,8 +13,6 @@ namespace Squidex.Domain.Apps.Entities { public abstract class SquidexCommand : ICommand { - public ClaimsPrincipal Principal { get; set; } - public RefToken Actor { get; set; } public long? ExpectedVersion { get; set; } diff --git a/src/Squidex.Infrastructure/Commands/IAggregate.cs b/src/Squidex.Infrastructure/Commands/IAggregate.cs deleted file mode 100644 index abc522e2a..000000000 --- a/src/Squidex.Infrastructure/Commands/IAggregate.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// IAggregate.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.Commands -{ - public interface IAggregate - { - Guid Id { get; } - - int Version { get; } - - void ApplyEvent(Envelope @event); - - void ClearUncommittedEvents(); - - ICollection> GetUncomittedEvents(); - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index 801feffba..acac9d548 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -273,6 +273,8 @@ namespace Squidex.Domain.Apps.Entities.Apps sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } })); + Assert.True(sut.State.LanguagesConfig.Contains(Language.DE)); + sut.GetUncomittedEvents() .ShouldHaveSameEvents( CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/WebhookActionTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/WebhookActionTests.cs new file mode 100644 index 000000000..fed8f64ba --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/WebhookActionTests.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// WebhookActionTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions +{ + public sealed class WebhookActionTests + { + [Fact] + public async Task Should_add_error_if_url_is_null() + { + var action = new WebhookAction { Url = null }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_add_error_if_url_is_relative() + { + var action = new WebhookAction { Url = new Uri("/invalid", UriKind.Relative) }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_url_is_absolute() + { + var action = new WebhookAction { Url = new Uri("https://squidex.io", UriKind.Absolute) }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs new file mode 100644 index 000000000..949759999 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// GuardRuleTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Rules.Guards +{ + public class GuardRuleTests + { + private readonly Uri validUrl = new Uri("https://squidex.io"); + private readonly Rule rule_0 = new Rule(new ContentChangedTrigger(), new WebhookAction()); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly IAppProvider appProvider = A.Fake(); + + public GuardRuleTests() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Name, A.Ignored, false)) + .Returns(A.Fake()); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_trigger_null() + { + var command = CreateCommand(new CreateRule + { + Trigger = null, + Action = new WebhookAction + { + Url = validUrl + } + }); + + await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_action_null() + { + var command = CreateCommand(new CreateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }, + Action = null + }); + + await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = CreateCommand(new CreateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }, + Action = new WebhookAction + { + Url = validUrl + } + }); + + await GuardRule.CanCreate(command, appProvider); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() + { + var command = new UpdateRule(); + + await Assert.ThrowsAsync(() => GuardRule.CanUpdate(command, appProvider)); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = CreateCommand(new UpdateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }, + Action = new WebhookAction + { + Url = validUrl + } + }); + + await GuardRule.CanUpdate(command, appProvider); + } + + [Fact] + public void CanEnable_should_throw_exception_if_rule_enabled() + { + var command = new EnableRule(); + + var rule_1 = rule_0.Enable(); + + Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_rule_disabled() + { + var command = new EnableRule(); + + var rule_1 = rule_0.Disable(); + + GuardRule.CanEnable(command, rule_1); + } + + [Fact] + public void CanDisable_should_throw_exception_if_rule_disabled() + { + var command = new DisableRule(); + + var rule_1 = rule_0.Disable(); + + Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); + } + + [Fact] + public void CanDisable_should_not_throw_exception_if_rule_enabled() + { + var command = new DisableRule(); + + var rule_1 = rule_0.Enable(); + + GuardRule.CanDisable(command, rule_1); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteRule(); + + GuardRule.CanDelete(command); + } + + private T CreateCommand(T command) where T : AppCommand + { + command.AppId = appId; + + return command; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs new file mode 100644 index 000000000..cb495d264 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// ContentChangedTriggerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers +{ + public class ContentChangedTriggerTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly string appName = "my-app"; + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) + .Returns(Task.FromResult(null)); + + var trigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Create( + new ContentChangedTriggerSchema() + ) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_null() + { + var trigger = new ContentChangedTrigger(); + + var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_empty() + { + var trigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_ids_are_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) + .Returns(A.Fake()); + + var trigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Create( + new ContentChangedTriggerSchema() + ) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs new file mode 100644 index 000000000..aa9b3abe9 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// RuleCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleCommandMiddlewareTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly RuleCommandMiddleware sut; + private readonly RuleDomainObject rule; + private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); + private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; + private readonly Guid ruleId = Guid.NewGuid(); + + public RuleCommandMiddlewareTests() + { + A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) + .Returns(A.Fake()); + + rule = new RuleDomainObject(); + + sut = new RuleCommandMiddleware(Handler, appProvider); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + + await TestCreate(rule, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }); + + CreateRule(); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Enable_should_update_domain_object() + { + CreateRule(); + DisableRule(); + + var command = CreateContextForCommand(new EnableRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + [Fact] + public async Task Disable_should_update_domain_object() + { + CreateRule(); + + var command = CreateContextForCommand(new DisableRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateRule(); + + var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void DisableRule() + { + rule.Disable(new DisableRule()); + } + + private void CreateRule() + { + rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs new file mode 100644 index 000000000..10dd3948b --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -0,0 +1,249 @@ +// ========================================================================== +// RuleDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Immutable; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleDomainObjectTests : HandlerTestBase + { + private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); + private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; + private readonly RuleDomainObject sut; + + public Guid RuleId { get; } = Guid.NewGuid(); + + public RuleDomainObjectTests() + { + sut = new RuleDomainObject(); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + + Assert.Throws(() => + { + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Create_should_create_events() + { + var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction }; + + sut.Create(CreateRuleCommand(command)); + + Assert.Same(ruleTrigger, sut.State.RuleDef.Trigger); + Assert.Same(ruleAction, sut.State.RuleDef.Action); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Update_should_create_events() + { + var newTrigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }; + + var newAction = new WebhookAction + { + Url = new Uri("https://squidex.io/v2") + }; + + CreateRule(); + + var command = new UpdateRule { Trigger = newTrigger, Action = newAction }; + + sut.Update(CreateRuleCommand(command)); + + Assert.Same(newTrigger, sut.State.RuleDef.Trigger); + Assert.Same(newAction, sut.State.RuleDef.Action); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction }) + ); + } + + [Fact] + public void Enable_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Enable(CreateRuleCommand(new EnableRule())); + }); + } + + [Fact] + public void Enable_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Enable(CreateRuleCommand(new EnableRule())); + }); + } + + [Fact] + public void Enable_should_create_events() + { + CreateRule(); + + var command = new EnableRule(); + + sut.Enable(CreateRuleCommand(command)); + + Assert.True(sut.State.RuleDef.IsEnabled); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleEnabled()) + ); + } + + [Fact] + public void Disable_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Disable(CreateRuleCommand(new DisableRule())); + }); + } + + [Fact] + public void Disable_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Disable(CreateRuleCommand(new DisableRule())); + }); + } + + [Fact] + public void Disable_should_create_events() + { + CreateRule(); + + var command = new DisableRule(); + + sut.Disable(CreateRuleCommand(command)); + + Assert.False(sut.State.RuleDef.IsEnabled); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDisabled()) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + }); + } + + [Fact] + public void Delete_should_update_create_events() + { + CreateRule(); + + sut.Delete(CreateRuleCommand(new DeleteRule())); + + Assert.True(sut.State.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDeleted()) + ); + } + + private void CreateRule() + { + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); + sut.ClearUncommittedEvents(); + } + + private void DeleteRule() + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + sut.ClearUncommittedEvents(); + } + + protected T CreateRuleEvent(T @event) where T : RuleEvent + { + @event.RuleId = RuleId; + + return CreateEvent(@event); + } + + protected T CreateRuleCommand(T command) where T : RuleAggregateCommand + { + command.RuleId = RuleId; + + return CreateCommand(command); + } + } +} \ No newline at end of file From 50610d5cdd3913023def61aa3088d5e5b465ffcf Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 5 Dec 2017 22:49:08 +0100 Subject: [PATCH 05/30] Tests for assets. --- .../Assets/AssetDomainObject.cs | 16 +- .../DomainObjectState.cs | 1 + .../EntityMapper.cs | 31 ++- src/Squidex.Domain.Apps.Entities/IEntity.cs | 6 +- .../IUpdateableEntity.cs | 22 ++ .../EventSourcing/MongoEventStore.cs | 2 +- .../MongoDb/MongoRepositoryBase.cs | 5 +- .../States/MongoSnapshotStore.cs | 49 ++-- .../States/ISnapshotStore.cs | 6 +- .../States/Persistence.cs | 28 +-- .../States/StateFactory.cs | 12 +- src/Squidex.Infrastructure/States/Store.cs | 23 +- .../Apps/AppCommandMiddlewareTests.cs | 4 +- .../Apps/AppDomainObjectTests.cs | 7 +- .../Assets/AssetCommandMiddlewareTests.cs | 137 ++++++++++++ .../Assets/AssetDomainObjectTests.cs | 209 ++++++++++++++++++ .../Assets/Guards/GuardAssetTests.cs | 65 ++++++ .../Rules/RuleCommandMiddlewareTests.cs | 6 +- .../Rules/RuleDomainObjectTests.cs | 14 +- 19 files changed, 532 insertions(+), 111 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index b38838e99..d66b14a94 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -26,14 +26,19 @@ namespace Squidex.Domain.Apps.Entities.Assets { FileName = command.File.FileName, FileSize = command.File.FileSize, - FileVersion = State.FileVersion + 1, + FileVersion = 0, MimeType = command.File.MimeType, PixelWidth = command.ImageInfo?.PixelWidth, PixelHeight = command.ImageInfo?.PixelHeight, IsImage = command.ImageInfo != null }); - UpdateState(command, s => SimpleMapper.Map(@event, s)); + UpdateState(command, s => + { + s.TotalSize = @event.FileSize; + + SimpleMapper.Map(@event, s); + }); RaiseEvent(@event); @@ -54,7 +59,12 @@ namespace Squidex.Domain.Apps.Entities.Assets IsImage = command.ImageInfo != null }); - UpdateState(command, s => SimpleMapper.Map(@event, s)); + UpdateState(command, s => + { + s.TotalSize += @event.FileSize; + + SimpleMapper.Map(@event, s); + }); RaiseEvent(@event); diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index c13e058d7..3775b6244 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -14,6 +14,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, + IUpdateableEntity, IUpdateableEntityWithCreatedBy, IUpdateableEntityWithLastModifiedBy, IUpdateableEntityWithVersion diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index d100f596a..b7fbf2671 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -8,6 +8,7 @@ using System; using NodaTime; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { @@ -17,28 +18,24 @@ namespace Squidex.Domain.Apps.Entities { var timestamp = SystemClock.Instance.GetCurrentInstant(); + SetId(entity, command); SetAppId(entity, command); - SetVersion(entity); SetCreated(entity, timestamp); SetCreatedBy(entity, command); SetLastModified(entity, timestamp); SetLastModifiedBy(entity, command); + SetVersion(entity); updater?.Invoke(entity); return entity; } - private static void SetLastModified(IEntity entity, Instant timestamp) - { - entity.LastModified = timestamp; - } - - private static void SetCreated(IEntity entity, Instant timestamp) + private static void SetId(IEntity entity, SquidexCommand command) { - if (entity.Created == default(Instant)) + if (entity is IUpdateableEntity updateable && command is IAggregateCommand aggregateCommand) { - entity.Created = timestamp; + updateable.Id = aggregateCommand.AggregateId; } } @@ -50,6 +47,14 @@ namespace Squidex.Domain.Apps.Entities } } + private static void SetCreated(IEntity entity, Instant timestamp) + { + if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) + { + updateable.Created = timestamp; + } + } + private static void SetCreatedBy(IEntity entity, SquidexCommand command) { if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) @@ -58,6 +63,14 @@ namespace Squidex.Domain.Apps.Entities } } + private static void SetLastModified(IEntity entity, Instant timestamp) + { + if (entity is IUpdateableEntity updateable) + { + updateable.LastModified = timestamp; + } + } + private static void SetLastModifiedBy(IEntity entity, SquidexCommand command) { if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) diff --git a/src/Squidex.Domain.Apps.Entities/IEntity.cs b/src/Squidex.Domain.Apps.Entities/IEntity.cs index 9aa8ea2b0..a23a78d08 100644 --- a/src/Squidex.Domain.Apps.Entities/IEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/IEntity.cs @@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Entities { public interface IEntity { - Guid Id { get; set; } + Guid Id { get; } - Instant Created { get; set; } + Instant Created { get; } - Instant LastModified { get; set; } + Instant LastModified { get; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs new file mode 100644 index 000000000..7e1b2f03d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IUpdateableEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IUpdateableEntity + { + Guid Id { get; set; } + + Instant Created { get; set; } + + Instant LastModified { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 0d14e580e..6dd770faf 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -200,7 +200,7 @@ namespace Squidex.Infrastructure.EventSourcing { var document = await Collection.Find(Filter.Eq(EventStreamField, streamName)) - .Project(Project + .Project(Projection .Include(EventStreamOffsetField) .Include(EventsCountField)) .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 929757756..056dd8595 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -13,18 +13,21 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Tasks; +#pragma warning disable RECS0108 // Warns about static fields in generic types + namespace Squidex.Infrastructure.MongoDb { public abstract class MongoRepositoryBase : IExternalSystem { private const string CollectionFormat = "{0}Set"; + protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; protected static readonly SortDefinitionBuilder Sort = Builders.Sort; protected static readonly UpdateDefinitionBuilder Update = Builders.Update; protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; - protected static readonly ProjectionDefinitionBuilder Project = Builders.Projection; + protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; private readonly IMongoDatabase mongoDatabase; private Lazy> mongoCollection; diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index c9d728434..0eaa6c56e 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -6,46 +6,34 @@ // All rights reserved. // ========================================================================== -using System; using System.Threading.Tasks; using MongoDB.Driver; using Newtonsoft.Json; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public sealed class MongoSnapshotStore : ISnapshotStore, IExternalSystem + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - private readonly IMongoDatabase database; private readonly JsonSerializer serializer; public MongoSnapshotStore(IMongoDatabase database, JsonSerializer serializer) + : base(database) { - Guard.NotNull(database, nameof(database)); Guard.NotNull(serializer, nameof(serializer)); - this.database = database; this.serializer = serializer; } - public void Connect() + protected override string CollectionName() { - try - { - database.ListCollections(); - } - catch (Exception ex) - { - throw new ConfigurationException($"MongoDb connection failed to connect to database {database.DatabaseNamespace.DatabaseName}", ex); - } + return $"States_{typeof(T).Name}"; } - public async Task<(T Value, long Version)> ReadAsync(string key) + public async Task<(T Value, long Version)> ReadAsync(string key) { - var collection = GetCollection(); - var existing = - await collection.Find(x => x.Id == key) + await Collection.Find(x => x.Id == key) .FirstOrDefaultAsync(); if (existing != null) @@ -56,18 +44,16 @@ namespace Squidex.Infrastructure.States return (default(T), -1); } - public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) + public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) { - var collection = GetCollection(); - try { - await collection.UpdateOneAsync( - Builders>.Filter.And( - Builders>.Filter.Eq(x => x.Id, key), - Builders>.Filter.Eq(x => x.Version, oldVersion) + await Collection.UpdateOneAsync( + Filter.And( + Filter.Eq(x => x.Id, key), + Filter.Eq(x => x.Version, oldVersion) ), - Builders>.Update + Update .Set(x => x.Doc, value) .Set(x => x.Version, newVersion), Upsert); @@ -77,8 +63,8 @@ namespace Squidex.Infrastructure.States if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await collection.Find(x => x.Id == key) - .Project>(Builders>.Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key) + .Project>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); if (existingVersion != null) { @@ -91,10 +77,5 @@ namespace Squidex.Infrastructure.States } } } - - private IMongoCollection> GetCollection() - { - return database.GetCollection>($"States_{typeof(T).Name}"); - } } } diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 8b43eed65..38d62d737 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -10,10 +10,10 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.States { - public interface ISnapshotStore + public interface ISnapshotStore { - Task WriteAsync(string key, T value, long oldVersion, long newVersion); + Task WriteAsync(string key, T value, long oldVersion, long newVersion); - Task<(T Value, long Version)> ReadAsync(string key); + Task<(T Value, long Version)> ReadAsync(string key); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index b4a517616..d127964ea 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -13,10 +13,10 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public sealed class Persistence : IPersistence + internal sealed class Persistence : IPersistence { private readonly string ownerKey; - private readonly ISnapshotStore snapshotStore; + private readonly ISnapshotStore snapshotStore; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; @@ -30,13 +30,11 @@ namespace Squidex.Infrastructure.States Action invalidate, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, + ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, Func applyState, Func, Task> applyEvent) { - Guard.NotNull(ownerKey, nameof(ownerKey)); - this.ownerKey = ownerKey; this.applyState = applyState; this.applyEvent = applyEvent; @@ -54,7 +52,7 @@ namespace Squidex.Infrastructure.States if (snapshotStore != null) { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); + var (state, position) = await snapshotStore.ReadAsync(ownerKey); positionSnapshot = position; positionEvent = position; @@ -104,13 +102,10 @@ namespace Squidex.Infrastructure.States public async Task WriteSnapshotAsync(TState state) { - if (snapshotStore == null) - { - throw new InvalidOperationException("Snapshots are not supported."); - } - var newPosition = - eventStore != null ? positionEvent : positionSnapshot + 1; + eventStore != null ? + positionEvent : + positionSnapshot + 1; if (newPosition != positionSnapshot) { @@ -126,18 +121,13 @@ namespace Squidex.Infrastructure.States positionSnapshot = newPosition; } - invalidate(); + invalidate?.Invoke(); } public async Task WriteEventsAsync(params Envelope[] @events) { Guard.NotNull(events, nameof(@events)); - if (eventStore == null) - { - throw new InvalidOperationException("Events are not supported."); - } - if (@events.Length > 0) { var commitId = Guid.NewGuid(); @@ -157,7 +147,7 @@ namespace Squidex.Infrastructure.States positionEvent += events.Length; } - invalidate(); + invalidate?.Invoke(); } private EventData[] GetEventData(Envelope[] events, Guid commitId) diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index a0513c441..4394b8f06 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -21,7 +21,6 @@ namespace Squidex.Infrastructure.States private readonly IPubSub pubSub; private readonly IMemoryCache statesCache; private readonly IServiceProvider services; - private readonly ISnapshotStore snapshotStore; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; @@ -54,22 +53,19 @@ namespace Squidex.Infrastructure.States IEventStore eventStore, IEventDataFormatter eventDataFormatter, IServiceProvider services, - ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver) { Guard.NotNull(services, nameof(services)); Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(pubSub, nameof(pubSub)); - Guard.NotNull(snapshotStore, nameof(snapshotStore)); Guard.NotNull(statesCache, nameof(statesCache)); Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - this.services = services; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; this.pubSub = pubSub; - this.snapshotStore = snapshotStore; + this.services = services; this.statesCache = statesCache; this.streamNameResolver = streamNameResolver; } @@ -89,7 +85,7 @@ namespace Squidex.Infrastructure.States { Guard.NotNull(key, nameof(key)); - var stateStore = new Store(() => { }, eventStore, eventDataFormatter, snapshotStore, streamNameResolver); + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); var state = (T)services.GetService(typeof(T)); await state.ActivateAsync(key, stateStore); @@ -110,10 +106,10 @@ namespace Squidex.Infrastructure.States var state = (T)services.GetService(typeof(T)); - var stateStore = new Store(() => + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => { pubSub.Publish(new InvalidateMessage { Key = key }, false); - }, eventStore, eventDataFormatter, snapshotStore, streamNameResolver); + }); stateObj = new ObjectHolder(state, key, stateStore); diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 0fd3faab9..b217dac38 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -15,37 +15,46 @@ namespace Squidex.Infrastructure.States public sealed class Store : IStore { private readonly Action invalidate; - private readonly ISnapshotStore snapshotStore; + private readonly IServiceProvider services; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; public Store( - Action invalidate, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, - IStreamNameResolver streamNameResolver) + IServiceProvider services, + IStreamNameResolver streamNameResolver, + Action invalidate = null) { this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; this.invalidate = invalidate; - this.snapshotStore = snapshotStore; + this.services = services; this.streamNameResolver = streamNameResolver; } public IPersistence WithEventSourcing(string key, Func, Task> applyEvent) { - return new Persistence(key, invalidate, eventStore, eventDataFormatter, null, streamNameResolver, null, applyEvent); + return CreatePersistence(key, null, applyEvent); } public IPersistence WithSnapshots(string key, Func applySnapshot) { - return new Persistence(key, invalidate, null, null, snapshotStore, null, applySnapshot, null); + return CreatePersistence(key, applySnapshot, null); } public IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent) { + return CreatePersistence(key, applySnapshot, applyEvent); + } + + private IPersistence CreatePersistence(string key, Func applySnapshot, Func, Task> applyEvent) + { + Guard.NotNullOrEmpty(key, nameof(key)); + + var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applySnapshot, applyEvent); } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index c095e4868..14a93859f 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); - private readonly AppDomainObject app; + private readonly AppDomainObject app = new AppDomainObject(); private readonly Language language = Language.DE; private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientName = "client"; @@ -34,8 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Apps public AppCommandMiddlewareTests() { - app = new AppDomainObject(); - A.CallTo(() => appProvider.GetAppAsync(AppName)) .Returns((IAppEntity)null); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index acac9d548..4dc8b389d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -24,12 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string clientId = "client"; private readonly string clientNewName = "My Client"; private readonly string planId = "premium"; - private readonly AppDomainObject sut; - - public AppDomainObjectTests() - { - sut = new AppDomainObject(); - } + private readonly AppDomainObject sut = new AppDomainObject(); [Fact] public void Create_should_throw_exception_if_created() diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs new file mode 100644 index 000000000..fbd1bbd7a --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -0,0 +1,137 @@ +// ========================================================================== +// AssetCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetCommandMiddlewareTests : HandlerTestBase + { + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetStore assetStore = A.Fake(); + private readonly Guid assetId = Guid.NewGuid(); + private readonly Stream stream = new MemoryStream(); + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly AssetDomainObject asset = new AssetDomainObject(); + private readonly AssetFile file; + private readonly AssetCommandMiddleware sut; + + public AssetCommandMiddlewareTests() + { + file = new AssetFile("my-image.png", "image/png", 1024, () => stream); + + sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file }); + + SetupStore(0, context.ContextId); + SetupImageInfo(); + + await TestCreate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(assetId, context.Result>().IdOrValue); + + AssertAssetHasBeenUploaded(0, context.ContextId); + AssertAssetImageChecked(); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file }); + + SetupStore(1, context.ContextId); + SetupImageInfo(); + + CreateAsset(); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + AssertAssetHasBeenUploaded(1, context.ContextId); + AssertAssetImageChecked(); + } + + [Fact] + public async Task Rename_should_update_domain_object() + { + CreateAsset(); + + var context = CreateContextForCommand(new RenameAsset { AssetId = assetId, FileName = "my-new-image.png" }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateAsset(); + + var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void CreateAsset() + { + asset.Create(CreateCommand(new CreateAsset { File = file })); + } + + private void SetupImageInfo() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(image); + } + + private void SetupStore(long version, Guid commitId) + { + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)) + .Returns(TaskHelper.Done); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)) + .Returns(TaskHelper.Done); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) + .Returns(TaskHelper.Done); + } + + private void AssertAssetImageChecked() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)).MustHaveHappened(); + } + + private void AssertAssetHasBeenUploaded(long version, Guid commitId) + { + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)).MustHaveHappened(); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).MustHaveHappened(); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())).MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs new file mode 100644 index 000000000..081ffecd4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs @@ -0,0 +1,209 @@ +// ========================================================================== +// AssetDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetDomainObjectTests : HandlerTestBase + { + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly Guid assetId = Guid.NewGuid(); + private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); + private readonly AssetDomainObject sut = new AssetDomainObject(); + + [Fact] + public void Create_should_throw_exception_if_created() + { + CreateAsset(); + + Assert.Throws(() => + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); + }); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image })); + + Assert.Equal(0, sut.State.FileVersion); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetCreated + { + IsImage = true, + FileName = file.FileName, + FileSize = file.FileSize, + FileVersion = 0, + MimeType = file.MimeType, + PixelWidth = image.PixelWidth, + PixelHeight = image.PixelHeight + }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateAsset(); + + sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image })); + + Assert.Equal(1, sut.State.FileVersion); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated + { + IsImage = true, + FileSize = file.FileSize, + FileVersion = 1, + MimeType = file.MimeType, + PixelWidth = image.PixelWidth, + PixelHeight = image.PixelHeight + }) + ); + } + + [Fact] + public void Rename_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" })); + }); + } + + [Fact] + public void Rename_should_throw_exception_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Rename_should_create_events() + { + CreateAsset(); + + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" })); + + Assert.Equal("my-new-image.png", sut.State.FileName); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_create_events_with_total_file_size() + { + CreateAsset(); + UpdateAsset(); + + sut.Delete(CreateAssetCommand(new DeleteAsset())); + + Assert.True(sut.State.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) + ); + } + + private void CreateAsset() + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); + sut.ClearUncommittedEvents(); + } + + private void UpdateAsset() + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + sut.ClearUncommittedEvents(); + } + + private void DeleteAsset() + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + sut.ClearUncommittedEvents(); + } + + protected T CreateAssetEvent(T @event) where T : AssetEvent + { + @event.AssetId = assetId; + + return CreateEvent(@event); + } + + protected T CreateAssetCommand(T command) where T : AssetAggregateCommand + { + command.AssetId = assetId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs new file mode 100644 index 000000000..92d676354 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// GuardAssetTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public class GuardAssetTests + { + [Fact] + public void CanRename_should_throw_exception_if_name_not_defined() + { + var command = new RenameAsset(); + + Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); + } + + [Fact] + public void CanRename_should_throw_exception_if_name_are_the_same() + { + var command = new RenameAsset { FileName = "asset-name" }; + + Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); + } + + [Fact] + public void CanRename_not_should_throw_exception_if_name_are_different() + { + var command = new RenameAsset { FileName = "new-name" }; + + GuardAsset.CanRename(command, "asset-name"); + } + + [Fact] + public void CanCreate_should_not_throw_exception() + { + var command = new CreateAsset(); + + GuardAsset.CanCreate(command); + } + + [Fact] + public void CanUpdate_should_not_throw_exception() + { + var command = new UpdateAsset(); + + GuardAsset.CanUpdate(command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteAsset(); + + GuardAsset.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs index aa9b3abe9..3b04844ff 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -23,19 +23,17 @@ namespace Squidex.Domain.Apps.Entities.Rules public class RuleCommandMiddlewareTests : HandlerTestBase { private readonly IAppProvider appProvider = A.Fake(); - private readonly RuleCommandMiddleware sut; - private readonly RuleDomainObject rule; + private readonly RuleDomainObject rule = new RuleDomainObject(); private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; private readonly Guid ruleId = Guid.NewGuid(); + private readonly RuleCommandMiddleware sut; public RuleCommandMiddlewareTests() { A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) .Returns(A.Fake()); - rule = new RuleDomainObject(); - sut = new RuleCommandMiddleware(Handler, appProvider); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index 10dd3948b..4f4376e56 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -21,16 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Rules { public class RuleDomainObjectTests : HandlerTestBase { + private readonly Guid ruleId = Guid.NewGuid(); private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; - private readonly RuleDomainObject sut; - - public Guid RuleId { get; } = Guid.NewGuid(); - - public RuleDomainObjectTests() - { - sut = new RuleDomainObject(); - } + private readonly RuleDomainObject sut = new RuleDomainObject(); [Fact] public void Create_should_throw_exception_if_created() @@ -234,14 +228,14 @@ namespace Squidex.Domain.Apps.Entities.Rules protected T CreateRuleEvent(T @event) where T : RuleEvent { - @event.RuleId = RuleId; + @event.RuleId = ruleId; return CreateEvent(@event); } protected T CreateRuleCommand(T command) where T : RuleAggregateCommand { - command.RuleId = RuleId; + command.RuleId = ruleId; return CreateCommand(command); } From 3334d8c71abadb382ea063f5dc8c494c7a01e118 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 6 Dec 2017 20:56:43 +0100 Subject: [PATCH 06/30] Created synced object --- Squidex.sln | 15 ++ .../Apps/MongoAppEntity.cs | 34 ++++ .../Apps/MongoAppRepository.cs | 85 ++++++++++ .../Assets/MongoAssetEntity.cs | 30 ++++ .../Assets/MongoAssetRepository.cs | 140 +++++++++++++++ .../Assets/MongoAssetStatsEntity.cs | 6 +- .../Assets/MongoAssetStatsRepository.cs | 19 +-- ...MongoAssetStatsRepository_EventHandling.cs | 8 +- .../Contents/Extensions.cs | 2 +- .../Contents/MongoContentEntity.cs | 11 +- .../Contents/MongoContentRepository.cs | 29 +++- .../Contents/Visitors/ConstantVisitor.cs | 2 +- .../Contents/Visitors/FilterBuilder.cs | 2 +- .../Contents/Visitors/FilterVisitor.cs | 2 +- .../Contents/Visitors/FindExtensions.cs | 2 +- .../Contents/Visitors/PropertyVisitor.cs | 2 +- .../Contents/Visitors/SearchTermVisitor.cs | 2 +- .../Contents/Visitors/SortBuilder.cs | 2 +- .../History/MongoHistoryEventEntity.cs | 3 +- .../History/MongoHistoryEventRepository.cs | 27 ++- .../History/ParsedHistoryEvent.cs | 4 +- .../MongoCollectionExtensions.cs | 2 +- .../Rules/MongoRuleEntity.cs | 30 ++++ .../Rules/MongoRuleEventEntity.cs | 6 +- .../Rules/MongoRuleEventRepository.cs | 30 ++-- .../Rules/MongoRuleRepository.cs | 85 ++++++++++ ...uidex.Domain.Apps.Entities.MongoDb.csproj} | 2 +- .../AppProvider.cs | 66 ++++++++ .../Apps/Repositories/IAppRepository.cs | 18 ++ .../Contents/State/ContentState.cs | 22 +++ .../Rules/Repositories/IRuleRepository.cs | 19 +++ .../Schemas/Repositories/ISchemaRepository.cs | 21 +++ .../Assets/MongoAssetEntity.cs | 75 -------- .../Assets/MongoAssetRepository.cs | 100 ----------- .../MongoAssetRepository_EventHandling.cs | 64 ------- .../MongoContentRepository_EventHandling.cs | 160 ------------------ .../States/MongoSnapshotStore.cs | 6 +- .../Commands/AggregateHandler.cs | 47 ++++- .../Commands/CommandExtensions.cs | 20 +++ .../Commands/IAggregateHandler.cs | 4 + .../Grains/EventConsumerGrainManager.cs | 2 +- .../States/IStateFactory.cs | 4 +- .../States/StateFactory.cs | 4 +- src/Squidex.Infrastructure/Tasks/AsyncLock.cs | 74 ++++++++ .../Tasks/AsyncLockPool.cs | 37 ++++ 45 files changed, 836 insertions(+), 489 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Assets/MongoAssetStatsEntity.cs (87%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Assets/MongoAssetStatsRepository.cs (76%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Assets/MongoAssetStatsRepository_EventHandling.cs (90%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Extensions.cs (97%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/MongoContentEntity.cs (87%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/MongoContentRepository.cs (88%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/ConstantVisitor.cs (96%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/FilterBuilder.cs (96%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/FilterVisitor.cs (98%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/FindExtensions.cs (97%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/PropertyVisitor.cs (97%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/SearchTermVisitor.cs (94%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Contents/Visitors/SortBuilder.cs (96%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/History/MongoHistoryEventEntity.cs (95%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/History/MongoHistoryEventRepository.cs (77%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/History/ParsedHistoryEvent.cs (95%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/MongoCollectionExtensions.cs (98%) create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Rules/MongoRuleEventEntity.cs (90%) rename src/{Squidex.Domain.Apps.Read.MongoDb => Squidex.Domain.Apps.Entities.MongoDb}/Rules/MongoRuleEventRepository.cs (70%) create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs rename src/{Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj => Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj} (92%) create mode 100644 src/Squidex.Domain.Apps.Entities/AppProvider.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs create mode 100644 src/Squidex.Infrastructure/Tasks/AsyncLock.cs create mode 100644 src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs diff --git a/Squidex.sln b/Squidex.sln index fb24873ad..4d49d682e 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -69,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entitie EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.Tests", "tests\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj", "{AA003372-CD8D-4DBC-962C-F61E0C93CF05}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.MongoDb", "src\Squidex.Domain.Apps.Entities.MongoDb\Squidex.Domain.Apps.Entities.MongoDb.csproj", "{7DA5B308-D950-4496-93D5-21D6C4D91644}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -343,6 +345,18 @@ Global {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x64.Build.0 = Release|Any CPU {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.ActiveCfg = Release|Any CPU {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.Build.0 = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x64.Build.0 = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x86.Build.0 = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|Any CPU.Build.0 = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x64.ActiveCfg = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x64.Build.0 = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x86.ActiveCfg = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -374,6 +388,7 @@ Global {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892} {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892} {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892} + {7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs new file mode 100644 index 000000000..64f694162 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// MongoAppEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Apps.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +{ + public sealed class MongoAppEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public AppState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + + [BsonElement] + [BsonRequired] + public string[] UserIds { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs new file mode 100644 index 000000000..675ccddda --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// MongoAppRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Apps.Repositories; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +{ + public sealed class MongoAppRepository : MongoRepositoryBase, IAppRepository, ISnapshotStore + { + public MongoAppRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); + } + + public async Task<(AppState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task> QueryUserAppNamesAsync(string userId) + { + var appEntities = + await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)).ToListAsync(); + + return appEntities.Select(x => x.Id).ToList(); + } + + public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key) + .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs new file mode 100644 index 000000000..fb6af615b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// MongoAssetEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Assets.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed class MongoAssetEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public AssetState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs new file mode 100644 index 000000000..32b27a99f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// MongoAssetRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, ISnapshotStore + { + public MongoAssetRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.State.AppId) + .Ascending(x => x.State.FileName) + .Ascending(x => x.State.MimeType) + .Descending(x => x.State.LastModified)); + } + + public async Task<(AssetState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) + { + var filter = CreateFilter(appId, mimeTypes, ids, query); + + var assetEntities = + await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.State.LastModified) + .ToListAsync(); + + return assetEntities.OfType().ToList(); + } + + public async Task CountAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null) + { + var filter = CreateFilter(appId, mimeTypes, ids, query); + + var assetsCount = + await Collection.Find(filter) + .CountAsync(); + + return assetsCount; + } + + public async Task FindAssetAsync(Guid id) + { + var (state, etag) = await ReadAsync(id.ToString()); + + return state; + } + + private static FilterDefinition CreateFilter(Guid appId, ICollection mimeTypes, ICollection ids, string query) + { + var filters = new List> + { + Filter.Eq(x => x.State.AppId, appId) + }; + + if (ids != null && ids.Count > 0) + { + filters.Add(Filter.In(x => x.Id, ids.Select(x => x.ToString()))); + } + + if (mimeTypes != null && mimeTypes.Count > 0) + { + filters.Add(Filter.In(x => x.State.MimeType, mimeTypes)); + } + + if (!string.IsNullOrWhiteSpace(query)) + { + filters.Add(Filter.Regex(x => x.State.FileName, new BsonRegularExpression(query, "i"))); + } + + var filter = Filter.And(filters); + + return filter; + } + + public async Task WriteAsync(string key, AssetState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key) + .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs similarity index 87% rename from src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs index 9fe49a6b0..cc7ddf110 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs @@ -9,9 +9,9 @@ using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Read.Assets; +using Squidex.Domain.Apps.Entities.Assets; -namespace Squidex.Domain.Apps.Read.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public sealed class MongoAssetStatsEntity : IAssetStatsEntity { @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets [BsonRequired] [BsonElement] - public Guid AppId { get; set; } + public Guid AssetId { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs similarity index 76% rename from src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs index 290c754e7..a8fc50d26 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs @@ -11,12 +11,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IAssetEventConsumer { @@ -30,17 +30,16 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets return "Projections_AssetStats"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.Date)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Date))); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AssetId).Ascending(x => x.Date)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AssetId).Descending(x => x.Date)); } public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) { var originalSizesEntities = - await Collection.Find(x => x.AppId == appId && x.Date >= fromDate && x.Date <= toDate).SortBy(x => x.Date) + await Collection.Find(x => x.AssetId == appId && x.Date >= fromDate && x.Date <= toDate).SortBy(x => x.Date) .ToListAsync(); var enrichedSizes = new List(); @@ -64,7 +63,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets if (previousSize < 0) { var firstBeforeRangeEntity = - await Collection.Find(x => x.AppId == appId && x.Date < fromDate).SortByDescending(x => x.Date) + await Collection.Find(x => x.AssetId == appId && x.Date < fromDate).SortByDescending(x => x.Date) .FirstOrDefaultAsync(); previousSize = firstBeforeRangeEntity?.TotalSize ?? 0L; @@ -88,7 +87,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets public async Task GetTotalSizeAsync(Guid appId) { var totalSizeEntity = - await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Date) + await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date) .FirstOrDefaultAsync(); return totalSizeEntity?.TotalSize ?? 0; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs similarity index 90% rename from src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs index b46374dd7..16e153aa3 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs @@ -13,12 +13,10 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Read.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public partial class MongoAssetStatsRepository { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - public string Name { get { return GetType().Name; } @@ -60,14 +58,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets if (assetStatsEntity == null) { var lastEntity = - await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Date) + await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date) .FirstOrDefaultAsync(); assetStatsEntity = new MongoAssetStatsEntity { Id = id, Date = date, - AppId = appId, + AssetId = appId, TotalSize = lastEntity?.TotalSize ?? 0, TotalCount = lastEntity?.TotalCount ?? 0 }; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs similarity index 97% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs index be7dcb916..cb68d1c52 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs @@ -16,7 +16,7 @@ using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { public static class Extensions { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs similarity index 87% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index f2f19251e..bb8ac758d 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -13,18 +13,13 @@ using MongoDB.Bson.Serialization.Attributes; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Contents; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public sealed class MongoContentEntity : - IContentEntity, - IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithAppRef + public sealed class MongoContentEntity : IContentEntity { private NamedContentData data; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs similarity index 88% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 9f1b3b9fb..29be5d217 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -14,17 +14,16 @@ using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public partial class MongoContentRepository : IContentRepository, IEventConsumer + public class MongoContentRepository : IContentRepository { private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; @@ -189,5 +188,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents await action(collection, schema); } + + private Task ForAppIdAsync(Guid appId, Func, Task> action) + { + var collection = GetCollection(appId); + + return action(collection); + } + + private IMongoCollection GetCollection(Guid appId) + { + var name = $"{Prefix}{appId}"; + + return database.GetCollection(name); + } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs similarity index 96% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs index 9a21a44c4..58e116b23 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs @@ -12,7 +12,7 @@ using Microsoft.OData.UriParser; using NodaTime; using NodaTime.Text; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public sealed class ConstantVisitor : QueryNodeVisitor { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs similarity index 96% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterBuilder.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs index 203ca26c7..895e5d79b 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterBuilder.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs @@ -12,7 +12,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public static class FilterBuilder { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs index 250b4d513..f4d69dea9 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs @@ -13,7 +13,7 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public class FilterVisitor : QueryNodeVisitor> { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs similarity index 97% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 1abdb16a0..547107029 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -14,7 +14,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public static class FindExtensions { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs similarity index 97% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs index 36ddad63c..0052167fc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs @@ -14,7 +14,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public sealed class PropertyVisitor : QueryNodeVisitor> { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SearchTermVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs similarity index 94% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SearchTermVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs index a99f5af0e..3e106af60 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SearchTermVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs @@ -9,7 +9,7 @@ using System; using Microsoft.OData.UriParser; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public class SearchTermVisitor : QueryNodeVisitor { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SortBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs similarity index 96% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SortBuilder.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs index 6b851c528..627095331 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SortBuilder.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs @@ -11,7 +11,7 @@ using Microsoft.OData.UriParser; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public static class SortBuilder { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs similarity index 95% rename from src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs index 4990fd528..a3223f276 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs @@ -12,11 +12,12 @@ using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History { public sealed class MongoHistoryEventEntity : MongoEntity, IEntity, IEntityWithAppRef, + IUpdateableEntity, IUpdateableEntityWithVersion, IUpdateableEntityWithCreatedBy, IUpdateableEntityWithAppRef diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs similarity index 77% rename from src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index 3404a64a7..169ed0df3 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -11,13 +11,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Read.History; -using Squidex.Domain.Apps.Read.History.Repositories; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History { public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, IEventConsumer { @@ -53,23 +53,22 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History return "Projections_History"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - return Task.WhenAll( - collection.Indexes.CreateOneAsync( - Index - .Ascending(x => x.AppId) - .Ascending(x => x.Channel) - .Descending(x => x.Created) - .Descending(x => x.Version)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) })); + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.Channel) + .Descending(x => x.Created) + .Descending(x => x.Version)); + + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) }); } public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) { var historyEventEntities = - await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) - .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) + await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) .ToListAsync(); return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs similarity index 95% rename from src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs index e3520bd43..c3aea8c1b 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs @@ -9,12 +9,12 @@ using System; using System.Collections.Generic; using NodaTime; -using Squidex.Domain.Apps.Read.History; +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.Read.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History { internal sealed class ParsedHistoryEvent : IHistoryEventEntity { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs index 3e840b8b1..2893111cc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb +namespace Squidex.Domain.Apps.Entities.MongoDb { public static class MongoCollectionExtensions { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs new file mode 100644 index 000000000..304e5d01a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// MongoRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Rules.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public RuleState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs similarity index 90% rename from src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs index e48aa1543..f57382d6e 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs @@ -11,16 +11,16 @@ using MongoDB.Bson.Serialization.Attributes; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Entities.Rules; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity { [BsonRequired] [BsonElement] - public Guid AppId { get; set; } + public Guid AssetId { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs similarity index 70% rename from src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs index 99b22d125..73c5655f2 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -14,12 +14,12 @@ using MongoDB.Driver; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Read.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository { @@ -33,12 +33,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules return "RuleEvents"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Created)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AssetId).Descending(x => x.Created)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }); } public Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)) @@ -49,7 +48,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) { var ruleEventEntities = - await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) + await Collection.Find(x => x.AssetId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) .ToListAsync(); return ruleEventEntities; @@ -66,7 +65,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public async Task CountByAppAsync(Guid appId) { - return (int)await Collection.CountAsync(x => x.AppId == appId); + return (int)await Collection.CountAsync(x => x.AssetId == appId); } public Task EnqueueAsync(Guid id, Instant nextAttempt) @@ -84,11 +83,12 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) { return Collection.UpdateOneAsync(x => x.Id == jobId, - Update.Set(x => x.Result, result) - .Set(x => x.LastDump, dump) - .Set(x => x.JobResult, jobResult) - .Set(x => x.NextAttempt, nextAttempt) - .Inc(x => x.NumCalls, 1)); + Update + .Set(x => x.Result, result) + .Set(x => x.LastDump, dump) + .Set(x => x.JobResult, jobResult) + .Set(x => x.NextAttempt, nextAttempt) + .Inc(x => x.NumCalls, 1)); } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs new file mode 100644 index 000000000..c65f2820a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// MongoRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +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.Domain.Apps.Entities.Rules.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, ISnapshotStore + { + public MongoRuleRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + } + + public async Task<(RuleState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task> QueryRuleIdsAsync(Guid appId) + { + var ruleEntities = + await Collection.Find(x => x.State.AppId == appId).Project(Projection.Include(x => x.Id)).ToListAsync(); + + return ruleEntities.Select(x => x.Id).ToList(); + } + + public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key) + .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj similarity index 92% rename from src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj rename to src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index b8e390b69..2fc0d6ace 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs new file mode 100644 index 000000000..d862fe9a8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// AppProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class AppProvider : IAppProvider + { + private readonly IStateFactory factory; + + public AppProvider(IStateFactory factory) + { + Guard.NotNull(factory, nameof(factory)); + + this.factory = factory; + } + + public Task GetAppAsync(string appName) + { + return null; + } + + public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) + { + return null; + } + + public Task> GetRulesAsync(string appName) + { + return null; + } + + public Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) + { + return null; + } + + public Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) + { + return null; + } + + public Task> GetSchemasAsync(string appName) + { + return null; + } + + public Task> GetUserApps(string userId) + { + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs new file mode 100644 index 000000000..2e567dc45 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// IAppRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Apps.Repositories +{ + public interface IAppRepository + { + Task> QueryUserAppNamesAsync(string userId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs new file mode 100644 index 000000000..bee8e3061 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// ContentState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.State +{ + public sealed class ContentState : DomainObjectState + { + [JsonProperty] + public IdContentData Data { get; set; } + + [JsonProperty] + public string Status { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs new file mode 100644 index 000000000..3a3f309ab --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// IRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules.Repositories +{ + public interface IRuleRepository + { + Task> QueryRuleIdsAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs new file mode 100644 index 000000000..04df68b68 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// ISchemaRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Schemas.Repositories +{ + public interface ISchemaRepository + { + Task FindSchemaNameAsync(Guid schemaId); + + Task> QuerySchemaNamesAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs deleted file mode 100644 index dae9ddcfc..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// MongoAssetEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Assets -{ - public sealed class MongoAssetEntity : - MongoEntity, - IAssetEntity, - IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithAppRef - { - [BsonRequired] - [BsonElement] - public string MimeType { get; set; } - - [BsonRequired] - [BsonElement] - public string FileName { get; set; } - - [BsonRequired] - [BsonElement] - public long FileSize { get; set; } - - [BsonRequired] - [BsonElement] - public long FileVersion { get; set; } - - [BsonRequired] - [BsonElement] - public bool IsImage { get; set; } - - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public int? PixelWidth { get; set; } - - [BsonRequired] - [BsonElement] - public int? PixelHeight { get; set; } - - [BsonRequired] - [BsonElement] - public Guid AppId { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken LastModifiedBy { get; set; } - - Guid IAssetInfo.AssetId - { - get { return Id; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs deleted file mode 100644 index 73bb48ae8..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// MongoAssetRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Assets -{ - public partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, IAssetEventConsumer - { - public MongoAssetRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_Assets"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return collection.Indexes.CreateOneAsync( - Index.Ascending(x => x.AppId) - .Ascending(x => x.FileName) - .Ascending(x => x.MimeType) - .Descending(x => x.LastModified)); - } - - public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) - { - var filter = CreateFilter(appId, mimeTypes, ids, query); - - var assetEntities = - await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.LastModified) - .ToListAsync(); - - return assetEntities.OfType().ToList(); - } - - public async Task CountAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null) - { - var filter = CreateFilter(appId, mimeTypes, ids, query); - - var assetsCount = - await Collection.Find(filter) - .CountAsync(); - - return assetsCount; - } - - public async Task FindAssetAsync(Guid id) - { - var assetEntity = - await Collection.Find(s => s.Id == id) - .FirstOrDefaultAsync(); - - return assetEntity; - } - - private static FilterDefinition CreateFilter(Guid appId, ICollection mimeTypes, ICollection ids, string query) - { - var filters = new List> - { - Filter.Eq(x => x.AppId, appId) - }; - - if (ids != null && ids.Count > 0) - { - filters.Add(Filter.In(x => x.Id, ids)); - } - - if (mimeTypes != null && mimeTypes.Count > 0) - { - filters.Add(Filter.In(x => x.MimeType, mimeTypes)); - } - - if (!string.IsNullOrWhiteSpace(query)) - { - filters.Add(Filter.Regex(x => x.FileName, new BsonRegularExpression(query, "i"))); - } - - var filter = Filter.And(filters); - - return filter; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs deleted file mode 100644 index b05fe5d41..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// MongoAssetRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Assets -{ - public partial class MongoAssetRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^asset-"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected Task On(AssetCreated @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AssetUpdated @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AssetRenamed @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AssetDeleted @event, EnvelopeHeaders headers) - { - return Collection.DeleteOneAsync(x => x.Id == @event.AssetId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs deleted file mode 100644 index 85acec317..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ /dev/null @@ -1,160 +0,0 @@ -// ========================================================================== -// MongoContentRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Contents -{ - public partial class MongoContentRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^(content-)|(app-)|(asset-)"; } - } - - public async Task ClearAsync() - { - using (var collections = await database.ListCollectionsAsync()) - { - while (await collections.MoveNextAsync()) - { - foreach (var collection in collections.Current) - { - var name = collection["name"].ToString(); - - if (name.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) - { - await database.DropCollectionAsync(name); - } - } - } - } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected Task On(AppCreated @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, async collection => - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status)); - await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); - }); - } - - protected Task On(ContentCreated @event, EnvelopeHeaders headers) - { - return ForSchemaAsync(@event.AppId, @event.SchemaId.Id, (collection, schema) => - { - return collection.CreateAsync(@event, headers, content => - { - content.SchemaId = @event.SchemaId.Id; - - SimpleMapper.Map(@event, content); - - var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); - - content.DataText = idData?.ToFullText(); - content.IdData = idData; - content.ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef); - }); - }); - } - - protected Task On(ContentUpdated @event, EnvelopeHeaders headers) - { - return ForSchemaAsync(@event.AppId, @event.SchemaId.Id, (collection, schema) => - { - var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); - - return collection.UpdateOneAsync( - Filter.Eq(x => x.Id, @event.ContentId), - Update - .Set(x => x.DataText, idData.ToFullText()) - .Set(x => x.IdData, idData) - .Set(x => x.ReferencedIds, idData.ToReferencedIds(schema.SchemaDef)) - .Set(x => x.LastModified, headers.Timestamp()) - .Set(x => x.LastModifiedBy, @event.Actor) - .Set(x => x.Version, headers.EventStreamNumber())); - }); - } - - protected Task On(ContentStatusChanged @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, collection => - { - return collection.UpdateOneAsync( - Filter.Eq(x => x.Id, @event.ContentId), - Update - .Set(x => x.Status, @event.Status) - .Set(x => x.LastModified, headers.Timestamp()) - .Set(x => x.LastModifiedBy, @event.Actor) - .Set(x => x.Version, headers.EventStreamNumber())); - }); - } - - protected Task On(AssetDeleted @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, collection => - { - return collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, @event.AssetId), - Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)), - Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId)); - }); - } - - protected Task On(ContentDeleted @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, async collection => - { - await collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), - Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), - Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); - - await collection.DeleteOneAsync(x => x.Id == @event.ContentId); - }); - } - - private Task ForAppIdAsync(Guid appId, Func, Task> action) - { - var collection = GetCollection(appId); - - return action(collection); - } - - private IMongoCollection GetCollection(Guid appId) - { - var name = $"{Prefix}{appId}"; - - return database.GetCollection(name); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 0eaa6c56e..f29c431a0 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -48,11 +48,7 @@ namespace Squidex.Infrastructure.States { try { - await Collection.UpdateOneAsync( - Filter.And( - Filter.Eq(x => x.Id, key), - Filter.Eq(x => x.Version, oldVersion) - ), + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.Doc, value) .Set(x => x.Version, newVersion), diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index 10f17b85b..ba32106a4 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -10,11 +10,13 @@ using System; using System.Threading.Tasks; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Commands { public sealed class AggregateHandler : IAggregateHandler { + private readonly AsyncLockPool lockPool = new AsyncLockPool(10000); private readonly IStateFactory stateFactory; private readonly ISemanticLog log; private readonly IServiceProvider serviceProvider; @@ -45,13 +47,27 @@ namespace Squidex.Infrastructure.Commands return InvokeAsync(context, updater, true); } + public Task CreateSyncedAsync(CommandContext context, Func creator) where T : class, IDomainObject + { + Guard.NotNull(creator, nameof(creator)); + + return InvokeSyncedAsync(context, creator, false); + } + + public Task UpdateSyncedAsync(CommandContext context, Func updater) where T : class, IDomainObject + { + Guard.NotNull(updater, nameof(updater)); + + return InvokeSyncedAsync(context, updater, true); + } + private async Task InvokeAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject { Guard.NotNull(context, nameof(context)); var domainObjectCommand = GetCommand(context); var domainObjectId = domainObjectCommand.AggregateId; - var domainObject = await stateFactory.GetDetachedAsync(domainObjectId.ToString()); + var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); await domainObject.WriteAsync(log); @@ -70,6 +86,35 @@ namespace Squidex.Infrastructure.Commands return domainObject; } + private async Task InvokeSyncedAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject + { + Guard.NotNull(context, nameof(context)); + + var domainObjectCommand = GetCommand(context); + var domainObjectId = domainObjectCommand.AggregateId; + + using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) + { + var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + + await domainObject.WriteAsync(log); + + if (!context.IsCompleted) + { + if (isUpdate) + { + context.Complete(new EntitySavedResult(domainObject.Version)); + } + else + { + context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version)); + } + } + + return domainObject; + } + } + private static IAggregateCommand GetCommand(CommandContext context) { if (!(context.Command is IAggregateCommand command)) diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 5eb115cd6..2c5d721e5 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -34,6 +34,26 @@ namespace Squidex.Infrastructure.Commands }); } + public static Task CreateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject + { + return handler.CreateSyncedAsync(context, x => + { + creator(x); + + return TaskHelper.Done; + }); + } + + public static Task UpdateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject + { + return handler.UpdateSyncedAsync(context, x => + { + updater(x); + + return TaskHelper.Done; + }); + } + public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) { return commandMiddleware.HandleAsync(context, () => TaskHelper.Done); diff --git a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs index c3a01f38d..d018ec4dd 100644 --- a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs @@ -15,6 +15,10 @@ namespace Squidex.Infrastructure.Commands { Task CreateAsync(CommandContext context, Func creator) where T : class, IDomainObject; + Task CreateSyncedAsync(CommandContext context, Func creator) where T : class, IDomainObject; + Task UpdateAsync(CommandContext context, Func updater) where T : class, IDomainObject; + + Task UpdateSyncedAsync(CommandContext context, Func updater) where T : class, IDomainObject; } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs index 80e0686a7..70ef98c40 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs @@ -39,7 +39,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains foreach (var consumer in consumers) { - var actor = factory.GetDetachedAsync(consumer.Name).Result; + var actor = factory.CreateAsync(consumer.Name).Result; actors[consumer.Name] = actor; actor.Activate(consumer); diff --git a/src/Squidex.Infrastructure/States/IStateFactory.cs b/src/Squidex.Infrastructure/States/IStateFactory.cs index ff5ab7018..b76180150 100644 --- a/src/Squidex.Infrastructure/States/IStateFactory.cs +++ b/src/Squidex.Infrastructure/States/IStateFactory.cs @@ -12,8 +12,8 @@ namespace Squidex.Infrastructure.States { public interface IStateFactory { - Task GetSynchronizedAsync(string key) where T : IStatefulObject; + Task GetSingleAsync(string key) where T : IStatefulObject; - Task GetDetachedAsync(string key) where T : IStatefulObject; + Task CreateAsync(string key) where T : IStatefulObject; } } diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index 4394b8f06..a2a9ce661 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -81,7 +81,7 @@ namespace Squidex.Infrastructure.States }); } - public async Task GetDetachedAsync(string key) where T : IStatefulObject + public async Task CreateAsync(string key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); @@ -93,7 +93,7 @@ namespace Squidex.Infrastructure.States return state; } - public Task GetSynchronizedAsync(string key) where T : IStatefulObject + public Task GetSingleAsync(string key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs new file mode 100644 index 000000000..ad5468ef7 --- /dev/null +++ b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// AsyncLock.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLock + { + private readonly SemaphoreSlim semaphore; + + public AsyncLock() + { + semaphore = new SemaphoreSlim(1); + } + + public Task LockAsync() + { + Task wait = semaphore.WaitAsync(); + + if (wait.IsCompleted) + { + return Task.FromResult((IDisposable)new LockReleaser(this)); + } + else + { + return wait.ContinueWith(x => (IDisposable)new LockReleaser(this), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + private class LockReleaser : IDisposable + { + private AsyncLock target; + + internal LockReleaser(AsyncLock target) + { + this.target = target; + } + + public void Dispose() + { + AsyncLock current = target; + + if (current == null) + { + return; + } + + target = null; + + try + { + current.semaphore.Release(); + } + catch + { + // just ignore the Exception + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs b/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs new file mode 100644 index 000000000..6701782b2 --- /dev/null +++ b/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// AsyncLockPool.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLockPool + { + private readonly AsyncLock[] locks; + + public AsyncLockPool(int poolSize) + { + Guard.GreaterThan(poolSize, 0, nameof(poolSize)); + + locks = new AsyncLock[poolSize]; + + for (var i = 0; i < poolSize; i++) + { + locks[i] = new AsyncLock(); + } + } + + public Task LockAsync(object target) + { + Guard.NotNull(target, nameof(target)); + + return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync(); + } + } +} From 322980d9c9c39f3836d6b4817e013831be5c6c2e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 6 Dec 2017 22:08:07 +0100 Subject: [PATCH 07/30] State updates fixed. --- .../MongoCollectionExtensions.cs | 2 +- .../Apps/AppDomainObject.cs | 63 +------- .../Apps/AppHistoryEventsCreator.cs | 18 +-- .../Apps/State/AppState.cs | 74 ++++++++- .../Assets/AssetDomainObject.cs | 24 +-- .../Assets/State/AssetState.cs | 38 ++++- .../EntityMapper.cs | 55 ++++--- .../Rules/RuleDequeuer.cs | 2 +- .../Rules/RuleDomainObject.cs | 22 +-- .../Rules/State/RuleState.cs | 36 ++++- .../Schemas/SchemaDomainObject.cs | 54 +------ .../Schemas/State/SchemaState.cs | 143 +++++++++++++++++- .../MongoXmlRepository.cs | 2 - .../Commands/DomainObjectBase.cs | 14 +- .../Triggers/ContentChangedTriggerTests.cs | 1 - .../TestHelpers/HandlerTestBase.cs | 10 ++ 16 files changed, 354 insertions(+), 204 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs index 2893111cc..b358f8511 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb { public static Task CreateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IEntity, new() { - var entity = EntityMapper.Create(@event, headers, updater); + var entity = new T().Update(@event, headers, updater); return collection.InsertOneIfNotExistsAsync(entity); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index 40593cc2c..d39102795 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Linq; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.State; @@ -28,10 +27,6 @@ namespace Squidex.Domain.Apps.Entities.Apps var appId = new NamedId(command.AppId, command.Name); - UpdateState(command, s => { s.Id = appId.Id; s.Name = command.Name; }); - - UpdateContributors(command, c => c.Assign(command.Actor.Identifier, AppContributorPermission.Owner)); - RaiseEvent(SimpleMapper.Map(command, CreateInitalEvent(appId))); RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId))); @@ -43,27 +38,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateLanguages(command, l => - { - var fallback = command.Fallback; - - if (fallback != null && fallback.Count > 0) - { - var existingLangauges = l.OfType().Select(x => x.Language); - - fallback = fallback.Intersect(existingLangauges).ToList(); - } - - l = l.Set(new LanguageConfig(command.Language, command.IsOptional, fallback)); - - if (command.IsMaster) - { - l = l.MakeMaster(command.Language); - } - - return l; - }); - RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); return this; @@ -75,15 +49,11 @@ namespace Squidex.Domain.Apps.Entities.Apps if (!string.IsNullOrWhiteSpace(command.Name)) { - UpdateClients(command, c => c.Rename(command.Id, command.Name)); - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); } if (command.Permission.HasValue) { - UpdateClients(command, c => c.Update(command.Id, command.Permission.Value)); - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value })); } @@ -94,8 +64,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateContributors(command, c => c.Assign(command.ContributorId, command.Permission)); - RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); return this; @@ -105,8 +73,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateContributors(command, c => c.Remove(command.ContributorId)); - RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); return this; @@ -116,8 +82,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateClients(command, c => c.Add(command.Id, command.Secret)); - RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); return this; @@ -127,8 +91,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateClients(command, c => c.Revoke(command.Id)); - RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); return this; @@ -138,8 +100,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateLanguages(command, l => l.Set(new LanguageConfig(command.Language))); - RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); return this; @@ -149,8 +109,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateLanguages(command, l => l.Remove(command.Language)); - RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); return this; @@ -160,8 +118,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { ThrowIfNotCreated(); - UpdateState(command, s => s.Plan = command.PlanId != null ? new AppPlan(command.Actor, command.PlanId) : null); - RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); return this; @@ -208,24 +164,9 @@ namespace Squidex.Domain.Apps.Entities.Apps } } - private void UpdateClients(ICommand command, Func updater) - { - UpdateState(command, s => s.Clients = updater(s.Clients)); - } - - private void UpdateContributors(ICommand command, Func updater) - { - UpdateState(command, s => s.Contributors = updater(s.Contributors)); - } - - private void UpdateLanguages(ICommand command, Func updater) - { - UpdateState(command, s => s.LanguagesConfig = updater(s.LanguagesConfig)); - } - - protected override AppState CloneState(ICommand command, Action updater) + protected override void OnRaised(Envelope @event) { - return State.Clone().Update((SquidexCommand)command, updater); + UpdateState(State.Apply(@event)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index 2d2cdd628..635387de5 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Apps "changed master language to {[Language]}"); } - protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) + protected Task On(AppContributorRemoved @event) { const string channel = "settings.contributors"; @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Contributor", @event.ContributorId)); } - protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers) + protected Task On(AppContributorAssigned @event) { const string channel = "settings.contributors"; @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Contributor", @event.ContributorId).AddParameter("Permission", @event.Permission)); } - protected Task On(AppClientAttached @event, EnvelopeHeaders headers) + protected Task On(AppClientAttached @event) { const string channel = "settings.clients"; @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Id", @event.Id)); } - protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) + protected Task On(AppClientRevoked @event) { const string channel = "settings.clients"; @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Id", @event.Id)); } - protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) + protected Task On(AppClientRenamed @event) { const string channel = "settings.clients"; @@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Id", @event.Id).AddParameter("Name", ClientName(@event))); } - protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) + protected Task On(AppLanguageAdded @event) { const string channel = "settings.languages"; @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) + protected Task On(AppLanguageRemoved @event) { const string channel = "settings.languages"; @@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppLanguageUpdated @event, EnvelopeHeaders headers) + protected Task On(AppLanguageUpdated @event) { const string channel = "settings.languages"; @@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers) + protected Task On(AppMasterLanguageSet @event) { const string channel = "settings.languages"; diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 857f66678..34988ff0f 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -8,11 +8,16 @@ using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps.State { - public sealed class AppState : DomainObjectState, IAppEntity + public class AppState : DomainObjectState, IAppEntity { private static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN); @@ -30,5 +35,72 @@ namespace Squidex.Domain.Apps.Entities.Apps.State [JsonProperty] public LanguagesConfig LanguagesConfig { get; set; } = English; + + protected void On(AppCreated @event) + { + SimpleMapper.Map(@event, this); + } + + protected void On(AppPlanChanged @event) + { + Plan = @event.PlanId == null ? null : new AppPlan(@event.Actor, @event.PlanId); + } + + protected void On(AppContributorAssigned @event) + { + Contributors = Contributors.Assign(@event.ContributorId, @event.Permission); + } + + protected void On(AppContributorRemoved @event) + { + Contributors = Contributors.Remove(@event.ContributorId); + } + + protected void On(AppClientAttached @event) + { + Clients = Clients.Add(@event.Id, @event.Secret); + } + + protected void On(AppClientUpdated @event) + { + Clients = Clients.Update(@event.Id, @event.Permission); + } + + protected void On(AppClientRenamed @event) + { + Clients = Clients.Rename(@event.Id, @event.Name); + } + + protected void On(AppClientRevoked @event) + { + Clients = Clients.Revoke(@event.Id); + } + + protected void On(AppLanguageAdded @event) + { + LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language)); + } + + protected void On(AppLanguageRemoved @event) + { + LanguagesConfig = LanguagesConfig.Remove(@event.Language); + } + + protected void On(AppLanguageUpdated @event) + { + LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, @event.Fallback)); + + if (@event.IsMaster) + { + LanguagesConfig = LanguagesConfig.MakeMaster(@event.Language); + } + } + + public AppState Apply(Envelope @event) + { + var payload = (SquidexEvent)@event.Payload; + + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index d66b14a94..52677b5c4 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -6,12 +6,12 @@ // All rights reserved. // ========================================================================== -using System; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets @@ -33,13 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Assets IsImage = command.ImageInfo != null }); - UpdateState(command, s => - { - s.TotalSize = @event.FileSize; - - SimpleMapper.Map(@event, s); - }); - RaiseEvent(@event); return this; @@ -59,13 +52,6 @@ namespace Squidex.Domain.Apps.Entities.Assets IsImage = command.ImageInfo != null }); - UpdateState(command, s => - { - s.TotalSize += @event.FileSize; - - SimpleMapper.Map(@event, s); - }); - RaiseEvent(@event); return this; @@ -75,8 +61,6 @@ namespace Squidex.Domain.Apps.Entities.Assets { VerifyCreatedAndNotDeleted(); - UpdateState(command, s => s.IsDeleted = true); - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = State.TotalSize })); return this; @@ -86,8 +70,6 @@ namespace Squidex.Domain.Apps.Entities.Assets { VerifyCreatedAndNotDeleted(); - UpdateState(command, s => s.FileName = command.FileName); - RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); return this; @@ -109,9 +91,9 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - protected override AssetState CloneState(ICommand command, Action updater) + protected override void OnRaised(Envelope @event) { - return State.Clone().Update((SquidexCommand)command, updater); + UpdateState(State.Apply(@event)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index 77e6ca5c9..b8ce1c92e 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -9,10 +9,15 @@ using System; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets.State { - public sealed class AssetState : DomainObjectState, + public class AssetState : DomainObjectState, IAssetEntity, IAssetInfo, IUpdateableEntityWithAppRef @@ -51,5 +56,36 @@ namespace Squidex.Domain.Apps.Entities.Assets.State { get { return Id; } } + + protected void On(AssetCreated @event) + { + SimpleMapper.Map(@event, this); + + TotalSize += @event.FileSize; + } + + protected void On(AssetUpdated @event) + { + SimpleMapper.Map(@event, this); + + TotalSize += @event.FileSize; + } + + protected void On(AssetRenamed @event) + { + FileName = @event.FileName; + } + + protected void On(AssetDeleted @event) + { + IsDeleted = true; + } + + public AssetState Apply(Envelope @event) + { + var payload = (SquidexEvent)@event.Payload; + + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index b7fbf2671..3e6243a34 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -1,5 +1,5 @@ // ========================================================================== -// EntityMapper.cs +// EntityMapper2.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,82 +8,81 @@ using System; using NodaTime; -using Squidex.Infrastructure.Commands; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities { public static class EntityMapper { - public static T Update(this T entity, SquidexCommand command, Action updater = null) where T : IEntity + public static T Update(this T entity, SquidexEvent @event, EnvelopeHeaders headers, Action updater = null) where T : IEntity { - var timestamp = SystemClock.Instance.GetCurrentInstant(); - - SetId(entity, command); - SetAppId(entity, command); - SetCreated(entity, timestamp); - SetCreatedBy(entity, command); - SetLastModified(entity, timestamp); - SetLastModifiedBy(entity, command); - SetVersion(entity); + SetId(entity, headers); + SetAppId(entity, @event); + SetCreated(entity, headers); + SetCreatedBy(entity, @event); + SetLastModified(entity, headers); + SetLastModifiedBy(entity, @event); + SetVersion(entity, headers); updater?.Invoke(entity); return entity; } - private static void SetId(IEntity entity, SquidexCommand command) + private static void SetId(IEntity entity, EnvelopeHeaders headers) { - if (entity is IUpdateableEntity updateable && command is IAggregateCommand aggregateCommand) + if (entity is IUpdateableEntity updateable) { - updateable.Id = aggregateCommand.AggregateId; + updateable.Id = headers.AggregateId(); } } - private static void SetVersion(IEntity entity) + private static void SetVersion(IEntity entity, EnvelopeHeaders headers) { if (entity is IUpdateableEntityWithVersion withVersion) { - withVersion.Version++; + withVersion.Version = headers.EventStreamNumber(); } } - private static void SetCreated(IEntity entity, Instant timestamp) + private static void SetCreated(IEntity entity, EnvelopeHeaders headers) { if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) { - updateable.Created = timestamp; + updateable.Created = headers.Timestamp(); } } - private static void SetCreatedBy(IEntity entity, SquidexCommand command) + private static void SetCreatedBy(IEntity entity, SquidexEvent @event) { if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) { - withCreatedBy.CreatedBy = command.Actor; + withCreatedBy.CreatedBy = @event.Actor; } } - private static void SetLastModified(IEntity entity, Instant timestamp) + private static void SetLastModified(IEntity entity, EnvelopeHeaders headers) { if (entity is IUpdateableEntity updateable) { - updateable.LastModified = timestamp; + updateable.LastModified = headers.Timestamp(); } } - private static void SetLastModifiedBy(IEntity entity, SquidexCommand command) + private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event) { if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) { - withModifiedBy.LastModifiedBy = command.Actor; + withModifiedBy.LastModifiedBy = @event.Actor; } } - private static void SetAppId(IEntity entity, SquidexCommand command) + private static void SetAppId(IEntity entity, SquidexEvent @event) { - if (entity is IUpdateableEntityWithAppRef appEntity && command is AppCommand appCommand) + if (entity is IUpdateableEntityWithAppRef appEntity && @event is AppEvent appEvent) { - appEntity.AppId = appCommand.AppId.Id; + appEntity.AppId = appEvent.AppId.Id; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs index a3e07ea47..5ae2477bf 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Rules { - public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem + public class RuleDequeuer : DisposableObjectBase, IExternalSystem { private readonly ActionBlock requestBlock; private readonly IRuleEventRepository ruleEventRepository; diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index 80522b6d5..6aa105077 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -6,13 +6,12 @@ // All rights reserved. // ========================================================================== -using System; -using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.State; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Rules @@ -23,8 +22,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { VerifyNotCreated(); - UpdateRule(command, r => new Rule(command.Trigger, command.Action)); - RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); } @@ -32,8 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { VerifyCreatedAndNotDeleted(); - UpdateRule(command, r => r.Update(command.Trigger).Update(command.Action)); - RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); } @@ -41,8 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { VerifyCreatedAndNotDeleted(); - UpdateRule(command, r => r.Enable()); - RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); } @@ -50,8 +43,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { VerifyCreatedAndNotDeleted(); - UpdateRule(command, r => r.Disable()); - RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); } @@ -59,8 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { VerifyCreatedAndNotDeleted(); - UpdateState(command, s => s.IsDeleted = true); - RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); } @@ -80,14 +69,9 @@ namespace Squidex.Domain.Apps.Entities.Rules } } - private void UpdateRule(ICommand command, Func updater) - { - UpdateState(command, s => s.RuleDef = updater(s.RuleDef)); - } - - protected override RuleState CloneState(ICommand command, Action updater) + protected override void OnRaised(Envelope @event) { - return State.Clone().Update((SquidexCommand)command, updater); + UpdateState(State.Apply(@event)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index c41d1761d..f08e8ddae 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -9,10 +9,17 @@ using System; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Rules.State { - public sealed class RuleState : DomainObjectState, IRuleEntity + public class RuleState : DomainObjectState, + IRuleEntity, + IEntityWithAppRef, + IUpdateableEntityWithAppRef { [JsonProperty] public Guid AppId { get; set; } @@ -22,5 +29,32 @@ namespace Squidex.Domain.Apps.Entities.Rules.State [JsonProperty] public bool IsDeleted { get; set; } + + protected void On(RuleCreated @event) + { + RuleDef = new Rule(@event.Trigger, @event.Action); + } + + protected void On(RuleUpdated @event) + { + RuleDef = RuleDef.Update(@event.Trigger).Update(@event.Action); + } + + protected void On(RuleEnabled @event) + { + RuleDef = RuleDef.Enable(); + } + + protected void On(RuleDisabled @event) + { + RuleDef = RuleDef.Disable(); + } + + public RuleState Apply(Envelope @event) + { + var payload = (SquidexEvent)@event.Payload; + + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index 6d3eb5277..c3ec2642b 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -8,13 +8,13 @@ using System; using System.Collections.Generic; -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.State; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas @@ -57,22 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - var partitioning = - string.Equals(command.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? - Partitioning.Language : - Partitioning.Invariant; - - var fieldId = State.TotalFields; - - var field = registry.CreateField(fieldId, command.Name, partitioning, command.Properties); - - UpdateState(command, state => - { - state.SchemaDef = state.SchemaDef.AddField(field); - state.TotalFields = fieldId + 1; - }); - - RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(fieldId + 1, command.Name) })); + RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(State.TotalFields + 1, command.Name) })); return this; } @@ -81,8 +66,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.UpdateField(command.FieldId, command.Properties)); - RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated())); return this; @@ -92,8 +75,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.LockField(command.FieldId)); - RaiseEvent(command, new FieldLocked()); return this; @@ -103,8 +84,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.HideField(command.FieldId)); - RaiseEvent(command, new FieldHidden()); return this; @@ -114,8 +93,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.ShowField(command.FieldId)); - RaiseEvent(command, new FieldShown()); return this; @@ -125,8 +102,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.DisableField(command.FieldId)); - RaiseEvent(command, new FieldDisabled()); return this; @@ -136,8 +111,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.EnableField(command.FieldId)); - RaiseEvent(command, new FieldEnabled()); return this; @@ -147,8 +120,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.DeleteField(command.FieldId)); - RaiseEvent(command, new FieldDeleted()); return this; @@ -158,8 +129,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.ReorderFields(command.FieldIds)); - RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); return this; @@ -169,8 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.Publish()); - RaiseEvent(SimpleMapper.Map(command, new SchemaPublished())); return this; @@ -180,8 +147,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateSchema(command, s => s.Unpublish()); - RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished())); return this; @@ -191,8 +156,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateState(command, s => SimpleMapper.Map(command, s)); - RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured())); return this; @@ -202,8 +165,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateState(command, s => s.IsDeleted = true); - RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted())); return this; @@ -213,8 +174,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyCreatedAndNotDeleted(); - UpdateState(command, s => SimpleMapper.Map(command, s)); - RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated())); return this; @@ -248,14 +207,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas } } - private void UpdateSchema(ICommand command, Func updater) - { - UpdateState(command, s => s.SchemaDef = updater(s.SchemaDef)); - } - - protected override SchemaState CloneState(ICommand command, Action updater) + protected override void OnRaised(Envelope @event) { - return State.Clone().Update((SquidexCommand)command, updater); + UpdateState(State.Apply(@event)); } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 9fd80f57b..585e890e4 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -1,5 +1,5 @@ // ========================================================================== -// JsonSchemaEntity.cs +// SchemaState.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,11 +8,17 @@ using System; using Newtonsoft.Json; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas.State { - public sealed class SchemaState : DomainObjectState, + public class SchemaState : DomainObjectState, ISchemaEntity, IUpdateableEntityWithAppRef, IUpdateableEntityWithCreatedBy, @@ -25,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State public Guid AppId { get; set; } [JsonProperty] - public int TotalFields { get; set; } + public int TotalFields { get; set; } = 1; [JsonProperty] public bool IsDeleted { get; set; } @@ -53,5 +59,136 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State { get { return SchemaDef.IsPublished; } } + + protected void On(SchemaCreated @event, FieldRegistry registry) + { + var schema = new Schema(@event.Name); + + if (@event.Properties != null) + { + schema = schema.Update(@event.Properties); + } + + if (@event.Fields != null) + { + foreach (var eventField in @event.Fields) + { + var partitioning = + string.Equals(eventField.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? + Partitioning.Language : + Partitioning.Invariant; + + var field = registry.CreateField(TotalFields, eventField.Name, partitioning, eventField.Properties); + + if (eventField.IsHidden) + { + field = field.Hide(); + } + + if (eventField.IsDisabled) + { + field = field.Disable(); + } + + if (eventField.IsLocked) + { + field = field.Lock(); + } + + schema = schema.AddField(field); + + TotalFields++; + } + } + + SchemaDef = schema; + } + + protected void On(FieldAdded @event, FieldRegistry registry) + { + var partitioning = + string.Equals(@event.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? + Partitioning.Language : + Partitioning.Invariant; + + var field = registry.CreateField(@event.FieldId.Id, @event.Name, partitioning, @event.Properties); + + SchemaDef = SchemaDef.DeleteField(@event.FieldId.Id); + SchemaDef = SchemaDef.AddField(field); + + TotalFields++; + } + + protected void On(SchemaPublished @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.Publish(); + } + + protected void On(SchemaUnpublished @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.Unpublish(); + } + + protected void On(SchemaUpdated @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.Update(@event.Properties); + } + + protected void On(SchemaFieldsReordered @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.ReorderFields(@event.FieldIds); + } + + protected void On(FieldUpdated @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.UpdateField(@event.FieldId.Id, @event.Properties); + } + + protected void On(FieldLocked @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.LockField(@event.FieldId.Id); + } + + protected void On(FieldDisabled @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.DisableField(@event.FieldId.Id); + } + + protected void On(FieldEnabled @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.EnableField(@event.FieldId.Id); + } + + protected void On(FieldHidden @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.HideField(@event.FieldId.Id); + } + + protected void On(FieldShown @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.ShowField(@event.FieldId.Id); + } + + protected void On(FieldDeleted @event, FieldRegistry registry) + { + SchemaDef = SchemaDef.DeleteField(@event.FieldId.Id); + } + + protected void On(SchemaDeleted @event, FieldRegistry registry) + { + IsDeleted = true; + } + + protected void On(ScriptsConfigured @event, FieldRegistry registry) + { + SimpleMapper.Map(@event, this); + } + + public SchemaState Apply(Envelope @event) + { + var payload = (SquidexEvent)@event.Payload; + + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + } } } diff --git a/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs b/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs index ad7b7c953..c730af9d3 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs @@ -18,8 +18,6 @@ namespace Squidex.Domain.Users.MongoDb { public sealed class MongoXmlRepository : MongoRepositoryBase, IXmlRepository { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - public MongoXmlRepository(IMongoDatabase database) : base(database) { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 40c1a0c03..62ba1fdcc 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -49,24 +49,28 @@ namespace Squidex.Infrastructure.Commands return persistence.ReadAsync(); } - protected void RaiseEvent(IEvent @event) + public void RaiseEvent(IEvent @event) { RaiseEvent(Envelope.Create(@event)); } - protected void RaiseEvent(Envelope @event) where TEvent : class, IEvent + public void RaiseEvent(Envelope @event) where TEvent : class, IEvent { Guard.NotNull(@event, nameof(@event)); + OnRaised(@event.To()); + uncomittedEvents.Add(@event.To()); } - public void UpdateState(ICommand command, Action updater) + public void UpdateState(TState newState) { - state = CloneState(command, updater); + state = newState; } - protected abstract TState CloneState(ICommand command, Action updater); + protected virtual void OnRaised(Envelope @event) + { + } public async Task WriteAsync(ISemanticLog log) { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs index cb495d264..f86e99df0 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -11,7 +11,6 @@ using System.Collections.Immutable; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Schemas; using Xunit; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 763b0db25..c97d25f1c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -33,6 +33,16 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers IsUpdated = false; } + public Task CreateSyncedAsync(CommandContext context, Func creator) where V : class, IDomainObject + { + return CreateAsync(context, creator); + } + + public Task UpdateSyncedAsync(CommandContext context, Func creator) where V : class, IDomainObject + { + return UpdateAsync(context, creator); + } + public async Task CreateAsync(CommandContext context, Func creator) where V : class, IDomainObject { IsCreated = true; From a7cd21473ad82259001d83261cf5bff1e2fbd96e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Dec 2017 21:39:29 +0100 Subject: [PATCH 08/30] Squidex project updated --- .../Apps/MongoAppRepository.cs | 36 ++- .../Assets/MongoAssetRepository.cs | 9 +- .../Assets/MongoAssetStatsRepository.cs | 3 +- .../Contents/MongoContentEntity.cs | 21 +- .../Contents/MongoContentRepository.cs | 172 ++++++----- .../MongoContentRepository_EventHandling.cs | 58 ++++ .../Contents/Visitors/FindExtensions.cs | 1 + .../Rules/MongoRuleRepository.cs | 21 +- .../Schemas/MongoSchemaEntity.cs | 30 ++ .../Schemas/MongoSchemaRepository.cs | 101 +++++++ .../AppProvider.cs | 141 +++++++-- .../Apps/Repositories/IAppRepository.cs | 5 +- .../Contents/Commands/ChangeContentStatus.cs | 17 ++ .../Contents/Commands/ContentCommand.cs | 26 ++ .../Contents/Commands/ContentDataCommand.cs | 17 ++ .../Contents/Commands/CreateContent.cs | 15 + .../Commands/DeleteContent.cs} | 8 +- .../Contents/Commands/PatchContent.cs | 14 + .../Contents/Commands/UpdateContent.cs | 14 + .../Contents/ContentCommandMiddleware.cs | 158 ++++++++++ .../Contents/ContentDataChangedResult.cs | 24 ++ .../Contents/ContentDomainObject.cs | 101 +++++++ .../Contents/ContentOperationContext.cs | 139 +++++++++ .../Contents/ContentQueryService.cs | 11 +- .../Contents/GraphQL/CachingGraphQLService.cs | 2 +- .../Contents/Guards/GuardContent.cs | 74 +++++ .../Contents/IContentQueryService.cs | 2 +- .../Repositories/IContentRepository.cs | 2 + .../Contents/State/ContentState.cs | 49 +++- .../DomainObjectState.cs | 4 + .../IAppProvider.cs | 10 +- .../Rules/Guards/GuardRule.cs | 4 +- .../Rules/Guards/RuleTriggerValidator.cs | 4 +- .../Rules/Repositories/IRuleRepository.cs | 2 +- .../Rules/RuleEnqueuer.cs | 2 +- .../Schemas/Guards/GuardSchema.cs | 2 +- .../Schemas/Repositories/ISchemaRepository.cs | 4 +- .../MongoDb/MongoExtensions.cs | 23 ++ .../States/MongoSnapshotStore.cs | 4 +- .../UsageTracking/MongoUsageStore.cs | 2 - .../Commands/DomainObjectBase.cs | 9 +- .../Commands/IDomainObject.cs | 2 +- .../States/IPersistence.cs | 2 + .../States/Persistence.cs | 13 +- .../Areas/Api/Controllers/ApiController.cs | 7 +- .../Controllers/Apps/AppClientsController.cs | 2 +- .../Apps/AppContributorsController.cs | 4 +- .../Apps/AppLanguagesController.cs | 2 +- .../Api/Controllers/Apps/AppsController.cs | 6 +- .../Assets/AssetContentController.cs | 2 +- .../Controllers/Assets/AssetsController.cs | 8 +- .../Assets/Models/AssetCreatedDto.cs | 2 +- .../Assets/Models/AssetReplacedDto.cs | 4 +- .../Content/ContentSwaggerController.cs | 4 +- .../Controllers/Content/ContentsController.cs | 21 +- .../Generator/SchemasSwaggerGenerator.cs | 4 +- .../Controllers/Content/Models/ContentDto.cs | 2 +- .../Controllers/History/HistoryController.cs | 2 +- .../Controllers/Plans/AppPlansController.cs | 6 +- .../Rules/Models/Converters/RuleConverter.cs | 4 +- .../Controllers/Rules/Models/RuleEventDto.cs | 2 +- .../Api/Controllers/Rules/RulesController.cs | 8 +- .../Models/Converters/SchemaConverter.cs | 4 +- .../Schemas/SchemaFieldsController.cs | 2 +- .../Controllers/Schemas/SchemasController.cs | 12 +- .../Statistics/UsagesController.cs | 4 +- .../IdentityServer/Config/LazyClientStore.cs | 2 +- .../Middlewares/PortalRedirectMiddleware.cs | 2 +- src/Squidex/Config/Domain/ReadServices.cs | 36 +-- src/Squidex/Config/Domain/StoreServices.cs | 66 +++-- src/Squidex/Config/Domain/WriteServices.cs | 31 +- src/Squidex/Config/MyUsageOptions.cs | 2 +- src/Squidex/Pipeline/ApiCostsFilter.cs | 2 +- src/Squidex/Pipeline/AppApiFilter.cs | 4 +- .../Pipeline/AppPermissionAttribute.cs | 2 +- .../EnrichWithActorCommandMiddleware.cs | 2 +- .../EnrichWithAppIdCommandMiddleware.cs | 2 +- .../EnrichWithSchemaIdCommandMiddleware.cs | 10 +- src/Squidex/Pipeline/GraphQLUrlGenerator.cs | 10 +- src/Squidex/Pipeline/IAppFeature.cs | 2 +- src/Squidex/Squidex.csproj | 7 +- .../Contents/ContentCommandMiddlewareTests.cs | 244 ++++++++++++++++ .../Contents/ContentDomainObjectTests.cs | 269 ++++++++++++++++++ .../Contents/ContentEventTests.cs | 70 +++++ .../Contents/ContentQueryServiceTests.cs | 14 +- .../Contents/GraphQLTests.cs | 10 +- .../Contents/Guard/GuardContentTests.cs | 99 +++++++ .../Contents/ODataQueryTests.cs | 4 +- .../Rules/Guards/GuardRuleTests.cs | 2 +- .../Triggers/ContentChangedTriggerTests.cs | 14 +- .../Rules/RuleCommandMiddlewareTests.cs | 2 +- .../Rules/RuleEnqueuerTests.cs | 2 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 2 + .../Commands/AggregateHandlerTests.cs | 28 +- .../Commands/DomainObjectBaseTests.cs | 61 +--- .../Commands/TestHelpers/MyDomainObject.cs | 11 +- .../Grains/EventConsumerManagerTests.cs | 4 +- .../States/StateEventSourcingTests.cs | 42 +-- .../States/StateSnapshotTests.cs | 48 ++-- 99 files changed, 2121 insertions(+), 455 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs rename src/Squidex.Domain.Apps.Entities/{Assets/IAssetEventConsumer.cs => Contents/Commands/DeleteContent.cs} (65%) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 675ccddda..ee186a620 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -29,6 +30,33 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); } + public async Task FindAppIdByNameAsync(string name) + { + var appEntity = + await Collection.Find(x => x.State.Name == name).Only(x => x.Id) + .FirstOrDefaultAsync(); + + return appEntity != null ? Guid.Parse(appEntity.Id) : Guid.Empty; + } + + public async Task> QueryUserAppIdsAsync(string userId) + { + var appEntities = + await Collection.Find(x => x.UserIds.Contains(userId)).Only(x => x.Id) + .ToListAsync(); + + return appEntities.Select(x => Guid.Parse(x.Id)).ToList(); + } + + public async Task> QueryUserAppNamesAsync(string userId) + { + var appEntities = + await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)) + .ToListAsync(); + + return appEntities.Select(x => x.Id).ToList(); + } + public async Task<(AppState Value, long Version)> ReadAsync(string key) { var existing = @@ -43,14 +71,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return (null, -1); } - public async Task> QueryUserAppNamesAsync(string userId) - { - var appEntities = - await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)).ToListAsync(); - - return appEntities.Select(x => x.Id).ToList(); - } - public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) { try diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 32b27a99f..445ebe747 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -27,6 +27,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { } + protected override string CollectionName() + { + return "Snapshots_Assets"; + } + protected override Task SetupCollectionAsync(IMongoCollection collection) { return collection.Indexes.CreateOneAsync( @@ -122,8 +127,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key) - .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); if (existingVersion != null) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs index a8fc50d26..ff01ab832 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs @@ -14,11 +14,12 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IAssetEventConsumer + public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IEventConsumer { public MongoAssetStatsRepository(IMongoDatabase database) : base(database) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index bb8ac758d..c1b815624 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -25,13 +25,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonId] [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } + public string DocumentId { get; set; } [BsonRequired] - [BsonElement("st")] - [BsonRepresentation(BsonType.String)] - public Status Status { get; set; } + [BsonElement] + public Guid Id { get; set; } [BsonRequired] [BsonElement("ct")] @@ -73,10 +71,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonElement("rd")] public List ReferencedIdsDeleted { get; set; } = new List(); + [BsonRequired] + [BsonElement("lt")] + public bool IsLatest { get; set; } + + [BsonRequired] + [BsonElement("st")] + [BsonRepresentation(BsonType.String)] + public Status Status { get; set; } + [BsonRequired] [BsonElement("do")] [BsonJson] - public IdContentData IdData { get; set; } + public IdContentData DataByIds { get; set; } NamedContentData IContentEntity.Data { @@ -85,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public void ParseData(Schema schema) { - data = IdData.ToData(schema, ReferencedIdsDeleted); + data = DataByIds.ToData(schema, ReferencedIdsDeleted); } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 29be5d217..8b927faf5 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -11,74 +11,129 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.OData.UriParser; -using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public class MongoContentRepository : IContentRepository + public partial class MongoContentRepository : MongoRepositoryBase, + IEventConsumer, + IContentRepository, + ISnapshotStore { - private const string Prefix = "Projections_Content_"; - private readonly IMongoDatabase database; private readonly IAppProvider appProvider; - protected static FilterDefinitionBuilder Filter + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) + : base(database) { - get - { - return Builders.Filter; - } + Guard.NotNull(appProvider, nameof(appProvider)); + + this.appProvider = appProvider; } - protected static UpdateDefinitionBuilder Update + protected override string CollectionName() { - get - { - return Builders.Update; - } + return "Snapshots_Assets"; } - protected static ProjectionDefinitionBuilder Projection + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - get - { - return Builders.Projection; - } + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.Id) + .Descending(x => x.Version)); + + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.SchemaId) + .Descending(x => x.IsLatest) + .Descending(x => x.LastModified)); + + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status)); + await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); } - protected static IndexKeysDefinitionBuilder Index + public async Task WriteAsync(string key, ContentState value, long oldVersion, long newVersion) { - get + var documentId = $"{key}_{oldVersion}"; + + var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId); + + if (schema == null) { - return Builders.IndexKeys; + throw new InvalidOperationException($"Cannot find schema {value.SchemaId}"); + } + + var idData = value.Data?.ToIdModel(schema.SchemaDef, true); + + var document = SimpleMapper.Map(value, new MongoContentEntity + { + DocumentId = documentId, + DataText = idData?.ToFullText(), + DataByIds = idData, + ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef), + }); + + try + { + await Collection.InsertOneAsync(document); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == value.Id && x.IsLatest).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } } } - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) + public async Task<(ContentState Value, long Version)> ReadAsync(string key) { - Guard.NotNull(database, nameof(database)); - Guard.NotNull(appProvider, nameof(appProvider)); + var id = Guid.Parse(key); - this.database = database; - this.appProvider = appProvider; + var existing = + await Collection.Find(x => x.Id == id && x.IsLatest) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (SimpleMapper.Map(existing, new ContentState()), existing.Version); + } + + return (null, -1); } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) { - var collection = GetCollection(app.Id); - IFindFluent cursor; try { cursor = - collection + Collection .Find(odataQuery, schema.Id, schema.SchemaDef, status) .Take(odataQuery) .Skip(odataQuery) @@ -103,14 +158,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return contentEntities; } - public Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) + public async Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) { - var collection = GetCollection(app.Id); - IFindFluent cursor; try { - cursor = collection.Find(odataQuery, schema.Id, schema.SchemaDef, status); + cursor = Collection.Find(odataQuery, schema.Id, schema.SchemaDef, status); } catch (NotSupportedException) { @@ -121,15 +174,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents throw new ValidationException("This odata operation is not supported."); } - return cursor.CountAsync(); + return await cursor.CountAsync(); } public async Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var collection = GetCollection(app.Id); - var contentsCount = - await collection.Find(x => ids.Contains(x.Id)) + await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest) .CountAsync(); return contentsCount; @@ -137,10 +188,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var collection = GetCollection(app.Id); - var contentEntities = - await collection.Find(x => ids.Contains(x.Id)) + await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest) .ToListAsync(); foreach (var entity in contentEntities) @@ -153,21 +202,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds) { - var collection = GetCollection(appId); - var contentEntities = - await collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Project(Projection.Include(x => x.Id)) + await Collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Only(x => x.Id) .ToListAsync(); - return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); + return contentIds.Except(contentEntities.Select(x => x.Id)).ToList(); } - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) { - var collection = GetCollection(app.Id); - var contentEntity = - await collection.Find(x => x.Id == id) + await Collection.Find(x => x.Id == id && x.Version == version) .FirstOrDefaultAsync(); contentEntity?.ParseData(schema.SchemaDef); @@ -175,32 +220,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return contentEntity; } - private async Task ForSchemaAsync(NamedId appId, Guid schemaId, Func, ISchemaEntity, Task> action) - { - var collection = GetCollection(appId.Id); - - var schema = await appProvider.GetSchemaAsync(appId.Name, schemaId, true); - - if (schema == null) - { - return; - } - - await action(collection, schema); - } - - private Task ForAppIdAsync(Guid appId, Func, Task> action) + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) { - var collection = GetCollection(appId); - - return action(collection); - } + var contentEntity = + await Collection.Find(x => x.Id == id && x.IsLatest) + .FirstOrDefaultAsync(); - private IMongoCollection GetCollection(Guid appId) - { - var name = $"{Prefix}{appId}"; + contentEntity?.ParseData(schema.SchemaDef); - return database.GetCollection(name); + return contentEntity; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs new file mode 100644 index 000000000..74e8b36f7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// MongoContentRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public partial class MongoContentRepository + { + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^(content-)|(asset-)"; } + } + + public override Task ClearAsync() + { + return TaskHelper.Done; + } + + public Task On(Envelope @event) + { + return this.DispatchActionAsync(@event.Payload, @event.Headers); + } + + protected Task On(AssetDeleted @event) + { + return Collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, @event.AssetId), + Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)), + Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId)); + } + + protected Task On(ContentDeleted @event) + { + return Collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), + Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), + Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 547107029..0c09e1398 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -69,6 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors var filters = new List> { Filter.Eq(x => x.SchemaId, schemaId), + Filter.Eq(x => x.IsLatest, true), Filter.In(x => x.Status, status) }; diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index c65f2820a..895bc54db 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -25,9 +25,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override string CollectionName() { - return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + return "States_Rules"; + } + + protected override async Task SetupCollectionAsync(IMongoCollection collection) + { + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.IsDeleted)); } public async Task<(RuleState Value, long Version)> ReadAsync(string key) @@ -44,12 +50,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return (null, -1); } - public async Task> QueryRuleIdsAsync(Guid appId) + public async Task> QueryRuleIdsAsync(Guid appId) { var ruleEntities = - await Collection.Find(x => x.State.AppId == appId).Project(Projection.Include(x => x.Id)).ToListAsync(); + await Collection.Find(x => x.State.AppId == appId && !x.State.IsDeleted).Only(x => x.Id) + .ToListAsync(); - return ruleEntities.Select(x => x.Id).ToList(); + return ruleEntities.Select(x => Guid.Parse(x.Id)).ToList(); } public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) @@ -67,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key) - .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); if (existingVersion != null) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs new file mode 100644 index 000000000..541575cef --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// MongoSchemaEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Schemas.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +{ + public sealed class MongoSchemaEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public SchemaState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs new file mode 100644 index 000000000..b1c14d9e9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// MongoSchemaRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +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.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +{ + public sealed class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISnapshotStore + { + public MongoSchemaRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Snapshots_Schemas"; + } + + protected override async Task SetupCollectionAsync(IMongoCollection collection) + { + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.Name)); + } + + public async Task<(SchemaState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task FindSchemaIdAsync(Guid appId, string name) + { + var schemaEntity = + await Collection.Find(x => x.State.Name == name).Only(x => x.Id) + .FirstOrDefaultAsync(); + + return schemaEntity != null ? Guid.Parse(schemaEntity.Id) : Guid.Empty; + } + + public async Task> QuerySchemaIdsAsync(Guid appId) + { + var schemaEntities = + await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id) + .ToListAsync(); + + return schemaEntities.Select(x => Guid.Parse(x.Id)).ToList(); + } + + public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key).Only(x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index d862fe9a8..6d494f377 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -7,11 +7,16 @@ // ========================================================================== using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; 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.States; @@ -19,48 +24,148 @@ namespace Squidex.Domain.Apps.Entities { public sealed class AppProvider : IAppProvider { - private readonly IStateFactory factory; + private readonly ConcurrentDictionary appIds = new ConcurrentDictionary(); + private readonly ConcurrentDictionary, Guid> schemaIds = new ConcurrentDictionary, Guid>(); + private readonly IAppRepository appRepository; + private readonly IRuleRepository ruleRepository; + private readonly ISchemaRepository schemaRepository; + private readonly IStateFactory stateFactory; - public AppProvider(IStateFactory factory) + public AppProvider( + IAppRepository appRepository, + ISchemaRepository schemaRepository, + IStateFactory stateFactory, + IRuleRepository ruleRepository) { - Guard.NotNull(factory, nameof(factory)); + Guard.NotNull(appRepository, nameof(appRepository)); + Guard.NotNull(schemaRepository, nameof(schemaRepository)); + Guard.NotNull(stateFactory, nameof(stateFactory)); + Guard.NotNull(ruleRepository, nameof(ruleRepository)); - this.factory = factory; + this.appRepository = appRepository; + this.schemaRepository = schemaRepository; + this.stateFactory = stateFactory; + this.ruleRepository = ruleRepository; } - public Task GetAppAsync(string appName) + public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - return null; + var app = await stateFactory.GetSingleAsync(appId.ToString()); + + if (app.Version < 0) + { + throw new DomainObjectNotFoundException(appId.ToString(), typeof(SchemaDomainObject)); + } + + var schema = await stateFactory.GetSingleAsync(id.ToString()); + + if (schema.Version < 0 || schema.State.IsDeleted) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject)); + } + + return (app.State, schema.State); + } + + public async Task GetAppAsync(string appName) + { + var appId = await GetAppIdAsync(appName); + + var app = await stateFactory.GetSingleAsync(appName); + + if (app.Version < 0) + { + throw new DomainObjectNotFoundException(appName, typeof(SchemaDomainObject)); + } + + return app.State; + } + + public async Task GetSchemaAsync(Guid appId, string name, bool provideDeleted = false) + { + var schemaId = await GetSchemaIdAsync(appId, name); + + var schema = await stateFactory.GetSingleAsync(schemaId.ToString()); + + if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted)) + { + throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(SchemaDomainObject)); + } + + return schema.State; } - public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) + public async Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false) { - return null; + var schema = await stateFactory.GetSingleAsync(id.ToString()); + + if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted)) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject)); + } + + return schema.State; } - public Task> GetRulesAsync(string appName) + public async Task> GetSchemasAsync(Guid appId) { - return null; + var ids = await schemaRepository.QuerySchemaIdsAsync(appId); + + var schemas = + await Task.WhenAll( + ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + + return schemas.Select(a => (ISchemaEntity)a.State).ToList(); } - public Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) + public async Task> GetRulesAsync(Guid appId) { - return null; + var ids = await ruleRepository.QueryRuleIdsAsync(appId); + + var rules = + await Task.WhenAll( + ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + + return rules.Select(a => (IRuleEntity)a.State).ToList(); } - public Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) + public async Task> GetUserApps(string userId) { - return null; + var ids = await appRepository.QueryUserAppIdsAsync(userId); + + var apps = + await Task.WhenAll( + ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + + return apps.Select(a => (IAppEntity)a.State).ToList(); } - public Task> GetSchemasAsync(string appName) + private async Task GetAppIdAsync(string name) { - return null; + var key = name; + + if (!appIds.TryGetValue(key, out var id)) + { + id = await appRepository.FindAppIdByNameAsync(name); + + appIds[key] = id; + } + + return id; } - public Task> GetUserApps(string userId) + private async Task GetSchemaIdAsync(Guid appId, string name) { - return null; + var key = Tuple.Create(appId, name); + + if (!schemaIds.TryGetValue(key, out var id)) + { + id = await schemaRepository.FindSchemaIdAsync(appId, name); + + schemaIds[key] = id; + } + + return id; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs index 2e567dc45..208ff3d5d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories { public interface IAppRepository { - Task> QueryUserAppNamesAsync(string userId); + Task FindAppIdByNameAsync(string name); + + Task> QueryUserAppIdsAsync(string userId); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs new file mode 100644 index 000000000..9628bbd3d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ChangeContentStatus.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================= + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class ChangeContentStatus : ContentCommand + { + public Status Status { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs new file mode 100644 index 000000000..e74e06d33 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// ContentCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Security.Claims; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public abstract class ContentCommand : SchemaCommand, IAggregateCommand + { + public ClaimsPrincipal User { get; set; } + + public Guid ContentId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return ContentId; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs new file mode 100644 index 000000000..61cb005ed --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ContentDataCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public abstract class ContentDataCommand : ContentCommand + { + public NamedContentData Data { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs new file mode 100644 index 000000000..87b43582b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// CreateContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class CreateContent : ContentDataCommand + { + public bool Publish { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs similarity index 65% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs rename to src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs index a188fead4..463f68010 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs @@ -1,16 +1,14 @@ // ========================================================================== -// IAssetEventConsumer.cs +// DeleteContent.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public interface IAssetEventConsumer : IEventConsumer + public sealed class DeleteContent : ContentCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs new file mode 100644 index 000000000..5f250f517 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// PatchContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class PatchContent : ContentDataCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs new file mode 100644 index 000000000..3d6df181b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UpdateContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class UpdateContent : ContentDataCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs new file mode 100644 index 000000000..95d376ec6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// ContentCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly IAppProvider appProvider; + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + private readonly IScriptEngine scriptEngine; + + public ContentCommandMiddleware( + IAggregateHandler handler, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + IContentRepository contentRepository) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentRepository, nameof(contentRepository)); + + this.handler = handler; + this.appProvider = appProvider; + this.scriptEngine = scriptEngine; + this.assetRepository = assetRepository; + this.contentRepository = contentRepository; + } + + protected async Task On(CreateContent command, CommandContext context) + { + await handler.CreateAsync(context, async content => + { + GuardContent.CanCreate(command); + + var operationContext = await CreateContext(command, content, () => "Failed to create content."); + + if (command.Publish) + { + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); + } + + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); + await operationContext.EnrichAsync(); + await operationContext.ValidateAsync(false); + + content.Create(command); + + context.Complete(EntityCreatedResult.Create(command.Data, content.Version)); + }); + } + + protected async Task On(UpdateContent command, CommandContext context) + { + await handler.UpdateAsync(context, async content => + { + GuardContent.CanUpdate(command); + + var operationContext = await CreateContext(command, content, () => "Failed to update content."); + + await operationContext.ValidateAsync(true); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); + + content.Update(command); + + context.Complete(new ContentDataChangedResult(content.State.Data, content.Version)); + }); + } + + protected async Task On(PatchContent command, CommandContext context) + { + await handler.UpdateAsync(context, async content => + { + GuardContent.CanPatch(command); + + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); + + await operationContext.ValidateAsync(true); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); + + content.Patch(command); + + context.Complete(new ContentDataChangedResult(content.State.Data, content.Version)); + }); + } + + protected Task On(ChangeContentStatus command, CommandContext context) + { + return handler.UpdateAsync(context, async content => + { + GuardContent.CanChangeContentStatus(content.State.Status, command); + + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); + + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); + + content.ChangeStatus(command); + }); + } + + protected Task On(DeleteContent command, CommandContext context) + { + return handler.UpdateAsync(context, async content => + { + GuardContent.CanDelete(command); + + var operationContext = await CreateContext(command, content, () => "Failed to delete content."); + + await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); + + content.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + + private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message) + { + var operationContext = + await ContentOperationContext.CreateAsync( + contentRepository, + content, + command, + appProvider, + assetRepository, + scriptEngine, + message); + + return operationContext; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs new file mode 100644 index 000000000..1a8937e10 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// ContentChangedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentDataChangedResult : EntitySavedResult + { + public NamedContentData Data { get; } + + public ContentDataChangedResult(NamedContentData data, long version) + : base(version) + { + Data = data; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs new file mode 100644 index 000000000..6284d11a7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// ContentDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentDomainObject : DomainObjectBase + { + public ContentDomainObject Create(CreateContent command) + { + VerifyNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + + if (command.Publish) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); + } + + return this; + } + + public ContentDomainObject Delete(DeleteContent command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + + return this; + } + + public ContentDomainObject ChangeStatus(ChangeContentStatus command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); + + return this; + } + + public ContentDomainObject Update(UpdateContent command) + { + VerifyCreatedAndNotDeleted(); + + if (!command.Data.Equals(State.Data)) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); + } + + return this; + } + + public ContentDomainObject Patch(PatchContent command) + { + VerifyCreatedAndNotDeleted(); + + var newData = State.Data.MergeInto(command.Data); + + if (!newData.Equals(State.Data)) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = newData })); + } + + return this; + } + + private void VerifyNotCreated() + { + if (State.Data != null) + { + throw new DomainException("Content has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (State.IsDeleted || State.Data == null) + { + throw new DomainException("Content has already been deleted or not created yet."); + } + } + + protected override void OnRaised(Envelope @event) + { + UpdateState(State.Apply(@event)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs new file mode 100644 index 000000000..6024c0b47 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// ContentOperationContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentOperationContext + { + private ContentDomainObject content; + private ContentCommand command; + private IContentRepository contentRepository; + private IAssetRepository assetRepository; + private IScriptEngine scriptEngine; + private ISchemaEntity schemaEntity; + private IAppEntity appEntity; + private Func message; + + public static async Task CreateAsync( + IContentRepository contentRepository, + ContentDomainObject content, + ContentCommand command, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + Func message) + { + var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id); + + var context = new ContentOperationContext + { + appEntity = appEntity, + assetRepository = assetRepository, + contentRepository = contentRepository, + content = content, + command = command, + message = message, + schemaEntity = schemaEntity, + scriptEngine = scriptEngine + }; + + return context; + } + + public Task EnrichAsync() + { + if (command is ContentDataCommand dataCommand) + { + dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); + } + + return TaskHelper.Done; + } + + public async Task ValidateAsync(bool partial) + { + if (command is ContentDataCommand dataCommand) + { + var errors = new List(); + + var appId = command.AppId.Id; + + var ctx = + new ValidationContext( + (contentIds, schemaId) => + { + return QueryContentsAsync(appId, schemaId, contentIds); + }, + assetIds => + { + return QueryAssetsAsync(appId, assetIds); + }); + + if (partial) + { + await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); + } + else + { + await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); + } + + if (errors.Count > 0) + { + throw new ValidationException(message(), errors.ToArray()); + } + } + } + + private async Task> QueryAssetsAsync(Guid appId, IEnumerable assetIds) + { + return await assetRepository.QueryAsync(appId, null, new HashSet(assetIds), null, int.MaxValue, 0); + } + + private async Task> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable contentIds) + { + return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); + } + + public Task ExecuteScriptAndTransformAsync(Func script, object operation) + { + if (command is ContentDataCommand dataCommand) + { + var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data }; + + dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity)); + } + + return TaskHelper.Done; + } + + public Task ExecuteScriptAsync(Func script, object operation) + { + var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString() }; + + scriptEngine.Execute(ctx, script(schemaEntity)); + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index d04adaa1c..d9144cafb 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.modelBuilder = modelBuilder; } - public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) + public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1) { Guard.NotNull(app, nameof(app)); Guard.NotNull(user, nameof(user)); @@ -60,7 +60,10 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await FindSchemaAsync(app, schemaIdOrName); - var content = await contentRepository.FindContentAsync(app, schema, id); + var content = + version > 0 ? + await contentRepository.FindContentAsync(app, schema, id, version) : + await contentRepository.FindContentAsync(app, schema, id); if (content == null || (content.Status != Status.Published && !isFrontendClient)) { @@ -155,12 +158,12 @@ namespace Squidex.Domain.Apps.Entities.Contents if (Guid.TryParse(schemaIdOrName, out var id)) { - schema = await appProvider.GetSchemaAsync(app.Name, id); + schema = await appProvider.GetSchemaAsync(app.Id, id); } if (schema == null) { - schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); + schema = await appProvider.GetSchemaAsync(app.Id, schemaIdOrName); } if (schema == null) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 1a7f208e1..b1647d210 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL if (modelContext == null) { - var allSchemas = await appProvider.GetSchemasAsync(app.Name); + var allSchemas = await appProvider.GetSchemasAsync(app.Id); modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs new file mode 100644 index 000000000..f93c685bc --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// GuardContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Guards +{ + public static class GuardContent + { + public static void CanCreate(CreateContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot created content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanUpdate(UpdateContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot update content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanPatch(PatchContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot patch content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanChangeContentStatus(Status status, ChangeContentStatus command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot change status.", error => + { + if (!StatusFlow.Exists(command.Status) || !StatusFlow.CanChange(status, command.Status)) + { + error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); + } + }); + } + + public static void CanDelete(DeleteContent command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 99517290d..a9a01d2df 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); - Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); + Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1); Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 79de0dbf0..857cc189a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -29,5 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); + + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index bee8e3061..356137ea9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -6,17 +6,60 @@ // All rights reserved. // ========================================================================== +using System; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Contents.State { - public sealed class ContentState : DomainObjectState + public class ContentState : DomainObjectState, IContentEntity { [JsonProperty] - public IdContentData Data { get; set; } + public NamedContentData Data { get; set; } [JsonProperty] - public string Status { get; set; } + public Guid AppId { get; set; } + + [JsonProperty] + public Guid SchemaId { get; set; } + + [JsonProperty] + public Status Status { get; set; } + + [JsonProperty] + public bool IsDeleted { get; set; } + + protected void On(ContentCreated @event) + { + SchemaId = @event.SchemaId.Id; + + Data = @event.Data; + } + + protected void On(ContentUpdated @event) + { + Data = @event.Data; + } + + protected void On(ContentStatusChanged @event) + { + Status = @event.Status; + } + + protected void On(ContentDeleted @event) + { + IsDeleted = true; + } + + public ContentState Apply(Envelope @event) + { + var payload = (SquidexEvent)@event.Payload; + + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 3775b6244..0a7dade8a 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -14,6 +14,10 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion, IUpdateableEntity, IUpdateableEntityWithCreatedBy, IUpdateableEntityWithLastModifiedBy, diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index 404a62e78..a8bef2d45 100644 --- a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -17,17 +17,17 @@ namespace Squidex.Domain.Apps.Entities { public interface IAppProvider { - Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id); + Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id); Task GetAppAsync(string appName); - Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false); + Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false); - Task GetSchemaAsync(string appName, string name, bool provideDeleted = false); + Task GetSchemaAsync(Guid appId, string name, bool provideDeleted = false); - Task> GetSchemasAsync(string appName); + Task> GetSchemasAsync(Guid appId); - Task> GetRulesAsync(string appName); + Task> GetRulesAsync(Guid appId); Task> GetUserApps(string userId); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs index 5a06c7b0c..ed9549606 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards } else { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); errors.Foreach(error); } @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (command.Trigger != null) { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); errors.Foreach(error); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index febb77364..faed510c2 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -26,12 +26,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards SchemaProvider = schemaProvider; } - public static Task> ValidateAsync(string appName, RuleTrigger action, IAppProvider appProvider) + public static Task> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider) { Guard.NotNull(action, nameof(action)); Guard.NotNull(appProvider, nameof(appProvider)); - var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appName, x)); + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); return action.Accept(visitor); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs index 3a3f309ab..2dbbfe1e6 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories { public interface IRuleRepository { - Task> QueryRuleIdsAsync(Guid appId); + Task> QueryRuleIdsAsync(Guid appId); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index 31caed52d..d3fcbf959 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { if (@event.Payload is AppEvent appEvent) { - var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name); + var rules = await appProvider.GetRulesAsync(appEvent.AppId.Id); foreach (var ruleEntity in rules) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index 70670d74d..420520df2 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); } - if (await appProvider.GetSchemaAsync(command.AppId.Name, command.Name) != null) + if (await appProvider.GetSchemaAsync(command.AppId.Id, command.Name) != null) { error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name))); } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs index 04df68b68..642072660 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -14,8 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Repositories { public interface ISchemaRepository { - Task FindSchemaNameAsync(Guid schemaId); + Task FindSchemaIdAsync(Guid appId, string name); - Task> QuerySchemaNamesAsync(Guid appId); + Task> QuerySchemaIdsAsync(Guid appId); } } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 603f41d85..6228da562 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -6,6 +6,8 @@ // All rights reserved. // ========================================================================== +using System; +using System.Linq.Expressions; using System.Threading.Tasks; using MongoDB.Driver; @@ -31,5 +33,26 @@ namespace Squidex.Infrastructure.MongoDb return true; } + + public static IFindFluent Only(this IFindFluent find, + Expression> include) + { + return find.Project(Builders.Projection.Include(include)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2) + { + return find.Project(Builders.Projection.Include(include1).Include(include2)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2, + Expression> include3) + { + return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); + } } } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index f29c431a0..393c41e6c 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -59,8 +59,8 @@ namespace Squidex.Infrastructure.States if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key) - .Project>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); if (existingVersion != null) { diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs index 43acf6f5b..2764342be 100644 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs @@ -17,8 +17,6 @@ namespace Squidex.Infrastructure.UsageTracking { public sealed class MongoUsageStore : MongoRepositoryBase, IUsageStore { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - public MongoUsageStore(IMongoDatabase database) : base(database) { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 62ba1fdcc..d3cee9702 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -18,18 +18,17 @@ namespace Squidex.Infrastructure.Commands public abstract class DomainObjectBase : IDomainObject where TState : new() { private readonly List> uncomittedEvents = new List>(); - private int version = -1; private TState state = new TState(); private IPersistence persistence; - public TState State + public long Version { - get { return state; } + get { return persistence.Version; } } - public int Version + public TState State { - get { return version; } + get { return state; } } public IReadOnlyList> GetUncomittedEvents() diff --git a/src/Squidex.Infrastructure/Commands/IDomainObject.cs b/src/Squidex.Infrastructure/Commands/IDomainObject.cs index 971382c81..f1df5c41e 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainObject.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainObject.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Commands { public interface IDomainObject : IStatefulObject { - int Version { get; } + long Version { get; } Task WriteAsync(ISemanticLog log); } diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 4a36c48bf..51b17312c 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.States { public interface IPersistence { + long Version { get; } + Task WriteEventsAsync(params Envelope[] @events); Task WriteSnapshotAsync(TState state); diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index d127964ea..5be140a1c 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -26,6 +26,11 @@ namespace Squidex.Infrastructure.States private long positionSnapshot = -1; private long positionEvent = -1; + public long Version + { + get { return Math.Max(positionEvent, positionSnapshot); } + } + public Persistence(string ownerKey, Action invalidate, IEventStore eventStore, @@ -85,17 +90,17 @@ namespace Squidex.Infrastructure.States } } - var maxVersion = Math.Max(positionEvent, positionSnapshot); + var newVersion = Version; - if (expectedVersion.HasValue && expectedVersion.Value != maxVersion) + if (expectedVersion.HasValue && expectedVersion.Value != newVersion) { - if (maxVersion == -1) + if (newVersion == -1) { throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); } else { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), maxVersion, expectedVersion.Value); + throw new DomainObjectVersionException(ownerKey, typeof(TOwner), newVersion, expectedVersion.Value); } } } diff --git a/src/Squidex/Areas/Api/Controllers/ApiController.cs b/src/Squidex/Areas/Api/Controllers/ApiController.cs index fafbca6e0..66d163d43 100644 --- a/src/Squidex/Areas/Api/Controllers/ApiController.cs +++ b/src/Squidex/Areas/Api/Controllers/ApiController.cs @@ -9,7 +9,7 @@ using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; @@ -41,6 +41,11 @@ namespace Squidex.Areas.Api.Controllers get { return App.Name; } } + protected Guid AppId + { + get { return App.Id; } + } + protected ApiController(ICommandBus commandBus) { Guard.NotNull(commandBus, nameof(commandBus)); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 0c2ab7276..6f0130bc5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index c6b6fc22c..3a2faddc5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -12,8 +12,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 1c499ff2e..4d055de27 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 71aa34709..78cd235c1 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -13,9 +13,9 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 2d5e3b28e..330209f41 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -11,7 +11,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 7f7313c40..d65cf73f7 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -16,10 +16,10 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Assets.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Write.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs index b2db2f78a..6becf0403 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs @@ -8,7 +8,7 @@ using System; using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure.Commands; namespace Squidex.Areas.Api.Controllers.Assets.Models diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs index a324b9ceb..c038d4f48 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs @@ -7,8 +7,8 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Write.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; namespace Squidex.Areas.Api.Controllers.Assets.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs index 84a0afd57..a1f54b271 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Contents.Generator; -using Squidex.Domain.Apps.Read; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; @@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(0)] public async Task GetSwagger(string app) { - var schemas = await appProvider.GetSchemasAsync(AppName); + var schemas = await appProvider.GetSchemasAsync(AppId); var swaggerDocument = await schemasSwaggerGenerator.Generate(App, schemas); diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 0f8612fe5..c01ee14ec 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -16,10 +16,9 @@ using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Write.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -33,17 +32,14 @@ namespace Squidex.Areas.Api.Controllers.Contents public sealed class ContentsController : ApiController { private readonly IContentQueryService contentQuery; - private readonly IContentVersionLoader contentVersionLoader; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, - IContentVersionLoader contentVersionLoader, IGraphQLService graphQl) : base(commandBus) { this.contentQuery = contentQuery; - this.contentVersionLoader = contentVersionLoader; this.graphQl = graphQl; } @@ -142,9 +138,16 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetContentVersion(string name, Guid id, int version) { - var contentData = await contentVersionLoader.LoadAsync(App.Id, id, version); + var content = await contentQuery.FindContentAsync(App, name, User, id, version); - var response = contentData; + var response = SimpleMapper.Map(content.Content, new ContentDto()); + + if (content.Content.Data != null) + { + var isFrontendClient = User.IsFrontendClient(); + + response.Data = content.Content.Data.ToApiModel(content.Schema.SchemaDef, App.LanguagesConfig, !isFrontendClient); + } Response.Headers["ETag"] = new StringValues(version.ToString()); diff --git a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs index 82295c3a5..c895641f1 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs @@ -16,8 +16,8 @@ using NSwag; using NSwag.AspNetCore; using NSwag.SwaggerGeneration; using Squidex.Config; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs index 2a048594b..62471547c 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs @@ -10,7 +10,7 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index d0e224275..a4570cf22 100644 --- a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.History.Models; -using Squidex.Domain.Apps.Read.History.Repositories; +using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 6481585a7..e7a42a3fc 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -12,8 +12,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Plans.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Plans { CurrentPlanId = planId, Plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList(), - PlanOwner = App.PlanOwner, + PlanOwner = App.Plan?.Owner.Identifier, HasPortal = appPlansBillingManager.HasPortal }; diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs index 3cf4b64ed..072ce5141 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs @@ -7,8 +7,8 @@ // ========================================================================== using System; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index 1a09e88be..7a973ee52 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -10,7 +10,7 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Entities.Rules; namespace Squidex.Areas.Api.Controllers.Rules.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 5d0e34c75..481e4f309 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -14,9 +14,9 @@ using NodaTime; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Areas.Api.Controllers.Rules.Models.Converters; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -59,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task GetRules(string app) { - var rules = await appProvider.GetRulesAsync(AppName); + var rules = await appProvider.GetRulesAsync(AppId); var response = rules.Select(r => r.ToModel()); diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs index 7233f65bc..dc7ba4c68 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs @@ -8,8 +8,8 @@ using System.Collections.Generic; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 7fbda49ad..40c40429e 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Schemas.Models; -using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 2e5b8e00f..26ba58ac0 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -15,9 +15,9 @@ using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models.Converters; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -56,7 +56,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(0)] public async Task GetSchemas(string app) { - var schemas = await appProvider.GetSchemasAsync(AppName); + var schemas = await appProvider.GetSchemasAsync(AppId); var response = schemas.Select(s => s.ToModel()).ToList(); @@ -83,11 +83,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas if (Guid.TryParse(name, out var id)) { - entity = await appProvider.GetSchemaAsync(AppName, id); + entity = await appProvider.GetSchemaAsync(AppId, id); } else { - entity = await appProvider.GetSchemaAsync(AppName, name); + entity = await appProvider.GetSchemaAsync(AppId, name); } if (entity == null || entity.IsDeleted) diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index d9895c4b1..d12f17656 100644 --- a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -12,8 +12,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Statistics.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.UsageTracking; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index 4d86a172b..d384db228 100644 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -15,7 +15,7 @@ using IdentityServer4.Stores; using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; namespace Squidex.Areas.IdentityServer.Config diff --git a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs index 603f7bb94..a17190981 100644 --- a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs +++ b/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Security; namespace Squidex.Areas.Portal.Middlewares diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index 13f0ff490..d11421a78 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -13,23 +13,20 @@ using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.Actions; using Squidex.Domain.Apps.Core.HandleRules.Triggers; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.Edm; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Read.History; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.State; -using Squidex.Domain.Apps.Read.State.Grains; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Edm; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.States; @@ -103,15 +100,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); - services.AddSingletonAs(c => - new CompoundEventConsumer(c.GetServices().ToArray())); - services.AddSingletonAs(c => { var allEventConsumers = c.GetServices(); @@ -120,11 +111,6 @@ namespace Squidex.Config.Domain }); services.AddSingletonAs(); - - services.AddTransient(typeof(DomainObjectWrapper<>)); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); } } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 5c76a96e1..a66dd3078 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -13,22 +13,30 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Newtonsoft.Json; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.History; -using Squidex.Domain.Apps.Read.History.Repositories; -using Squidex.Domain.Apps.Read.MongoDb.Assets; -using Squidex.Domain.Apps.Read.MongoDb.Contents; -using Squidex.Domain.Apps.Read.MongoDb.History; -using Squidex.Domain.Apps.Read.MongoDb.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; +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.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.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.States; using Squidex.Infrastructure.UsageTracking; using Squidex.Shared.Users; @@ -55,8 +63,8 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As() + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) + .As>() .As(); services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) @@ -78,12 +86,33 @@ namespace Squidex.Config.Domain .As() .As(); + services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) + .As() + .As(); + + services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) + .As() + .As>() + .As(); + + services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) + .As() + .As>() + .As(); + services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) .As() + .As>() .As(); - services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) - .As() + services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase)) + .As() + .As>() + .As(); + + services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) + .As() + .As>() .As(); services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) @@ -91,14 +120,9 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) - .As() - .As() - .As(); - services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase)) .As() - .As() + .As() .As(); } }); diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index b0b7e070d..56dba6359 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -7,13 +7,13 @@ // ========================================================================== using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Write.Apps; -using Squidex.Domain.Apps.Write.Assets; -using Squidex.Domain.Apps.Write.Contents; -using Squidex.Domain.Apps.Write.Rules; -using Squidex.Domain.Apps.Write.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.State.SchemaDefs; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; using Squidex.Pipeline.CommandMiddlewares; @@ -30,9 +30,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); @@ -63,17 +60,11 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs>(c => (id => new AppDomainObject(id, -1))); - services.AddSingletonAs>(c => (id => new RuleDomainObject(id, -1))); - services.AddSingletonAs>(c => (id => new AssetDomainObject(id, -1))); - services.AddSingletonAs>(c => (id => new ContentDomainObject(id, -1))); - - services.AddSingletonAs>(c => - { - var fieldRegistry = c.GetRequiredService(); - - return id => new SchemaDomainObject(id, -1, fieldRegistry); - }); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/Squidex/Config/MyUsageOptions.cs b/src/Squidex/Config/MyUsageOptions.cs index 370c4a90b..53c558bad 100644 --- a/src/Squidex/Config/MyUsageOptions.cs +++ b/src/Squidex/Config/MyUsageOptions.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; namespace Squidex.Config { diff --git a/src/Squidex/Pipeline/ApiCostsFilter.cs b/src/Squidex/Pipeline/ApiCostsFilter.cs index 358c1a6f3..557fc9647 100644 --- a/src/Squidex/Pipeline/ApiCostsFilter.cs +++ b/src/Squidex/Pipeline/ApiCostsFilter.cs @@ -11,7 +11,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.UsageTracking; namespace Squidex.Pipeline diff --git a/src/Squidex/Pipeline/AppApiFilter.cs b/src/Squidex/Pipeline/AppApiFilter.cs index a789893c7..88fa4555a 100644 --- a/src/Squidex/Pipeline/AppApiFilter.cs +++ b/src/Squidex/Pipeline/AppApiFilter.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; namespace Squidex.Pipeline { diff --git a/src/Squidex/Pipeline/AppPermissionAttribute.cs b/src/Squidex/Pipeline/AppPermissionAttribute.cs index 82517dd38..d6405f89a 100644 --- a/src/Squidex/Pipeline/AppPermissionAttribute.cs +++ b/src/Squidex/Pipeline/AppPermissionAttribute.cs @@ -12,7 +12,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Security; using Squidex.Shared.Identity; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs index 07a24847a..1cda63c8f 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs @@ -10,7 +10,7 @@ using System; using System.Security; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Write; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index d5a277039..856af45fe 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -9,7 +9,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Write; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index 596ea587f..b1b0df833 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -9,10 +9,8 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write; -using Squidex.Domain.Apps.Write.Schemas; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -44,11 +42,11 @@ namespace Squidex.Pipeline.CommandMiddlewares if (Guid.TryParse(schemaName, out var id)) { - schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Name, id); + schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, id); } else { - schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Name, schemaName); + schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, schemaName); } if (schema == null) diff --git a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs index e6d06026e..5f6b783d3 100644 --- a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs +++ b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs @@ -8,11 +8,11 @@ using Microsoft.Extensions.Options; using Squidex.Config; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure.Assets; namespace Squidex.Pipeline diff --git a/src/Squidex/Pipeline/IAppFeature.cs b/src/Squidex/Pipeline/IAppFeature.cs index d85882707..fa2ced5e2 100644 --- a/src/Squidex/Pipeline/IAppFeature.cs +++ b/src/Squidex/Pipeline/IAppFeature.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities.Apps; namespace Squidex.Pipeline { diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index ebc86a383..bd330a626 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -31,9 +31,11 @@ + + - + @@ -41,9 +43,6 @@ - - - diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs new file mode 100644 index 000000000..b94d97994 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -0,0 +1,244 @@ +// ========================================================================== +// ContentCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentCommandMiddlewareTests : HandlerTestBase + { + private readonly ISchemaEntity schema = A.Fake(); + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppEntity app = A.Fake(); + private readonly ClaimsPrincipal user = new ClaimsPrincipal(); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); + private readonly Guid contentId = Guid.NewGuid(); + private readonly ContentDomainObject content = new ContentDomainObject(); + private readonly ContentCommandMiddleware sut; + + private readonly NamedContentData invalidData = + new NamedContentData() + .AddField("my-field1", new ContentFieldData() + .AddValue(null)) + .AddField("my-field2", new ContentFieldData() + .AddValue(1)); + private readonly NamedContentData data = + new NamedContentData() + .AddField("my-field1", new ContentFieldData() + .AddValue(1)) + .AddField("my-field2", new ContentFieldData() + .AddValue(1)); + private readonly NamedContentData patch = + new NamedContentData() + .AddField("my-field1", new ContentFieldData() + .AddValue(1)); + + public ContentCommandMiddlewareTests() + { + var schemaDef = + new Schema("my-schema") + .AddField(new NumberField(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })) + .AddField(new NumberField(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false })); + + sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), scriptEngine, A.Dummy()); + + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + + A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app); + + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.ScriptCreate).Returns(""); + A.CallTo(() => schema.ScriptChange).Returns(""); + A.CallTo(() => schema.ScriptUpdate).Returns(""); + A.CallTo(() => schema.ScriptDelete).Returns(""); + + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema)); + } + + [Fact] + public async Task Create_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestCreate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Create_should_create_content() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user }); + + await TestCreate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(data, context.Result>().IdOrValue); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_also_invoke_publish_script_when_publishing() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true }); + + await TestCreate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(data, context.Result>().IdOrValue); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + CreateContent(); + + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + CreateContent(); + + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(data, context.Result().Data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + CreateContent(); + + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Patch_should_update_domain_object() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)).Returns(patch); + + CreateContent(); + + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.NotNull(context.Result().Data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_publish_domain_object() + { + CreateContent(); + + var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateContent(); + + var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(command); + }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + private void CreateContent() + { + content.Create(new CreateContent { Data = data }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs new file mode 100644 index 000000000..400964524 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -0,0 +1,269 @@ +// ========================================================================== +// ContentDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentDomainObjectTests : HandlerTestBase + { + private readonly NamedContentData data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData otherData = + new NamedContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly Guid contentId = Guid.NewGuid(); + private readonly ContentDomainObject sut = new ContentDomainObject(); + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateContent { Data = data }); + + Assert.Throws(() => + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + }); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }) + ); + } + + [Fact] + public void Create_should_also_publish_if_set_to_true() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Update(CreateContentCommand(new UpdateContent())); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateContent(); + + sut.Update(CreateContentCommand(new UpdateContent { Data = otherData })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + } + + [Fact] + public void Update_should_not_create_event_for_same_data() + { + CreateContent(); + UpdateContent(); + + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + + sut.GetUncomittedEvents().Should().BeEmpty(); + } + + [Fact] + public void Patch_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Patch(CreateContentCommand(new PatchContent { Data = data })); + }); + } + + [Fact] + public void Patch_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Patch(CreateContentCommand(new PatchContent())); + }); + } + + [Fact] + public void Patch_should_create_events() + { + CreateContent(); + + sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + } + + [Fact] + public void Patch_should_not_create_event_for_same_data() + { + CreateContent(); + UpdateContent(); + + sut.Patch(CreateContentCommand(new PatchContent { Data = data })); + + sut.GetUncomittedEvents().Should().BeEmpty(); + } + + [Fact] + public void ChangeStatus_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); + }); + } + + [Fact] + public void ChangeStatus_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); + }); + } + + [Fact] + public void ChangeStatus_should_refresh_properties_and_create_events() + { + CreateContent(); + + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); + + Assert.Equal(Status.Published, sut.State.Status); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_update_properties_and_create_events() + { + CreateContent(); + + sut.Delete(CreateContentCommand(new DeleteContent())); + + Assert.True(sut.State.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + } + + private void CreateContent() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + sut.ClearUncommittedEvents(); + } + + private void UpdateContent() + { + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + sut.ClearUncommittedEvents(); + } + + private void ChangeStatus(Status status) + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); + sut.ClearUncommittedEvents(); + } + + private void DeleteContent() + { + sut.Delete(CreateContentCommand(new DeleteContent())); + sut.ClearUncommittedEvents(); + } + + protected T CreateContentEvent(T @event) where T : ContentEvent + { + @event.ContentId = contentId; + + return CreateEvent(@event); + } + + protected T CreateContentCommand(T command) where T : ContentCommand + { + command.ContentId = contentId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs new file mode 100644 index 000000000..0dec9d79f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// ContentEventTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Events.Contents.Old; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable CS0612 // Type or member is obsolete + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentEventTests + { + private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); + private readonly Guid contentId = Guid.NewGuid(); + + [Fact] + public void Should_migrate_content_published_to_content_status_changed() + { + var source = CreateEvent(new ContentPublished()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); + } + + [Fact] + public void Should_migrate_content_unpublished_to_content_status_changed() + { + var source = CreateEvent(new ContentUnpublished()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); + } + + [Fact] + public void Should_migrate_content_restored_to_content_status_changed() + { + var source = CreateEvent(new ContentRestored()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); + } + + [Fact] + public void Should_migrate_content_archived_to_content_status_changed() + { + var source = CreateEvent(new ContentArchived()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); + } + + private T CreateEvent(T contentEvent) where T : ContentEvent + { + contentEvent.Actor = actor; + contentEvent.AppId = appId; + contentEvent.SchemaId = schemaId; + contentEvent.ContentId = contentId; + + return contentEvent; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 90fddc2c4..ecb503b83 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_schema_from_id_if_string_is_guid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); var result = await sut.FindSchemaAsync(app, schemaId.ToString()); @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_schema_from_name_if_string_not_guid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema", false)) .Returns(schema); var result = await sut.FindSchemaAsync(app, "my-schema"); @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_throw_if_schema_not_found() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema", false)) .Returns((ISchemaEntity)null); await Assert.ThrowsAsync(() => sut.FindSchemaAsync(app, "my-schema")); @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_content_from_repository_and_transform() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) .Returns(content); @@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_throw_if_content_to_find_does_not_exist() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) .Returns((IContentEntity)null); @@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupFakeWithIdQuery(Status[] status, HashSet ids) { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) .Returns(new List { content }); @@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupFakeWithOdataQuery(Status[] status) { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) .Returns(new List { content }); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs index c4624d1ff..b43cfe6ab 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var allSchemas = new List { schema }; - A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas); + A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas); sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator()); } @@ -391,7 +391,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); @@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refContents = new List { contentRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) @@ -543,7 +543,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refAssets = new List { assetRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) @@ -602,7 +602,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs new file mode 100644 index 000000000..49a9c465e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// GuardContentTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Guard +{ + public class GuardContentTests + { + [Fact] + public void CanCreate_should_throw_exception_if_data_is_null() + { + var command = new CreateContent(); + + Assert.Throws(() => GuardContent.CanCreate(command)); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_data_is_not_null() + { + var command = new CreateContent { Data = new NamedContentData() }; + + GuardContent.CanCreate(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_data_is_null() + { + var command = new UpdateContent(); + + Assert.Throws(() => GuardContent.CanUpdate(command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_data_is_not_null() + { + var command = new UpdateContent { Data = new NamedContentData() }; + + GuardContent.CanUpdate(command); + } + + [Fact] + public void CanPatch_should_throw_exception_if_data_is_null() + { + var command = new PatchContent(); + + Assert.Throws(() => GuardContent.CanPatch(command)); + } + + [Fact] + public void CanPatch_should_not_throw_exception_if_data_is_not_null() + { + var command = new PatchContent { Data = new NamedContentData() }; + + GuardContent.CanPatch(command); + } + + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() + { + var command = new ChangeContentStatus { Status = (Status)10 }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); + } + + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); + } + + [Fact] + public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + GuardContent.CanChangeContentStatus(Status.Draft, command); + } + + [Fact] + public void CanPatch_should_not_throw_exception() + { + var command = new DeleteContent(); + + GuardContent.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs index bd00e634b..1f4d4d239 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -/* using System; using System.Collections.Immutable; using FakeItEasy; @@ -390,5 +389,4 @@ namespace Squidex.Domain.Apps.Entities.Contents return query; } } -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs index 949759999..81d5f20fe 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards public GuardRuleTests() { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Name, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) .Returns(A.Fake()); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs index f86e99df0..f24ecd9cd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -19,12 +19,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers public class ContentChangedTriggerTests { private readonly IAppProvider appProvider = A.Fake(); - private readonly string appName = "my-app"; + private readonly Guid appId = Guid.NewGuid(); [Fact] public async Task Should_add_error_if_schemas_ids_are_not_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) .Returns(Task.FromResult(null)); var trigger = new ContentChangedTrigger @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers ) }; - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.NotEmpty(errors); } @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers { var trigger = new ContentChangedTrigger(); - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.Empty(errors); } @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers Schemas = ImmutableList.Empty }; - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.Empty(errors); } @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers [Fact] public async Task Should_not_add_error_if_schemas_ids_are_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) .Returns(A.Fake()); var trigger = new ContentChangedTrigger @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers ) }; - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.Empty(errors); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs index 3b04844ff..f721df368 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public RuleCommandMiddlewareTests() { - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) .Returns(A.Fake()); sut = new RuleCommandMiddleware(Handler, appProvider); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 3cd294d17..fb6b8ce31 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); - A.CallTo(() => appProvider.GetRulesAsync(appId.Name)) + A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); A.CallTo(() => ruleService.CreateJob(rule1, @event)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index eab4caa37..f89e8cc4f 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -12,7 +12,9 @@ + + diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index ea6bf1899..e95fca91e 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; using Xunit; @@ -19,42 +20,29 @@ namespace Squidex.Infrastructure.Commands { public class AggregateHandlerTests { + private readonly ISemanticLog log = A.Fake(); private readonly IServiceProvider serviceProvider = A.Fake(); private readonly IStore store = A.Fake(); private readonly IStateFactory stateFactory = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); - private readonly DomainObjectFactoryFunction factory; private readonly CommandContext context; - private readonly AggregateHandler sut; - private readonly DomainObjectWrapper domainObjectWrapper = new DomainObjectWrapper(); private readonly Guid domainObjectId = Guid.NewGuid(); - private readonly MyDomainObject domainObject; + private readonly MyDomainObject domainObject = new MyDomainObject(); + private readonly AggregateHandler sut; public AggregateHandlerTests() { - factory = new DomainObjectFactoryFunction(id => domainObject); - - domainObject = - new MyDomainObject(domainObjectId, 1) - .RaiseNewEvent(event1) - .RaiseNewEvent(event2); - - context = new CommandContext(new MyCommand { AggregateId = domainObject.Id }); + context = new CommandContext(new MyCommand { AggregateId = domainObjectId }); A.CallTo(() => store.WithEventSourcing(domainObjectId.ToString(), A, Task>>.Ignored)) .Returns(persistence); - A.CallTo(() => serviceProvider.GetService(factory.GetType())) - .Returns(factory); - - A.CallTo(() => stateFactory.GetDetachedAsync>(domainObject.Id.ToString())) - .Returns(Task.FromResult(domainObjectWrapper)); - - sut = new AggregateHandler(stateFactory, serviceProvider); + A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) + .Returns(Task.FromResult(domainObject)); - domainObjectWrapper.ActivateAsync(domainObjectId.ToString(), store).Wait(); + sut = new AggregateHandler(stateFactory, serviceProvider, log); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 8db73aea9..903a2837a 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.Commands var domainObjectId = Guid.NewGuid(); var domainObjectVersion = 123; - var sut = new MyDomainObject(domainObjectId, domainObjectVersion); + var sut = new MyDomainObject(); Assert.Equal(domainObjectId, sut.Id); Assert.Equal(domainObjectVersion, sut.Version); @@ -34,18 +34,16 @@ namespace Squidex.Infrastructure.Commands var event1 = new MyEvent(); var event2 = new MyEvent(); - var sut = new MyDomainObject(Guid.NewGuid(), 10); - - IAggregate aggregate = sut; + var sut = new MyDomainObject(); sut.RaiseNewEvent(event1); sut.RaiseNewEvent(event2); Assert.Equal(12, sut.Version); - Assert.Equal(new IEvent[] { event1, event2 }, aggregate.GetUncomittedEvents().Select(x => x.Payload).ToArray()); + Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); - aggregate.ClearUncommittedEvents(); + sut.ClearUncommittedEvents(); Assert.Equal(0, sut.GetUncomittedEvents().Count); } @@ -56,58 +54,13 @@ namespace Squidex.Infrastructure.Commands var event1 = new MyEvent(); var event2 = new MyEvent(); - var sut = new MyDomainObject(Guid.NewGuid(), 10); - - IAggregate aggregate = sut; + var sut = new MyDomainObject(); - aggregate.ApplyEvent(new Envelope(event1)); - aggregate.ApplyEvent(new Envelope(event2)); + sut.RaiseEvent(new Envelope(event1)); + sut.RaiseEvent(new Envelope(event2)); Assert.Equal(12, sut.Version); Assert.Equal(0, sut.GetUncomittedEvents().Count); } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var user1a = new MyDomainObject(id1, 1); - var user1b = new MyDomainObject(id1, 2); - var user2a = new MyDomainObject(id2, 2); - - Assert.True(user1a.Equals(user1b)); - Assert.False(user1a.Equals(user2a)); - } - - [Fact] - public void Should_make_correct_object_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var user1a = new MyDomainObject(id1, 1); - - object user1b = new MyDomainObject(id1, 2); - object user2a = new MyDomainObject(id2, 2); - - Assert.True(user1a.Equals(user1b)); - Assert.False(user1a.Equals(user2a)); - } - - [Fact] - public void Should_provide_correct_hash_codes() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var user1a = new MyDomainObject(id1, 1); - var user1b = new MyDomainObject(id1, 2); - var user2a = new MyDomainObject(id2, 2); - - Assert.Equal(user1a.GetHashCode(), user1b.GetHashCode()); - Assert.NotEqual(user1a.GetHashCode(), user2a.GetHashCode()); - } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs index 33d65efd8..c1eeb3c4b 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs @@ -11,13 +11,8 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.Commands.TestHelpers { - internal sealed class MyDomainObject : DomainObjectBase + internal sealed class MyDomainObject : DomainObjectBase { - public MyDomainObject(Guid id, int version) - : base(id, version) - { - } - public MyDomainObject RaiseNewEvent(IEvent @event) { RaiseEvent(@event); @@ -31,9 +26,5 @@ namespace Squidex.Infrastructure.Commands.TestHelpers return this; } - - protected override void DispatchEvent(Envelope @event) - { - } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs index db9602b0c..e6d1edd31 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs @@ -33,8 +33,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => consumer1.Name).Returns(consumerName1); A.CallTo(() => consumer2.Name).Returns(consumerName2); - A.CallTo(() => factory.GetDetachedAsync(consumerName1)).Returns(actor1); - A.CallTo(() => factory.GetDetachedAsync(consumerName2)).Returns(actor2); + A.CallTo(() => factory.CreateAsync(consumerName1)).Returns(actor1); + A.CallTo(() => factory.CreateAsync(consumerName2)).Returns(actor2); sut = new EventConsumerGrainManager(new IEventConsumer[] { consumer1, consumer2 }, pubSub, factory); } diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index 76198d0b1..b23a80a3b 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -82,13 +82,15 @@ namespace Squidex.Infrastructure.States .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot))) .Returns(statefulObjectWithSnapShot); + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + .Returns(snapshotStore); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key)) .Returns(key); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObjectWithSnapshot), key)) .Returns(key); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, snapshotStore, streamNameResolver); + sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); sut.Connect(); } @@ -102,7 +104,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(event1, event2); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -115,12 +117,12 @@ namespace Squidex.Infrastructure.States { statefulObjectWithSnapShot.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 2); - await sut.GetSynchronizedAsync(key); + await sut.GetSingleAsync(key); A.CallTo(() => eventStore.GetEventsAsync(key, 3)) .MustHaveHappened(); @@ -131,12 +133,12 @@ namespace Squidex.Infrastructure.States { statefulObjectWithSnapShot.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 0, 3); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -144,12 +146,12 @@ namespace Squidex.Infrastructure.States { statefulObjectWithSnapShot.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 4, 3); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -159,7 +161,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -169,7 +171,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -179,7 +181,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - await sut.GetSynchronizedAsync(key); + await sut.GetSingleAsync(key); } [Fact] @@ -189,7 +191,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -202,12 +204,12 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - var actualObject1 = await sut.GetSynchronizedAsync(key); + var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); - var actualObject2 = await sut.GetSynchronizedAsync(key); + var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); @@ -227,7 +229,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); @@ -250,7 +252,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .Throws(new WrongEventVersionException(1, 1)); @@ -263,7 +265,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -275,14 +277,14 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); var tasks = new List>(); for (var i = 0; i < 1000; i++) { - tasks.Add(Task.Run(() => sut.GetSynchronizedAsync(key))); + tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index a57f05ab1..bb45d0cac 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -56,7 +56,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -64,8 +64,10 @@ namespace Squidex.Infrastructure.States { A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .Returns(statefulObject); + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + .Returns(snapshotStore); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, snapshotStore, streamNameResolver); + sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); sut.Connect(); } @@ -79,10 +81,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 1)); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -95,10 +97,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = 0; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, -1)); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -106,10 +108,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2)); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -117,10 +119,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, -1)); - await sut.GetSynchronizedAsync(key); + await sut.GetSingleAsync(key); } [Fact] @@ -128,7 +130,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -139,12 +141,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject1 = await sut.GetSynchronizedAsync(key); + var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); - var actualObject2 = await sut.GetSynchronizedAsync(key); + var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); @@ -155,12 +157,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject1 = await sut.GetDetachedAsync(key); + var actualObject1 = await sut.CreateAsync(key); Assert.Same(statefulObject, actualObject1); Assert.Null(cache.Get(key)); - var actualObject2 = await sut.GetDetachedAsync(key); + var actualObject2 = await sut.CreateAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Twice); @@ -180,10 +182,10 @@ namespace Squidex.Infrastructure.States message = m; }); - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, version)); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.Equal(123, statefulObject.State); @@ -206,13 +208,13 @@ namespace Squidex.Infrastructure.States var version = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, version)); A.CallTo(() => snapshotStore.WriteAsync(key, 123, version, 2)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); } @@ -222,7 +224,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -234,14 +236,14 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); var tasks = new List>(); for (var i = 0; i < 1000; i++) { - tasks.Add(Task.Run(() => sut.GetSynchronizedAsync(key))); + tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); @@ -251,7 +253,7 @@ namespace Squidex.Infrastructure.States Assert.Same(retrievedStates[0], retrievedState); } - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .MustHaveHappened(Repeated.Exactly.Once); } From 0ec9c7a66bd89fe9dc5c55f8ad2902451731a99b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 00:52:12 +0100 Subject: [PATCH 09/30] Tests improved. --- .../Apps/MongoAppEntity.cs | 10 +- .../Apps/MongoAppRepository.cs | 19 +++- .../Assets/MongoAssetRepository.cs | 4 +- .../Contents/MongoContentRepository.cs | 8 +- .../Rules/MongoRuleEntity.cs | 15 ++- .../Rules/MongoRuleRepository.cs | 16 ++-- .../Schemas/MongoSchemaEntity.cs | 13 ++- .../Schemas/MongoSchemaRepository.cs | 16 ++-- .../AppProvider.cs | 68 ++++---------- .../EntityMapper.cs | 9 -- .../MongoDb/MongoExtensions.cs | 13 +-- .../States/MongoSnapshotStore.cs | 2 +- .../Commands/AggregateHandler.cs | 2 + .../Commands/DomainObjectBase.cs | 34 ++++--- .../States/IPersistence.cs | 5 +- .../States/Persistence.cs | 36 ++++--- src/Squidex/Config/Domain/StoreServices.cs | 16 ++-- .../Commands/AggregateHandlerTests.cs | 49 +++++----- .../Commands/DomainObjectBaseTests.cs | 93 +++++++++++++++---- .../Commands/TestHelpers/MyDomainObject.cs | 14 --- .../DispatchingTests.cs | 8 +- .../Grains/EventConsumerGrainTests.cs | 22 ++--- .../States/StateEventSourcingTests.cs | 10 +- .../Tasks/SingleThreadedDispatcherTests.cs | 4 +- 24 files changed, 281 insertions(+), 205 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs index 64f694162..bee8118bb 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -9,6 +9,7 @@ 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 { @@ -21,14 +22,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps [BsonElement] [BsonRequired] - public AppState State { get; set; } + public int Version { get; set; } [BsonElement] [BsonRequired] - public int Version { get; set; } + public string Name { get; set; } [BsonElement] [BsonRequired] public string[] UserIds { get; set; } + + [BsonJson] + [BsonElement] + [BsonRequired] + public AppState State { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index ee186a620..f77ab41fa 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -25,18 +25,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps { } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override string CollectionName() { - return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); + return "Snapshots_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 FindAppIdByNameAsync(string name) { var appEntity = - await Collection.Find(x => x.State.Name == name).Only(x => x.Id) + await Collection.Find(x => x.Name == name).Only(x => x.Id) .FirstOrDefaultAsync(); - return appEntity != null ? Guid.Parse(appEntity.Id) : Guid.Empty; + return appEntity != null ? Guid.Parse(appEntity["_id"].AsString) : Guid.Empty; } public async Task> QueryUserAppIdsAsync(string userId) @@ -45,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps await Collection.Find(x => x.UserIds.Contains(userId)).Only(x => x.Id) .ToListAsync(); - return appEntities.Select(x => Guid.Parse(x.Id)).ToList(); + return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } public async Task> QueryUserAppNamesAsync(string userId) @@ -75,9 +81,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps { try { + value.Version = newVersion; + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) + .Set(x => x.Name, value.Name) .Set(x => x.State, value) .Set(x => x.Version, newVersion), Upsert); diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 445ebe747..36dc642cb 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -116,6 +116,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { try { + value.Version = newVersion; + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) @@ -132,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets if (existingVersion != null) { - throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + throw new InconsistentStateException(existingVersion["Version"].AsInt64, oldVersion, ex); } } else diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 8b927faf5..bac856632 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents protected override string CollectionName() { - return "Snapshots_Assets"; + return "Snapshots_Contents"; } protected override async Task SetupCollectionAsync(IMongoCollection collection) @@ -89,6 +89,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents try { + value.Version = newVersion; + await Collection.InsertOneAsync(document); } catch (MongoWriteException ex) @@ -101,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (existingVersion != null) { - throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + throw new InconsistentStateException(existingVersion["Version"].AsInt64, oldVersion, ex); } } else @@ -206,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await Collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Only(x => x.Id) .ToListAsync(); - return contentIds.Except(contentEntities.Select(x => x.Id)).ToList(); + return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); } public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs index 304e5d01a..c1ba83cbe 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -6,9 +6,11 @@ // All rights reserved. // ========================================================================== +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 { @@ -21,10 +23,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules [BsonElement] [BsonRequired] - public RuleState State { get; set; } + public int Version { get; set; } [BsonElement] [BsonRequired] - public int Version { get; set; } + public Guid AppId { get; set; } + + [BsonElement] + [BsonRequired] + public bool IsDeleted { get; set; } + + [BsonJson] + [BsonElement] + [BsonRequired] + public RuleState State { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 895bc54db..13f5f31b6 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -27,13 +27,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules protected override string CollectionName() { - return "States_Rules"; + return "Snapshots_Rules"; } protected override async Task SetupCollectionAsync(IMongoCollection collection) { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.IsDeleted)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); } public async Task<(RuleState Value, long Version)> ReadAsync(string key) @@ -53,19 +53,23 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules public async Task> QueryRuleIdsAsync(Guid appId) { var ruleEntities = - await Collection.Find(x => x.State.AppId == appId && !x.State.IsDeleted).Only(x => x.Id) + await Collection.Find(x => x.AppId == appId && !x.IsDeleted).Only(x => x.Id) .ToListAsync(); - return ruleEntities.Select(x => Guid.Parse(x.Id)).ToList(); + return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) { try { + value.Version = newVersion; + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) + .Set(x => x.AppId, value.AppId) + .Set(x => x.IsDeleted, value.IsDeleted) .Set(x => x.Version, newVersion), Upsert); } @@ -79,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules if (existingVersion != null) { - throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + throw new InconsistentStateException(existingVersion["Version"].AsInt64, oldVersion, ex); } } else diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs index 541575cef..3efb1a83f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -6,9 +6,11 @@ // All rights reserved. // ========================================================================== +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 { @@ -21,10 +23,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas [BsonElement] [BsonRequired] - public SchemaState State { get; set; } + public string Name { get; set; } [BsonElement] [BsonRequired] public int Version { get; set; } + + [BsonElement] + [BsonRequired] + public Guid AppId { get; set; } + + [BsonJson] + [BsonElement] + [BsonRequired] + public SchemaState State { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index b1c14d9e9..e7174a83f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -32,8 +32,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas protected override async Task SetupCollectionAsync(IMongoCollection collection) { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.Name)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); } public async Task<(SchemaState Value, long Version)> ReadAsync(string key) @@ -53,10 +53,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas public async Task FindSchemaIdAsync(Guid appId, string name) { var schemaEntity = - await Collection.Find(x => x.State.Name == name).Only(x => x.Id) + await Collection.Find(x => x.Name == name).Only(x => x.Id) .FirstOrDefaultAsync(); - return schemaEntity != null ? Guid.Parse(schemaEntity.Id) : Guid.Empty; + return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty; } public async Task> QuerySchemaIdsAsync(Guid appId) @@ -65,16 +65,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id) .ToListAsync(); - return schemaEntities.Select(x => Guid.Parse(x.Id)).ToList(); + return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion) { try { + value.Version = newVersion; + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) + .Set(x => x.AppId, value.AppId) + .Set(x => x.Name, value.Name) .Set(x => x.Version, newVersion), Upsert); } @@ -88,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas if (existingVersion != null) { - throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + throw new InconsistentStateException(existingVersion["Version"].AsInt64, oldVersion, ex); } } else diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 6d494f377..dd9fc35ee 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -24,8 +24,6 @@ namespace Squidex.Domain.Apps.Entities { public sealed class AppProvider : IAppProvider { - private readonly ConcurrentDictionary appIds = new ConcurrentDictionary(); - private readonly ConcurrentDictionary, Guid> schemaIds = new ConcurrentDictionary, Guid>(); private readonly IAppRepository appRepository; private readonly IRuleRepository ruleRepository; private readonly ISchemaRepository schemaRepository; @@ -52,33 +50,23 @@ namespace Squidex.Domain.Apps.Entities { var app = await stateFactory.GetSingleAsync(appId.ToString()); - if (app.Version < 0) + if (IsNotFound(app)) { - throw new DomainObjectNotFoundException(appId.ToString(), typeof(SchemaDomainObject)); + return (null, null); } var schema = await stateFactory.GetSingleAsync(id.ToString()); - if (schema.Version < 0 || schema.State.IsDeleted) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject)); - } - - return (app.State, schema.State); + return IsNotFound(false, schema) ? (null, null) : (app.State, schema.State); } public async Task GetAppAsync(string appName) { var appId = await GetAppIdAsync(appName); - var app = await stateFactory.GetSingleAsync(appName); - - if (app.Version < 0) - { - throw new DomainObjectNotFoundException(appName, typeof(SchemaDomainObject)); - } + var app = await stateFactory.GetSingleAsync(appId.ToString()); - return app.State; + return IsNotFound(app) ? null : app.State; } public async Task GetSchemaAsync(Guid appId, string name, bool provideDeleted = false) @@ -87,24 +75,14 @@ namespace Squidex.Domain.Apps.Entities var schema = await stateFactory.GetSingleAsync(schemaId.ToString()); - if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted)) - { - throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(SchemaDomainObject)); - } - - return schema.State; + return IsNotFound(provideDeleted, schema) ? null : schema.State; } public async Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false) { var schema = await stateFactory.GetSingleAsync(id.ToString()); - if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted)) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject)); - } - - return schema.State; + return IsNotFound(provideDeleted, schema) ? null : schema.State; } public async Task> GetSchemasAsync(Guid appId) @@ -140,32 +118,24 @@ namespace Squidex.Domain.Apps.Entities return apps.Select(a => (IAppEntity)a.State).ToList(); } - private async Task GetAppIdAsync(string name) + private Task GetAppIdAsync(string name) { - var key = name; - - if (!appIds.TryGetValue(key, out var id)) - { - id = await appRepository.FindAppIdByNameAsync(name); - - appIds[key] = id; - } - - return id; + return appRepository.FindAppIdByNameAsync(name); } - private async Task GetSchemaIdAsync(Guid appId, string name) + private Task GetSchemaIdAsync(Guid appId, string name) { - var key = Tuple.Create(appId, name); - - if (!schemaIds.TryGetValue(key, out var id)) - { - id = await schemaRepository.FindSchemaIdAsync(appId, name); + return schemaRepository.FindSchemaIdAsync(appId, name); + } - schemaIds[key] = id; - } + private static bool IsNotFound(AppDomainObject app) + { + return app.Version < 0; + } - return id; + private static bool IsNotFound(bool provideDeleted, SchemaDomainObject schema) + { + return schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted); } } } diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index 3e6243a34..77c711a05 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -23,7 +23,6 @@ namespace Squidex.Domain.Apps.Entities SetCreatedBy(entity, @event); SetLastModified(entity, headers); SetLastModifiedBy(entity, @event); - SetVersion(entity, headers); updater?.Invoke(entity); @@ -38,14 +37,6 @@ namespace Squidex.Domain.Apps.Entities } } - private static void SetVersion(IEntity entity, EnvelopeHeaders headers) - { - if (entity is IUpdateableEntityWithVersion withVersion) - { - withVersion.Version = headers.EventStreamNumber(); - } - } - private static void SetCreated(IEntity entity, EnvelopeHeaders headers) { if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 6228da562..8a8d068bf 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -9,6 +9,7 @@ using System; using System.Linq.Expressions; using System.Threading.Tasks; +using MongoDB.Bson; using MongoDB.Driver; namespace Squidex.Infrastructure.MongoDb @@ -34,25 +35,25 @@ namespace Squidex.Infrastructure.MongoDb return true; } - public static IFindFluent Only(this IFindFluent find, + public static IFindFluent Only(this IFindFluent find, Expression> include) { - return find.Project(Builders.Projection.Include(include)); + return find.Project(Builders.Projection.Include(include)); } - public static IFindFluent Only(this IFindFluent find, + public static IFindFluent Only(this IFindFluent find, Expression> include1, Expression> include2) { - return find.Project(Builders.Projection.Include(include1).Include(include2)); + return find.Project(Builders.Projection.Include(include1).Include(include2)); } - public static IFindFluent Only(this IFindFluent find, + public static IFindFluent Only(this IFindFluent find, Expression> include1, Expression> include2, Expression> include3) { - return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); + return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 393c41e6c..68f354dde 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.States if (existingVersion != null) { - throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + throw new InconsistentStateException(existingVersion["Version"].AsInt64, oldVersion, ex); } } else diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index ba32106a4..530f22591 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -69,6 +69,8 @@ namespace Squidex.Infrastructure.Commands var domainObjectId = domainObjectCommand.AggregateId; var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); + await handler(domainObject); + await domainObject.WriteAsync(log); if (!context.IsCompleted) diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index d3cee9702..96c96fa00 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -18,12 +18,13 @@ namespace Squidex.Infrastructure.Commands public abstract class DomainObjectBase : IDomainObject where TState : new() { private readonly List> uncomittedEvents = new List>(); + private Guid id; private TState state = new TState(); private IPersistence persistence; public long Version { - get { return persistence.Version; } + get { return persistence?.Version ?? -1; } } public TState State @@ -43,6 +44,8 @@ namespace Squidex.Infrastructure.Commands public Task ActivateAsync(string key, IStore store) { + id = Guid.Parse(key); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(); @@ -57,6 +60,8 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(@event, nameof(@event)); + @event.SetAggregateId(id); + OnRaised(@event.To()); uncomittedEvents.Add(@event.To()); @@ -73,19 +78,24 @@ namespace Squidex.Infrastructure.Commands public async Task WriteAsync(ISemanticLog log) { - await persistence.WriteSnapshotAsync(state); + var newVersion = Version + uncomittedEvents.Count; - try - { - await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); - } - catch (Exception ex) - { - log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); - } - finally + if (newVersion != Version) { - uncomittedEvents.Clear(); + await persistence.WriteSnapshotAsync(state, newVersion); + + try + { + await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + } + catch (Exception ex) + { + log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); + } + finally + { + uncomittedEvents.Clear(); + } } } } diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 51b17312c..25787a561 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; @@ -15,9 +16,9 @@ namespace Squidex.Infrastructure.States { long Version { get; } - Task WriteEventsAsync(params Envelope[] @events); + Task WriteEventsAsync(IEnumerable> @events); - Task WriteSnapshotAsync(TState state); + Task WriteSnapshotAsync(TState state, long newVersion = -1); Task ReadAsync(long? expectedVersion = null); } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 5be140a1c..5e91a3905 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; @@ -55,7 +56,7 @@ namespace Squidex.Infrastructure.States positionSnapshot = -1; positionEvent = -1; - if (snapshotStore != null) + if (applyState != null) { var (state, position) = await snapshotStore.ReadAsync(ownerKey); @@ -68,7 +69,7 @@ namespace Squidex.Infrastructure.States } } - if (eventStore != null && streamNameResolver != null) + if (applyEvent != null && streamNameResolver != null) { var events = await eventStore.GetEventsAsync(GetStreamName(), positionEvent + 1); @@ -105,51 +106,56 @@ namespace Squidex.Infrastructure.States } } - public async Task WriteSnapshotAsync(TState state) + public async Task WriteSnapshotAsync(TState state, long newVersion = -1) { - var newPosition = - eventStore != null ? - positionEvent : - positionSnapshot + 1; + if (newVersion < 0) + { + newVersion = + applyEvent != null ? + positionEvent : + positionSnapshot + 1; + } - if (newPosition != positionSnapshot) + if (newVersion != positionSnapshot) { try { - await snapshotStore.WriteAsync(ownerKey, state, positionSnapshot, newPosition); + await snapshotStore.WriteAsync(ownerKey, state, positionSnapshot, newVersion); } catch (InconsistentStateException ex) { throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); } - positionSnapshot = newPosition; + positionSnapshot = newVersion; } invalidate?.Invoke(); } - public async Task WriteEventsAsync(params Envelope[] @events) + public async Task WriteEventsAsync(IEnumerable> events) { Guard.NotNull(events, nameof(@events)); - if (@events.Length > 0) + var eventArray = events.ToArray(); + + if (eventArray.Length > 0) { var commitId = Guid.NewGuid(); var eventStream = GetStreamName(); - var eventData = GetEventData(events, commitId); + var eventData = GetEventData(eventArray, commitId); try { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), positionEvent, eventData); + await eventStore.AppendEventsAsync(commitId, GetStreamName(), Version, eventData); } catch (WrongEventVersionException ex) { throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); } - positionEvent += events.Length; + positionEvent += eventArray.Length; } invalidate?.Invoke(); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index a66dd3078..06a5257c7 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -31,6 +31,7 @@ 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; @@ -100,19 +101,20 @@ namespace Squidex.Config.Domain .As>() .As(); - services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) - .As() - .As>() - .As(); - services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase)) .As() .As>() - .As(); + .As(); services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) .As() - .As>() + .As>() + .As(); + + services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) + .As() + .As>() + .As() .As(); services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index e95fca91e..3285fed20 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.Commands.TestHelpers; @@ -32,32 +33,40 @@ namespace Squidex.Infrastructure.Commands private readonly MyDomainObject domainObject = new MyDomainObject(); private readonly AggregateHandler sut; + public sealed class MyEvent : IEvent + { + } + public AggregateHandlerTests() { context = new CommandContext(new MyCommand { AggregateId = domainObjectId }); - A.CallTo(() => store.WithEventSourcing(domainObjectId.ToString(), A, Task>>.Ignored)) + A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) .Returns(persistence); A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) .Returns(Task.FromResult(domainObject)); sut = new AggregateHandler(stateFactory, serviceProvider, log); + + domainObject.ActivateAsync(domainObjectId.ToString(), store).Wait(); } [Fact] - public Task Create_async_should_throw_exception_if_not_aggregate_command() + public Task Create_with_task_should_throw_exception_if_not_aggregate_command() { return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); } [Fact] - public async Task Create_async_should_create_domain_object_and_save() + public async Task Create_with_task_should_create_domain_object_and_save() { MyDomainObject passedDomainObject = null; await sut.CreateAsync(context, async x => { + x.RaiseEvent(new MyEvent()); + await Task.Yield(); passedDomainObject = x; @@ -66,46 +75,44 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result>()); - A.CallTo(() => persistence.ReadAsync(-1)) - .MustHaveHappened(); - - A.CallTo(() => persistence.WriteEventsAsync(A[]>.Ignored)) + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .MustHaveHappened(); } [Fact] - public async Task Create_sync_should_create_domain_object_and_save() + public async Task Create_should_create_domain_object_and_save() { MyDomainObject passedDomainObject = null; await sut.CreateAsync(context, x => { + x.RaiseEvent(new MyEvent()); + passedDomainObject = x; }); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result>()); - A.CallTo(() => persistence.ReadAsync(-1)) - .MustHaveHappened(); - - A.CallTo(() => persistence.WriteEventsAsync(A[]>.Ignored)) + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .MustHaveHappened(); } [Fact] - public Task Update_async_should_throw_exception_if_not_aggregate_command() + public Task Update_with_task_should_throw_exception_if_not_aggregate_command() { return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); } [Fact] - public async Task Update_async_should_create_domain_object_and_save() + public async Task Update_with_task_should_create_domain_object_and_save() { MyDomainObject passedDomainObject = null; await sut.UpdateAsync(context, async x => { + x.RaiseEvent(new MyEvent()); + await Task.Yield(); passedDomainObject = x; @@ -114,30 +121,26 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result()); - A.CallTo(() => persistence.ReadAsync(null)) - .MustHaveHappened(); - - A.CallTo(() => persistence.WriteEventsAsync(A[]>.Ignored)) + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .MustHaveHappened(); } [Fact] - public async Task Update_sync_should_create_domain_object_and_save() + public async Task Update_should_create_domain_object_and_save() { MyDomainObject passedDomainObject = null; await sut.UpdateAsync(context, x => { + x.RaiseEvent(new MyEvent()); + passedDomainObject = x; }); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result()); - A.CallTo(() => persistence.ReadAsync(null)) - .MustHaveHappened(); - - A.CallTo(() => persistence.WriteEventsAsync(A[]>.Ignored)) + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .MustHaveHappened(); } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 903a2837a..661d2f8e8 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -7,60 +7,115 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; using Xunit; namespace Squidex.Infrastructure.Commands { public class DomainObjectBaseTests { + private readonly IStore store = A.Fake(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyDomainObject sut = new MyDomainObject(); + + public DomainObjectBaseTests() + { + A.CallTo(() => store.WithSnapshots(id.ToString(), A>.Ignored)) + .Returns(persistence); + } + [Fact] public void Should_instantiate() { - var domainObjectId = Guid.NewGuid(); - var domainObjectVersion = 123; + Assert.Equal(-1, sut.Version); + } - var sut = new MyDomainObject(); + [Fact] + public void Should_add_event_to_uncommitted_events_and_not_increase_version_when_raised() + { + var event1 = new MyEvent(); + var event2 = new MyEvent(); + + sut.RaiseEvent(event1); + sut.RaiseEvent(event2); + + Assert.Equal(-1, sut.Version); + Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); - Assert.Equal(domainObjectId, sut.Id); - Assert.Equal(domainObjectVersion, sut.Version); + sut.ClearUncommittedEvents(); + + Assert.Equal(0, sut.GetUncomittedEvents().Count); } [Fact] - public void Should_add_event_to_uncommitted_events_and_increase_version_when_raised() + public async Task Should_write_state_and_events_when_saved() { + A.CallTo(() => persistence.Version) + .Returns(100); + + await sut.ActivateAsync(id.ToString(), store); + + Assert.Equal(100, sut.Version); + var event1 = new MyEvent(); var event2 = new MyEvent(); - var sut = new MyDomainObject(); + sut.RaiseEvent(event1); + sut.RaiseEvent(event2); - sut.RaiseNewEvent(event1); - sut.RaiseNewEvent(event2); + var newState = "STATE"; - Assert.Equal(12, sut.Version); + sut.UpdateState(newState); - Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); + await sut.WriteAsync(A.Fake()); - sut.ClearUncommittedEvents(); + A.CallTo(() => persistence.WriteSnapshotAsync(newState, 102)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) + .MustHaveHappened(); - Assert.Equal(0, sut.GetUncomittedEvents().Count); + Assert.Empty(sut.GetUncomittedEvents()); } [Fact] - public void Should_not_add_event_to_uncommitted_events_and_increase_version_when_raised() + public async Task Should_ignore_exception_when_saving() { + A.CallTo(() => persistence.Version) + .Returns(100); + + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .Throws(new InvalidOperationException()); + + await sut.ActivateAsync(id.ToString(), store); + + Assert.Equal(100, sut.Version); + var event1 = new MyEvent(); var event2 = new MyEvent(); - var sut = new MyDomainObject(); + sut.RaiseEvent(event1); + sut.RaiseEvent(event2); - sut.RaiseEvent(new Envelope(event1)); - sut.RaiseEvent(new Envelope(event2)); + var newState = "STATE"; - Assert.Equal(12, sut.Version); - Assert.Equal(0, sut.GetUncomittedEvents().Count); + sut.UpdateState(newState); + + await sut.WriteAsync(A.Fake()); + + A.CallTo(() => persistence.WriteSnapshotAsync(newState, 102)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) + .MustHaveHappened(); + + Assert.Empty(sut.GetUncomittedEvents()); } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs index c1eeb3c4b..a725a7a13 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs @@ -6,25 +6,11 @@ // All rights reserved. // ========================================================================== -using System; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.Commands.TestHelpers { internal sealed class MyDomainObject : DomainObjectBase { - public MyDomainObject RaiseNewEvent(IEvent @event) - { - RaiseEvent(@event); - - return this; - } - - public MyDomainObject RaiseNewEvent(Envelope @event) - { - RaiseEvent(@event); - - return this; - } } } diff --git a/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs b/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs index 0eee5acf1..bd8a5a8b4 100644 --- a/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs @@ -208,7 +208,7 @@ namespace Squidex.Infrastructure } [Fact] - public async Task Should_invoke_correct_event_asynchronously() + public async Task Should_invoke_correct_event_with_taskhronously() { var consumer = new MyAsyncConsumer(); @@ -222,7 +222,7 @@ namespace Squidex.Infrastructure } [Fact] - public async Task Should_invoke_correct_event_with_context_asynchronously() + public async Task Should_invoke_correct_event_with_context_with_taskhronously() { var consumer = new MyAsyncConsumer(); @@ -264,7 +264,7 @@ namespace Squidex.Infrastructure } [Fact] - public async Task Should_invoke_correct_event_and_return_synchronously() + public async Task Should_invoke_correct_event_and_returnhronously() { var consumer = new MyAsyncFuncConsumer(); @@ -278,7 +278,7 @@ namespace Squidex.Infrastructure } [Fact] - public async Task Should_invoke_correct_event_with_context_and_return_synchronously() + public async Task Should_invoke_correct_event_with_context_and_returnhronously() { var consumer = new MyAsyncFuncConsumer(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index b5537df60..1915c6a08 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -70,8 +70,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => persistence.ReadAsync(null)) .Invokes(new Action(s => apply(state))); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) - .Invokes(new Action(s => state = s)); + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + .Invokes(new Action((s, v) => state = s)); A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); @@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -150,7 +150,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Twice); A.CallTo(() => eventConsumer.ClearAsync()) @@ -180,7 +180,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventConsumer.On(envelope)) @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventConsumer.On(envelope)) @@ -243,7 +243,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustNotHaveHappened(); } @@ -263,7 +263,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -292,7 +292,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -323,7 +323,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustNotHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -354,7 +354,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) .MustHaveHappened(Repeated.Exactly.Twice); A.CallTo(() => eventSubscription.StopAsync()) diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index b23a80a3b..cb1d48e71 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -52,13 +52,13 @@ namespace Squidex.Infrastructure.States private class MyStatefulObjectWithSnapshot : IStatefulObject { - private IPersistence persistence; + private IPersistence persistence; public long? ExpectedVersion { get; set; } public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); + persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); return persistence.ReadAsync(ExpectedVersion); } @@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.States .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot))) .Returns(statefulObjectWithSnapShot); - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key)) @@ -278,7 +278,7 @@ namespace Squidex.Infrastructure.States statefulObject.ExpectedVersion = null; A.CallTo(() => snapshotStore.ReadAsync(key)) - .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); + .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); var tasks = new List>(); diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs b/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs index 5e110745c..25f0d5d7f 100644 --- a/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs @@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Tasks private readonly SingleThreadedDispatcher sut = new SingleThreadedDispatcher(); [Fact] - public async Task Should_handle_async_messages_sequentially() + public async Task Should_handle_with_task_messages_sequentially() { var source = Enumerable.Range(1, 100); var target = new List(); @@ -39,7 +39,7 @@ namespace Squidex.Infrastructure.Tasks } [Fact] - public async Task Should_handle_sync_messages_sequentially() + public async Task Should_handle_messages_sequentially() { var source = Enumerable.Range(1, 100); var target = new List(); From 84fb38138061871adc015572aefbac4f074b8fbc Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 00:53:20 +0100 Subject: [PATCH 10/30] Synced calls --- .../Apps/AppCommandMiddleware.cs | 20 ++++++------- .../Assets/AssetCommandMiddleware.cs | 8 ++--- .../Rules/RuleCommandMiddleware.cs | 10 +++---- .../Schemas/SchemaCommandMiddleware.cs | 30 +++++++++---------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 584471e4d..532d13b0b 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(CreateApp command, CommandContext context) { - return handler.CreateAsync(context, async a => + return handler.CreateSyncedAsync(context, async a => { await GuardApp.CanCreate(command, appProvider); @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AssignContributor command, CommandContext context) { - return handler.UpdateAsync(context, async a => + return handler.handler.UpdateSyncedAsync(context, async a => { await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan?.PlanId)); @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RemoveContributor command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppContributors.CanRemove(a.State.Contributors, command); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AttachClient command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanAttach(a.State.Clients, command); @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(UpdateClient command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanUpdate(a.State.Clients, command); @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RevokeClient command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanRevoke(a.State.Clients, command); @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AddLanguage command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanAdd(a.State.LanguagesConfig, command); @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RemoveLanguage command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanRemove(a.State.LanguagesConfig, command); @@ -130,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(UpdateLanguage command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanUpdate(a.State.LanguagesConfig, command); @@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(ChangePlan command, CommandContext context) { - return handler.UpdateAsync(context, async a => + return handler.handler.UpdateSyncedAsync(context, async a => { GuardApp.CanChangePlan(command, a.State.Plan, appPlansProvider); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 5f1fe9459..9a9a4ff52 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); try { - var asset = await handler.CreateAsync(context, async a => + var asset = await handler.CreateSyncedAsync(context, async a => { GuardAsset.CanCreate(command); @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets try { - var asset = await handler.UpdateAsync(context, async a => + var asset = await handler.handler.UpdateSyncedAsync(context, async a => { GuardAsset.CanUpdate(command); @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets protected Task On(RenameAsset command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAsset.CanRename(command, a.State.FileName); @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Assets protected Task On(DeleteAsset command, CommandContext context) { - return handler.UpdateAsync(context, a => + return handler.handler.UpdateSyncedAsync(context, a => { GuardAsset.CanDelete(command); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 8fe0e34f7..140c5f27a 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(CreateRule command, CommandContext context) { - return handler.CreateAsync(context, async w => + return handler.CreateSyncedAsync(context, async w => { await GuardRule.CanCreate(command, appProvider); @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(UpdateRule command, CommandContext context) { - return handler.UpdateAsync(context, async c => + return handler.handler.UpdateSyncedAsync(context, async c => { await GuardRule.CanUpdate(command, appProvider); @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(EnableRule command, CommandContext context) { - return handler.UpdateAsync(context, r => + return handler.handler.UpdateSyncedAsync(context, r => { GuardRule.CanEnable(command, r.State.RuleDef); @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(DisableRule command, CommandContext context) { - return handler.UpdateAsync(context, r => + return handler.handler.UpdateSyncedAsync(context, r => { GuardRule.CanDisable(command, r.State.RuleDef); @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(DeleteRule command, CommandContext context) { - return handler.UpdateAsync(context, c => + return handler.handler.UpdateSyncedAsync(context, c => { GuardRule.CanDelete(command); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs index f22b1a599..5c2904f56 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(CreateSchema command, CommandContext context) { - return handler.CreateAsync(context, async s => + return handler.CreateSyncedAsync(context, async s => { await GuardSchema.CanCreate(command, appProvider); @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(AddField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanAdd(s.State.SchemaDef, command); @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DeleteField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanDelete(s.State.SchemaDef, command); @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(LockField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanLock(s.State.SchemaDef, command); @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(HideField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanHide(s.State.SchemaDef, command); @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ShowField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanShow(s.State.SchemaDef, command); @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DisableField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanDisable(s.State.SchemaDef, command); @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(EnableField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanEnable(s.State.SchemaDef, command); @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UpdateField command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanUpdate(s.State.SchemaDef, command); @@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ReorderFields command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchema.CanReorder(s.State.SchemaDef, command); @@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UpdateSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchema.CanUpdate(s.State.SchemaDef, command); @@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(PublishSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchema.CanPublish(s.State.SchemaDef, command); @@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UnpublishSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchema.CanUnpublish(s.State.SchemaDef, command); @@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ConfigureScripts command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchema.CanConfigureScripts(s.State.SchemaDef, command); @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DeleteSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => + return handler.handler.UpdateSyncedAsync(context, s => { GuardSchema.CanDelete(s.State.SchemaDef, command); From 0c32083671b4994e8aeb6736eef51ea983d71403 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 01:16:11 +0100 Subject: [PATCH 11/30] Tests fixed in infrastructure. --- .../Commands/AggregateHandler.cs | 2 + src/Squidex.Infrastructure/Language.cs | 6 +- .../Commands/AggregateHandlerTests.cs | 95 +++++++++++++++++++ .../Grains/EventConsumerGrainTests.cs | 21 ++++ .../LanguageTests.cs | 12 +++ .../Reflection/SimpleCopierTests.cs | 14 ++- .../States/StateSnapshotTests.cs | 37 ++++++-- 7 files changed, 173 insertions(+), 14 deletions(-) diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index 530f22591..b30298026 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -99,6 +99,8 @@ namespace Squidex.Infrastructure.Commands { var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + await handler(domainObject); + await domainObject.WriteAsync(log); if (!context.IsCompleted) diff --git a/src/Squidex.Infrastructure/Language.cs b/src/Squidex.Infrastructure/Language.cs index aedd0f070..f392a2f5c 100644 --- a/src/Squidex.Infrastructure/Language.cs +++ b/src/Squidex.Infrastructure/Language.cs @@ -14,10 +14,10 @@ namespace Squidex.Infrastructure { public sealed partial class Language { - private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$"); + private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); + private static readonly Dictionary AllLanguagesField = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly string iso2Code; private readonly string englishName; - private static readonly Dictionary AllLanguagesField = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Language AddLanguage(string iso2Code, string englishName) { @@ -106,7 +106,7 @@ namespace Squidex.Infrastructure return null; } - input = match.Groups[0].Value; + input = match.Groups[1].Value; } if (TryGetLanguage(input.ToLowerInvariant(), out var result)) diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index 3285fed20..1a020997d 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -47,6 +47,9 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) .Returns(Task.FromResult(domainObject)); + A.CallTo(() => stateFactory.GetSingleAsync(domainObjectId.ToString())) + .Returns(Task.FromResult(domainObject)); + sut = new AggregateHandler(stateFactory, serviceProvider, log); domainObject.ActivateAsync(domainObjectId.ToString(), store).Wait(); @@ -58,6 +61,12 @@ namespace Squidex.Infrastructure.Commands return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); } + [Fact] + public Task Create_synced_with_task_should_throw_exception_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + } + [Fact] public async Task Create_with_task_should_create_domain_object_and_save() { @@ -79,6 +88,27 @@ namespace Squidex.Infrastructure.Commands .MustHaveHappened(); } + [Fact] + public async Task Create_synced_with_task_should_create_domain_object_and_save() + { + MyDomainObject passedDomainObject = null; + + await sut.CreateSyncedAsync(context, async x => + { + x.RaiseEvent(new MyEvent()); + + await Task.Yield(); + + passedDomainObject = x; + }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .MustHaveHappened(); + } + [Fact] public async Task Create_should_create_domain_object_and_save() { @@ -98,12 +128,37 @@ namespace Squidex.Infrastructure.Commands .MustHaveHappened(); } + [Fact] + public async Task Create_synced_should_create_domain_object_and_save() + { + MyDomainObject passedDomainObject = null; + + await sut.CreateSyncedAsync(context, x => + { + x.RaiseEvent(new MyEvent()); + + passedDomainObject = x; + }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .MustHaveHappened(); + } + [Fact] public Task Update_with_task_should_throw_exception_if_not_aggregate_command() { return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); } + [Fact] + public Task Update_synced_with_task_should_throw_exception_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + } + [Fact] public async Task Update_with_task_should_create_domain_object_and_save() { @@ -125,6 +180,27 @@ namespace Squidex.Infrastructure.Commands .MustHaveHappened(); } + [Fact] + public async Task Update_synced_with_task_should_create_domain_object_and_save() + { + MyDomainObject passedDomainObject = null; + + await sut.UpdateSyncedAsync(context, async x => + { + x.RaiseEvent(new MyEvent()); + + await Task.Yield(); + + passedDomainObject = x; + }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); + + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .MustHaveHappened(); + } + [Fact] public async Task Update_should_create_domain_object_and_save() { @@ -143,5 +219,24 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .MustHaveHappened(); } + + [Fact] + public async Task Update_synced_should_create_domain_object_and_save() + { + MyDomainObject passedDomainObject = null; + + await sut.UpdateSyncedAsync(context, x => + { + x.RaiseEvent(new MyEvent()); + + passedDomainObject = x; + }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); + + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .MustHaveHappened(); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 1915c6a08..4a081616d 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -229,6 +229,27 @@ namespace Squidex.Infrastructure.EventSourcing.Grains .MustNotHaveHappened(); } + [Fact] + public async Task Should_stop_if_consumer_failed() + { + sut.ActivateAsync(consumerName, store).Wait(); + sut.Activate(eventConsumer); + + var ex = new InvalidOperationException(); + + await OnErrorAsync(eventSubscription, ex); + + sut.Dispose(); + + state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + .MustHaveHappened(Repeated.Exactly.Once); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(Repeated.Exactly.Once); + } + [Fact] public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() { diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index c4b900f36..98e95a985 100644 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -96,6 +96,18 @@ namespace Squidex.Infrastructure Assert.Equal(language, Language.GetLanguage(languageCode)); } + [Theory] + [InlineData("en-US", "en")] + [InlineData("en-GB", "en")] + [InlineData("EN-US", "en")] + [InlineData("EN-GB", "en")] + public void Should_parse_lanuages_from_culture(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + [Theory] [InlineData("")] [InlineData(" ")] diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs index f6a59a82c..90f86cf37 100644 --- a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs @@ -37,13 +37,24 @@ namespace Squidex.Infrastructure.Reflection { public int Value2 { get; set; } + public int ValueReadOnly { get; } + public Cloneable Cloneable { get; set; } + + public MyClass1() + { + } + + public MyClass1(int readValue) + { + ValueReadOnly = readValue; + } } [Fact] public void Should_copy_class() { - var value = new MyClass1 + var value = new MyClass1(100) { Value1 = 1, Value2 = 2, @@ -54,6 +65,7 @@ namespace Squidex.Infrastructure.Reflection Assert.Equal(value.Value1, copy.Value1); Assert.Equal(value.Value2, copy.Value2); + Assert.Equal(0, copy.ValueReadOnly); Assert.Equal(value.Cloneable.Value, copy.Cloneable.Value); Assert.NotSame(value.Cloneable, copy.Cloneable); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index bb45d0cac..cbca0905b 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -43,9 +43,9 @@ namespace Squidex.Infrastructure.States state = value; } - public Task WriteStateAsync() + public Task WriteStateAsync(long newVersion = -1) { - return persistence.WriteSnapshotAsync(state); + return persistence.WriteSnapshotAsync(state, newVersion); } } @@ -173,8 +173,6 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var version = 1; - InvalidateMessage message = null; pubSub.Subscribe(m => @@ -183,7 +181,7 @@ namespace Squidex.Infrastructure.States }); A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, version)); + .Returns((123, 13)); var actualObject = await sut.GetSingleAsync(key); @@ -194,7 +192,7 @@ namespace Squidex.Infrastructure.States await statefulObject.WriteStateAsync(); - A.CallTo(() => snapshotStore.WriteAsync(key, 456, version, 2)) + A.CallTo(() => snapshotStore.WriteAsync(key, 456, 13, 14)) .MustHaveHappened(); Assert.NotNull(message); @@ -202,16 +200,35 @@ namespace Squidex.Infrastructure.States } [Fact] - public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() + public async Task Should_write_to_store_with_explicit_version() { statefulObject.ExpectedVersion = null; - var version = 1; + A.CallTo(() => snapshotStore.ReadAsync(key)) + .Returns((123, 1)); + + var actualObject = await sut.GetSingleAsync(key); + + Assert.Same(statefulObject, actualObject); + Assert.Equal(123, statefulObject.State); + + statefulObject.SetState(456); + + await statefulObject.WriteStateAsync(100); + + A.CallTo(() => snapshotStore.WriteAsync(key, 456, 1, 100)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() + { + statefulObject.ExpectedVersion = null; A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, version)); + .Returns((123, 13)); - A.CallTo(() => snapshotStore.WriteAsync(key, 123, version, 2)) + A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); var actualObject = await sut.GetSingleAsync(key); From a826a0e3d6ddac79160cf32c027f5a4f5685da56 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 15:19:37 +0100 Subject: [PATCH 12/30] Expected version improved. --- .../DomainObjectState.cs | 3 +- .../EventSourcing/MongoEventStore.cs | 7 +- .../Commands/DomainObjectBase.cs | 27 ++-- .../EventSourcing/ExpectedVersion.cs | 17 +++ .../Grains/EventConsumerGrain.cs | 6 +- .../States/IPersistence.cs | 4 +- .../States/Persistence.cs | 117 ++++++++++++------ .../States/PersistenceMode.cs} | 10 +- src/Squidex.Infrastructure/States/Store.cs | 10 +- .../Commands/DomainObjectBaseTests.cs | 4 +- .../Grains/EventConsumerGrainTests.cs | 28 ++--- .../Reflection/SimpleCopierTests.cs | 1 + .../States/StateEventSourcingTests.cs | 24 ++-- .../States/StateSnapshotTests.cs | 45 ++----- 14 files changed, 168 insertions(+), 135 deletions(-) create mode 100644 src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs rename src/{Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs => Squidex.Infrastructure/States/PersistenceMode.cs} (66%) diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 0a7dade8a..2770d4e1a 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -20,8 +20,7 @@ namespace Squidex.Domain.Apps.Entities IEntityWithVersion, IUpdateableEntity, IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithVersion + IUpdateableEntityWithLastModifiedBy where T : Cloneable { [JsonProperty] diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 6dd770faf..0c2dba475 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -19,7 +19,6 @@ namespace Squidex.Infrastructure.EventSourcing { public class MongoEventStore : MongoRepositoryBase, IEventStore { - private const long AnyVersion = long.MinValue; private const int MaxAttempts = 20; private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); @@ -130,7 +129,7 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(commitId, streamName, AnyVersion, events); + return AppendEventsInternalAsync(commitId, streamName, ExpectedVersion.Any, events); } public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) @@ -152,7 +151,7 @@ namespace Squidex.Infrastructure.EventSourcing var currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != AnyVersion && expectedVersion != currentVersion) + if (expectedVersion != ExpectedVersion.Any && expectedVersion != currentVersion) { throw new WrongEventVersionException(currentVersion, expectedVersion); } @@ -175,7 +174,7 @@ namespace Squidex.Infrastructure.EventSourcing { currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != AnyVersion) + if (expectedVersion != ExpectedVersion.Any) { throw new WrongEventVersionException(currentVersion, expectedVersion); } diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 96c96fa00..32135d0fb 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -78,24 +78,19 @@ namespace Squidex.Infrastructure.Commands public async Task WriteAsync(ISemanticLog log) { - var newVersion = Version + uncomittedEvents.Count; + await persistence.WriteSnapshotAsync(state); - if (newVersion != Version) + try { - await persistence.WriteSnapshotAsync(state, newVersion); - - try - { - await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); - } - catch (Exception ex) - { - log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); - } - finally - { - uncomittedEvents.Clear(); - } + await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + } + catch (Exception ex) + { + log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); + } + finally + { + uncomittedEvents.Clear(); } } } diff --git a/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs b/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs new file mode 100644 index 000000000..3908d3cd2 --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ExpectedVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class ExpectedVersion + { + public const int Any = -2; + + public const int Empty = -1; + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index a40f15a66..eff3b2f72 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -68,17 +68,17 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public virtual void Stop() { - dispatcher.DispatchAsync(() => HandleStopAsync()).Forget(); + dispatcher.DispatchAsync(HandleStopAsync).Forget(); } public virtual void Start() { - dispatcher.DispatchAsync(() => HandleStartAsync()).Forget(); + dispatcher.DispatchAsync(HandleStartAsync).Forget(); } public virtual void Reset() { - dispatcher.DispatchAsync(() => HandleResetAsync()).Forget(); + dispatcher.DispatchAsync(HandleResetAsync).Forget(); } public virtual void Activate(IEventConsumer eventConsumer) diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 25787a561..3c9528178 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -18,8 +18,8 @@ namespace Squidex.Infrastructure.States Task WriteEventsAsync(IEnumerable> @events); - Task WriteSnapshotAsync(TState state, long newVersion = -1); + Task WriteSnapshotAsync(TState state); - Task ReadAsync(long? expectedVersion = null); + Task ReadAsync(long expectedVersion = ExpectedVersion.Any); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 5e91a3905..542320f75 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -21,15 +21,17 @@ namespace Squidex.Infrastructure.States private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; + private readonly PersistenceMode persistenceMode; private readonly Action invalidate; private readonly Func applyState; private readonly Func, Task> applyEvent; - private long positionSnapshot = -1; - private long positionEvent = -1; + private long versionSnapshot = -1; + private long versionEvents = -1; + private long version; public long Version { - get { return Math.Max(positionEvent, positionSnapshot); } + get { return version; } } public Persistence(string ownerKey, @@ -38,6 +40,7 @@ namespace Squidex.Infrastructure.States IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, + PersistenceMode persistenceMode, Func applyState, Func, Task> applyEvent) { @@ -47,37 +50,61 @@ namespace Squidex.Infrastructure.States this.invalidate = invalidate; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; + this.persistenceMode = persistenceMode; this.snapshotStore = snapshotStore; this.streamNameResolver = streamNameResolver; } - public async Task ReadAsync(long? expectedVersion) + public async Task ReadAsync(long expectedVersion = ExpectedVersion.Any) { - positionSnapshot = -1; - positionEvent = -1; + versionSnapshot = -1; + versionEvents = -1; - if (applyState != null) + await ReadSnapshotAsync(); + await ReadEventsAsync(); + + UpdateVersion(); + + if (expectedVersion != ExpectedVersion.Any && expectedVersion != version) + { + if (version == ExpectedVersion.Empty) + { + throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); + } + else + { + throw new DomainObjectVersionException(ownerKey, typeof(TOwner), version, expectedVersion); + } + } + } + + private async Task ReadSnapshotAsync() + { + if (UseSnapshots()) { var (state, position) = await snapshotStore.ReadAsync(ownerKey); - positionSnapshot = position; - positionEvent = position; + versionSnapshot = position; + versionEvents = position; if (applyState != null && position >= 0) { await applyState(state); } } + } - if (applyEvent != null && streamNameResolver != null) + private async Task ReadEventsAsync() + { + if (UseEventSourcing()) { - var events = await eventStore.GetEventsAsync(GetStreamName(), positionEvent + 1); + var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); foreach (var @event in events) { - positionEvent++; + versionEvents++; - if (@event.EventStreamNumber != positionEvent) + if (@event.EventStreamNumber != versionEvents) { throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); } @@ -90,46 +117,28 @@ namespace Squidex.Infrastructure.States } } } - - var newVersion = Version; - - if (expectedVersion.HasValue && expectedVersion.Value != newVersion) - { - if (newVersion == -1) - { - throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); - } - else - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), newVersion, expectedVersion.Value); - } - } } - public async Task WriteSnapshotAsync(TState state, long newVersion = -1) + public async Task WriteSnapshotAsync(TState state) { - if (newVersion < 0) - { - newVersion = - applyEvent != null ? - positionEvent : - positionSnapshot + 1; - } + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - if (newVersion != positionSnapshot) + if (newVersion != versionSnapshot) { try { - await snapshotStore.WriteAsync(ownerKey, state, positionSnapshot, newVersion); + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); } catch (InconsistentStateException ex) { throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); } - positionSnapshot = newVersion; + versionSnapshot = newVersion; } + UpdateVersion(); + invalidate?.Invoke(); } @@ -141,6 +150,8 @@ namespace Squidex.Infrastructure.States if (eventArray.Length > 0) { + var expectedVersion = UseEventSourcing() ? version : ExpectedVersion.Any; + var commitId = Guid.NewGuid(); var eventStream = GetStreamName(); @@ -155,9 +166,11 @@ namespace Squidex.Infrastructure.States throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); } - positionEvent += eventArray.Length; + versionEvents += eventArray.Length; } + UpdateVersion(); + invalidate?.Invoke(); } @@ -171,6 +184,16 @@ namespace Squidex.Infrastructure.States return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey); } + private bool UseSnapshots() + { + return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private bool UseEventSourcing() + { + return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + private Envelope ParseKnownEvent(StoredEvent storedEvent) { try @@ -182,5 +205,21 @@ namespace Squidex.Infrastructure.States return null; } } + + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; + } + else if (persistenceMode == PersistenceMode.EventSourcing) + { + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); + } + } } } diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs b/src/Squidex.Infrastructure/States/PersistenceMode.cs similarity index 66% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs rename to src/Squidex.Infrastructure/States/PersistenceMode.cs index 229f7ea2d..b1c09fa62 100644 --- a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs +++ b/src/Squidex.Infrastructure/States/PersistenceMode.cs @@ -1,15 +1,17 @@ // ========================================================================== -// IUpdateableEntityWithVersion.cs +// PersistenceMode.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Infrastructure.States { - public interface IUpdateableEntityWithVersion + public enum PersistenceMode { - long Version { get; set; } + EventSourcing, + Snapshots, + SnapshotsAndEventSourcing } } diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index b217dac38..578762dc5 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -36,26 +36,26 @@ namespace Squidex.Infrastructure.States public IPersistence WithEventSourcing(string key, Func, Task> applyEvent) { - return CreatePersistence(key, null, applyEvent); + return CreatePersistence(key, PersistenceMode.EventSourcing, null, applyEvent); } public IPersistence WithSnapshots(string key, Func applySnapshot) { - return CreatePersistence(key, applySnapshot, null); + return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); } public IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent) { - return CreatePersistence(key, applySnapshot, applyEvent); + return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } - private IPersistence CreatePersistence(string key, Func applySnapshot, Func, Task> applyEvent) + private IPersistence CreatePersistence(string key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) { Guard.NotNullOrEmpty(key, nameof(key)); var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applySnapshot, applyEvent); + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 661d2f8e8..aea30eb48 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -77,7 +77,7 @@ namespace Squidex.Infrastructure.Commands await sut.WriteAsync(A.Fake()); - A.CallTo(() => persistence.WriteSnapshotAsync(newState, 102)) + A.CallTo(() => persistence.WriteSnapshotAsync(newState)) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) .MustHaveHappened(); @@ -110,7 +110,7 @@ namespace Squidex.Infrastructure.Commands await sut.WriteAsync(A.Fake()); - A.CallTo(() => persistence.WriteSnapshotAsync(newState, 102)) + A.CallTo(() => persistence.WriteSnapshotAsync(newState)) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) .MustHaveHappened(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 4a081616d..85f873321 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -67,11 +67,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.Name) .Returns(consumerName); - A.CallTo(() => persistence.ReadAsync(null)) - .Invokes(new Action(s => apply(state))); + A.CallTo(() => persistence.ReadAsync(ExpectedVersion.Any)) + .Invokes(new Action(s => apply(state))); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) - .Invokes(new Action((s, v) => state = s)); + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Invokes(new Action(s => state = s)); A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); @@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -150,7 +150,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Twice); A.CallTo(() => eventConsumer.ClearAsync()) @@ -180,7 +180,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventConsumer.On(envelope)) @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventConsumer.On(envelope)) @@ -243,7 +243,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -264,7 +264,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustNotHaveHappened(); } @@ -284,7 +284,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -313,7 +313,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -344,7 +344,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustNotHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -375,7 +375,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Twice); A.CallTo(() => eventSubscription.StopAsync()) diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs index 90f86cf37..ba19dd2c7 100644 --- a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs @@ -65,6 +65,7 @@ namespace Squidex.Infrastructure.Reflection Assert.Equal(value.Value1, copy.Value1); Assert.Equal(value.Value2, copy.Value2); + Assert.Equal(0, copy.ValueReadOnly); Assert.Equal(value.Cloneable.Value, copy.Cloneable.Value); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index cb1d48e71..c7ae719ce 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.States private readonly List appliedEvents = new List(); private IPersistence persistence; - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public List AppliedEvents { @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.States { private IPersistence persistence; - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public Task ActivateAsync(string key, IStore store) { @@ -115,7 +115,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_read_events_from_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = null; + statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -131,7 +131,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = null; + statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -144,7 +144,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = null; + statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -177,7 +177,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(0); @@ -187,7 +187,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(0); @@ -200,7 +200,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(0); @@ -218,7 +218,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_position() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; InvalidateMessage message = null; @@ -248,7 +248,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(3); @@ -263,7 +263,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -275,7 +275,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index cbca0905b..b7d91bb0c 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -15,6 +15,8 @@ using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; using Xunit; +#pragma warning disable RECS0002 // Convert anonymous method to method group + namespace Squidex.Infrastructure.States { public class StateSnapshotTests : IDisposable @@ -24,7 +26,7 @@ namespace Squidex.Infrastructure.States private IPersistence persistence; private int state; - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public int State { @@ -43,9 +45,9 @@ namespace Squidex.Infrastructure.States state = value; } - public Task WriteStateAsync(long newVersion = -1) + public Task WriteStateAsync() { - return persistence.WriteSnapshotAsync(state, newVersion); + return persistence.WriteSnapshotAsync(state); } } @@ -117,7 +119,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, -1)); @@ -128,7 +130,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -139,7 +141,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject1 = await sut.GetSingleAsync(key); @@ -155,7 +157,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_serve_next_request_from_cache_when_detached() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject1 = await sut.CreateAsync(key); @@ -171,7 +173,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_version() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; InvalidateMessage message = null; @@ -199,31 +201,10 @@ namespace Squidex.Infrastructure.States Assert.Equal(key, message.Key); } - [Fact] - public async Task Should_write_to_store_with_explicit_version() - { - statefulObject.ExpectedVersion = null; - - A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, 1)); - - var actualObject = await sut.GetSingleAsync(key); - - Assert.Same(statefulObject, actualObject); - Assert.Equal(123, statefulObject.State); - - statefulObject.SetState(456); - - await statefulObject.WriteStateAsync(100); - - A.CallTo(() => snapshotStore.WriteAsync(key, 456, 1, 100)) - .MustHaveHappened(); - } - [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); @@ -239,7 +220,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -251,7 +232,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); From f4f5c357bea6d92c0f2aab746c12c4bd1f81ef62 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 18:35:34 +0100 Subject: [PATCH 13/30] A lot of small fixes. --- .../Apps/MongoAppRepository.cs | 5 +- .../Assets/MongoAssetRepository.cs | 5 +- .../Contents/MongoContentRepository.cs | 4 +- .../Rules/MongoRuleRepository.cs | 5 +- .../Schemas/MongoSchemaRepository.cs | 7 ++- .../Apps/AppCommandMiddleware.cs | 18 +++---- .../Assets/AssetCommandMiddleware.cs | 6 +-- .../Contents/ContentQueryService.cs | 2 +- .../Contents/IContentQueryService.cs | 3 +- .../DomainObjectState.cs | 2 + .../EntityMapper.cs | 11 +++- .../IUpdateableEntityWithVersion.cs} | 10 ++-- .../Rules/RuleCommandMiddleware.cs | 8 +-- .../Schemas/SchemaCommandMiddleware.cs | 28 +++++----- .../SquidexCommand.cs | 2 +- .../EventSourcing/Events/GetEventStore.cs | 2 +- .../EventSourcing/MongoEventStore.cs | 10 ++-- .../States/MongoSnapshotStore.cs | 2 +- .../Commands/AggregateHandler.cs | 18 +++++-- .../Commands/DomainObjectBase.cs | 41 ++++++++++----- .../Commands/ICommand.cs | 2 +- .../Commands/IDomainState.cs | 15 ++++++ src/Squidex.Infrastructure/EtagVersion.cs | 19 +++++++ .../EventSourcing/CommonHeaders.cs | 2 + .../EventSourcing/EnvelopeExtensions.cs | 14 ++++- .../States/IPersistence.cs | 2 +- .../States/Persistence.cs | 25 +++++---- .../Controllers/Apps/AppClientsController.cs | 3 +- .../ETagCommandMiddleware.cs | 5 ++ .../Contents/GraphQLTests.cs | 6 +-- .../Commands/AggregateHandlerTests.cs | 51 +++++++++++++++---- .../Commands/CommandContextTests.cs | 2 +- .../Commands/DomainObjectBaseTests.cs | 10 ++-- ...richWithTimestampCommandMiddlewareTests.cs | 2 +- .../CompoundEventConsumerTests.cs | 5 +- .../EventSourcing/EnvelopeExtensionsTests.cs | 13 ++++- .../EventSourcing/EventDataFormatterTests.cs | 6 +-- .../Grains/EventConsumerGrainTests.cs | 7 +-- .../PropertiesBagTests.cs | 14 ++--- .../States/StateEventSourcingTests.cs | 25 ++++----- .../States/StateSnapshotTests.cs | 20 ++++---- .../{Commands => }/TestHelpers/MyCommand.cs | 5 +- .../TestHelpers/MyDomainObject.cs | 6 +-- .../TestHelpers/MyDomainState.cs | 17 +++++++ .../{Commands => }/TestHelpers/MyEvent.cs | 3 +- 45 files changed, 302 insertions(+), 166 deletions(-) rename src/{Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs => Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs} (64%) create mode 100644 src/Squidex.Infrastructure/Commands/IDomainState.cs create mode 100644 src/Squidex.Infrastructure/EtagVersion.cs rename tests/Squidex.Infrastructure.Tests/{Commands => }/TestHelpers/MyCommand.cs (80%) rename tests/Squidex.Infrastructure.Tests/{Commands => }/TestHelpers/MyDomainObject.cs (78%) create mode 100644 tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs rename tests/Squidex.Infrastructure.Tests/{Commands => }/TestHelpers/MyEvent.cs (82%) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index f77ab41fa..1c65bb489 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -74,15 +75,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 36dc642cb..a470fe74b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -15,6 +15,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -53,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) @@ -116,8 +117,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index bac856632..720cab81b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -89,8 +89,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents try { - value.Version = newVersion; - await Collection.InsertOneAsync(document); } catch (MongoWriteException ex) @@ -126,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return (SimpleMapper.Map(existing, new ContentState()), existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 13f5f31b6..fe6a69c05 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -47,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task> QueryRuleIdsAsync(Guid appId) @@ -63,8 +64,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index e7174a83f..23ccea311 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -47,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task FindSchemaIdAsync(Guid appId, string name) @@ -62,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas public async Task> QuerySchemaIdsAsync(Guid appId) { var schemaEntities = - await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id) + await Collection.Find(x => x.AppId == appId).Only(x => x.Id) .ToListAsync(); return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); @@ -72,8 +73,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 532d13b0b..22ea628b2 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AssignContributor command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, async a => + return handler.UpdateSyncedAsync(context, async a => { await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan?.PlanId)); @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RemoveContributor command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppContributors.CanRemove(a.State.Contributors, command); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AttachClient command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanAttach(a.State.Clients, command); @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(UpdateClient command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanUpdate(a.State.Clients, command); @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RevokeClient command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanRevoke(a.State.Clients, command); @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AddLanguage command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanAdd(a.State.LanguagesConfig, command); @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RemoveLanguage command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanRemove(a.State.LanguagesConfig, command); @@ -130,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(UpdateLanguage command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanUpdate(a.State.LanguagesConfig, command); @@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(ChangePlan command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, async a => + return handler.UpdateSyncedAsync(context, async a => { GuardApp.CanChangePlan(command, a.State.Plan, appPlansProvider); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 9a9a4ff52..887c7acdb 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets try { - var asset = await handler.handler.UpdateSyncedAsync(context, async a => + var asset = await handler.UpdateSyncedAsync(context, async a => { GuardAsset.CanUpdate(command); @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets protected Task On(RenameAsset command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAsset.CanRename(command, a.State.FileName); @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Assets protected Task On(DeleteAsset command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAsset.CanDelete(command); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index d9144cafb..95f95ee31 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await FindSchemaAsync(app, schemaIdOrName); var content = - version > 0 ? + version > EtagVersion.Empty ? await contentRepository.FindContentAsync(app, schema, id, version) : await contentRepository.FindContentAsync(app, schema, id); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index a9a01d2df..8baaccd33 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -12,6 +12,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents { @@ -21,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); - Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1); + Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = EtagVersion.Any); Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); } diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 2770d4e1a..dc311eb6c 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -10,10 +10,12 @@ using System; using Newtonsoft.Json; using NodaTime; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, + IDomainState, IEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index 77c711a05..81bb15aa1 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -23,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities SetCreatedBy(entity, @event); SetLastModified(entity, headers); SetLastModifiedBy(entity, @event); + SetVersion(entity, headers); updater?.Invoke(entity); @@ -31,12 +32,20 @@ namespace Squidex.Domain.Apps.Entities private static void SetId(IEntity entity, EnvelopeHeaders headers) { - if (entity is IUpdateableEntity updateable) + if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty) { updateable.Id = headers.AggregateId(); } } + private static void SetVersion(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntityWithVersion updateable) + { + updateable.Version = headers.EventStreamNumber(); + } + } + private static void SetCreated(IEntity entity, EnvelopeHeaders headers) { if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) diff --git a/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs similarity index 64% rename from src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs rename to src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs index 3908d3cd2..229f7ea2d 100644 --- a/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs @@ -1,17 +1,15 @@ // ========================================================================== -// ExpectedVersion.cs +// IUpdateableEntityWithVersion.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Domain.Apps.Entities { - public static class ExpectedVersion + public interface IUpdateableEntityWithVersion { - public const int Any = -2; - - public const int Empty = -1; + long Version { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 140c5f27a..1217d9105 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(UpdateRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, async c => + return handler.UpdateSyncedAsync(context, async c => { await GuardRule.CanUpdate(command, appProvider); @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(EnableRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, r => + return handler.UpdateSyncedAsync(context, r => { GuardRule.CanEnable(command, r.State.RuleDef); @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(DisableRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, r => + return handler.UpdateSyncedAsync(context, r => { GuardRule.CanDisable(command, r.State.RuleDef); @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(DeleteRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, c => + return handler.UpdateSyncedAsync(context, c => { GuardRule.CanDelete(command); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs index 5c2904f56..87f0c4d4b 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(AddField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanAdd(s.State.SchemaDef, command); @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DeleteField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanDelete(s.State.SchemaDef, command); @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(LockField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanLock(s.State.SchemaDef, command); @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(HideField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanHide(s.State.SchemaDef, command); @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ShowField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanShow(s.State.SchemaDef, command); @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DisableField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanDisable(s.State.SchemaDef, command); @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(EnableField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanEnable(s.State.SchemaDef, command); @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UpdateField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanUpdate(s.State.SchemaDef, command); @@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ReorderFields command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanReorder(s.State.SchemaDef, command); @@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UpdateSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanUpdate(s.State.SchemaDef, command); @@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(PublishSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanPublish(s.State.SchemaDef, command); @@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UnpublishSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanUnpublish(s.State.SchemaDef, command); @@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ConfigureScripts command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanConfigureScripts(s.State.SchemaDef, command); @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DeleteSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanDelete(s.State.SchemaDef, command); diff --git a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs index ae4c6cb8d..c22d7861d 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs @@ -15,6 +15,6 @@ namespace Squidex.Domain.Apps.Entities { public RefToken Actor { get; set; } - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs index cd65c5e43..cd6659044 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs @@ -89,7 +89,7 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(streamName, ExpectedVersion.Any, events); + return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); } public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 0c2dba475..7eb6942d8 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -129,12 +129,12 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(commitId, streamName, ExpectedVersion.Any, events); + return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events); } public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) { - Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion)); + Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events); } @@ -151,7 +151,7 @@ namespace Squidex.Infrastructure.EventSourcing var currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != ExpectedVersion.Any && expectedVersion != currentVersion) + if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) { throw new WrongEventVersionException(currentVersion, expectedVersion); } @@ -174,7 +174,7 @@ namespace Squidex.Infrastructure.EventSourcing { currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != ExpectedVersion.Any) + if (expectedVersion != EtagVersion.Any) { throw new WrongEventVersionException(currentVersion, expectedVersion); } @@ -210,7 +210,7 @@ namespace Squidex.Infrastructure.EventSourcing return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); } - return -1; + return EtagVersion.Empty; } private static FilterDefinition CreateFilter(string streamFilter, StreamPosition streamPosition) diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 68f354dde..c6f6cee6b 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.States return (existing.Doc, existing.Version); } - return (default(T), -1); + return (default(T), EtagVersion.NotFound); } public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index b30298026..c51c651bf 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -65,10 +65,15 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(context, nameof(context)); - var domainObjectCommand = GetCommand(context); - var domainObjectId = domainObjectCommand.AggregateId; + var domainCommand = GetCommand(context); + var domainObjectId = domainCommand.AggregateId; var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); + if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) + { + throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion); + } + await handler(domainObject); await domainObject.WriteAsync(log); @@ -92,13 +97,18 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(context, nameof(context)); - var domainObjectCommand = GetCommand(context); - var domainObjectId = domainObjectCommand.AggregateId; + var domainCommand = GetCommand(context); + var domainObjectId = domainCommand.AggregateId; using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) { var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) + { + throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion); + } + await handler(domainObject); await domainObject.WriteAsync(log); diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 32135d0fb..06c81f26f 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -15,16 +15,16 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectBase : IDomainObject where TState : new() + public abstract class DomainObjectBase : IDomainObject where TState : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private Guid id; - private TState state = new TState(); + private TState state; private IPersistence persistence; public long Version { - get { return persistence?.Version ?? -1; } + get { return state.Version; } } public TState State @@ -32,6 +32,12 @@ namespace Squidex.Infrastructure.Commands get { return state; } } + protected DomainObjectBase() + { + state = new TState(); + state.Version = EtagVersion.Empty; + } + public IReadOnlyList> GetUncomittedEvents() { return uncomittedEvents; @@ -78,19 +84,26 @@ namespace Squidex.Infrastructure.Commands public async Task WriteAsync(ISemanticLog log) { - await persistence.WriteSnapshotAsync(state); + var events = uncomittedEvents; - try - { - await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); - } - catch (Exception ex) - { - log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); - } - finally + if (events.Count > 0) { - uncomittedEvents.Clear(); + state.Version += events.Count; + + await persistence.WriteSnapshotAsync(state); + + try + { + await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + } + catch (Exception ex) + { + log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); + } + finally + { + uncomittedEvents.Clear(); + } } } } diff --git a/src/Squidex.Infrastructure/Commands/ICommand.cs b/src/Squidex.Infrastructure/Commands/ICommand.cs index f28392eb4..b64b682d4 100644 --- a/src/Squidex.Infrastructure/Commands/ICommand.cs +++ b/src/Squidex.Infrastructure/Commands/ICommand.cs @@ -10,6 +10,6 @@ namespace Squidex.Infrastructure.Commands { public interface ICommand { - long? ExpectedVersion { get; set; } + long ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure/Commands/IDomainState.cs b/src/Squidex.Infrastructure/Commands/IDomainState.cs new file mode 100644 index 000000000..7cd9bceff --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/IDomainState.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IDomainState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.Commands +{ + public interface IDomainState + { + long Version { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/EtagVersion.cs b/src/Squidex.Infrastructure/EtagVersion.cs new file mode 100644 index 000000000..d99ec1fd1 --- /dev/null +++ b/src/Squidex.Infrastructure/EtagVersion.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// EtagVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public static class EtagVersion + { + public const long Any = -2; + + public const long Empty = -1; + + public const long NotFound = long.MinValue; + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs b/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs index 276289788..713d3a3da 100644 --- a/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs +++ b/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs @@ -20,6 +20,8 @@ namespace Squidex.Infrastructure.EventSourcing public static readonly string EventStreamNumber = "EventStreamNumber"; + public static readonly string SnapshotVersion = "SnapshotVersion"; + public static readonly string Timestamp = "Timestamp"; public static readonly string Actor = "Actor"; diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index e1cc4f7b5..fe76d145f 100644 --- a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -26,9 +26,21 @@ namespace Squidex.Infrastructure.EventSourcing return envelope; } + public static long SnapshotVersion(this EnvelopeHeaders headers) + { + return headers[CommonHeaders.SnapshotVersion].ToInt64(CultureInfo.InvariantCulture); + } + + public static Envelope SetSnapshotVersion(this Envelope envelope, long value) where T : class + { + envelope.Headers.Set(CommonHeaders.SnapshotVersion, value); + + return envelope; + } + public static long EventStreamNumber(this EnvelopeHeaders headers) { - return headers[CommonHeaders.EventStreamNumber].ToInt32(CultureInfo.InvariantCulture); + return headers[CommonHeaders.EventStreamNumber].ToInt64(CultureInfo.InvariantCulture); } public static Envelope SetEventStreamNumber(this Envelope envelope, long value) where T : class diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 3c9528178..462cf5cc3 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -20,6 +20,6 @@ namespace Squidex.Infrastructure.States Task WriteSnapshotAsync(TState state); - Task ReadAsync(long expectedVersion = ExpectedVersion.Any); + Task ReadAsync(long expectedVersion = EtagVersion.Any); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 542320f75..4e633c967 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -12,6 +12,8 @@ using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; +#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement + namespace Squidex.Infrastructure.States { internal sealed class Persistence : IPersistence @@ -25,8 +27,8 @@ namespace Squidex.Infrastructure.States private readonly Action invalidate; private readonly Func applyState; private readonly Func, Task> applyEvent; - private long versionSnapshot = -1; - private long versionEvents = -1; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; private long version; public long Version @@ -55,19 +57,19 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public async Task ReadAsync(long expectedVersion = ExpectedVersion.Any) + public async Task ReadAsync(long expectedVersion = EtagVersion.Any) { - versionSnapshot = -1; - versionEvents = -1; + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; await ReadSnapshotAsync(); await ReadEventsAsync(); UpdateVersion(); - if (expectedVersion != ExpectedVersion.Any && expectedVersion != version) + if (expectedVersion != EtagVersion.Any && expectedVersion != version) { - if (version == ExpectedVersion.Empty) + if (version == EtagVersion.Empty) { throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); } @@ -84,6 +86,11 @@ namespace Squidex.Infrastructure.States { var (state, position) = await snapshotStore.ReadAsync(ownerKey); + if (position < EtagVersion.Empty) + { + position = EtagVersion.Empty; + } + versionSnapshot = position; versionEvents = position; @@ -150,7 +157,7 @@ namespace Squidex.Infrastructure.States if (eventArray.Length > 0) { - var expectedVersion = UseEventSourcing() ? version : ExpectedVersion.Any; + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; var commitId = Guid.NewGuid(); @@ -159,7 +166,7 @@ namespace Squidex.Infrastructure.States try { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), Version, eventData); + await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); } catch (WrongEventVersionException ex) { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 6f0130bc5..59f74abfd 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; @@ -81,7 +82,7 @@ namespace Squidex.Areas.Api.Controllers.Apps await CommandBus.PublishAsync(command); - var response = SimpleMapper.Map(command, new ClientDto { Name = command.Id }); + var response = SimpleMapper.Map(command, new ClientDto { Name = command.Id, Permission = AppClientPermission.Editor }); return CreatedAtAction(nameof(GetClients), new { app }, response); } diff --git a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs index 82029aea4..f2a9ad3f8 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs @@ -11,6 +11,7 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; namespace Squidex.Pipeline.CommandMiddlewares @@ -33,6 +34,10 @@ namespace Squidex.Pipeline.CommandMiddlewares { context.Command.ExpectedVersion = expectedVersion; } + else + { + context.Command.ExpectedVersion = EtagVersion.Any; + } await next(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs index b43cfe6ab..3ace057fe 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs @@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refContents = new List { contentRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) @@ -543,7 +543,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refAssets = new List { assetRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) @@ -602,7 +602,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index 1a020997d..5ca90705e 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -10,11 +10,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; -using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands @@ -29,17 +29,16 @@ namespace Squidex.Infrastructure.Commands private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); private readonly CommandContext context; + private readonly CommandContext invalidContext = new CommandContext(A.Dummy()); private readonly Guid domainObjectId = Guid.NewGuid(); + private readonly MyCommand command; private readonly MyDomainObject domainObject = new MyDomainObject(); private readonly AggregateHandler sut; - public sealed class MyEvent : IEvent - { - } - public AggregateHandlerTests() { - context = new CommandContext(new MyCommand { AggregateId = domainObjectId }); + command = new MyCommand { AggregateId = domainObjectId }; + context = new CommandContext(command); A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) .Returns(persistence); @@ -58,13 +57,29 @@ namespace Squidex.Infrastructure.Commands [Fact] public Task Create_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(invalidContext, x => TaskHelper.False)); } [Fact] public Task Create_synced_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(invalidContext, x => TaskHelper.False)); + } + + [Fact] + public Task Create_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(context, x => TaskHelper.False)); + } + + [Fact] + public Task Create_synced_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(context, x => TaskHelper.False)); } [Fact] @@ -150,13 +165,29 @@ namespace Squidex.Infrastructure.Commands [Fact] public Task Update_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(invalidContext, x => TaskHelper.False)); } [Fact] public Task Update_synced_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(invalidContext, x => TaskHelper.False)); + } + + [Fact] + public Task Update_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(context, x => TaskHelper.False)); + } + + [Fact] + public Task Update_synced_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(context, x => TaskHelper.False)); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs index 8e7256bdb..e8d043fcc 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs @@ -7,7 +7,7 @@ // ========================================================================== using System; -using Squidex.Infrastructure.Commands.TestHelpers; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index aea30eb48..bbd13e61d 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -11,10 +11,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; -using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands @@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public void Should_instantiate() { - Assert.Equal(-1, sut.Version); + Assert.Equal(EtagVersion.NotFound, sut.Version); } [Fact] @@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - Assert.Equal(-1, sut.Version); + Assert.Equal(EtagVersion.NotFound, sut.Version); Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); sut.ClearUncommittedEvents(); @@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - var newState = "STATE"; + var newState = new MyDomainState(); sut.UpdateState(newState); @@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - var newState = "STATE"; + var newState = new MyDomainState(); sut.UpdateState(newState); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs index 01f0dda59..febba61c0 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using FakeItEasy; using NodaTime; -using Squidex.Infrastructure.Commands.TestHelpers; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs index 817867902..39927fbca 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.EventSourcing @@ -18,10 +19,6 @@ namespace Squidex.Infrastructure.EventSourcing private readonly IEventConsumer consumer1 = A.Fake(); private readonly IEventConsumer consumer2 = A.Fake(); - private sealed class MyEvent : IEvent - { - } - [Fact] public void Should_return_given_name() { diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs index 9c5bb6101..6670ee657 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs @@ -81,7 +81,18 @@ namespace Squidex.Infrastructure.EventSourcing sut.SetEventStreamNumber(eventStreamNumber); Assert.Equal(eventStreamNumber, sut.Headers.EventStreamNumber()); - Assert.Equal(eventStreamNumber, sut.Headers["EventStreamNumber"].ToInt32(culture)); + Assert.Equal(eventStreamNumber, sut.Headers["EventStreamNumber"].ToInt64(culture)); + } + + [Fact] + public void Should_set_and_get_snapshot_version() + { + const int snapshotVersion = 123; + + sut.SetSnapshotVersion(snapshotVersion); + + Assert.Equal(snapshotVersion, sut.Headers.SnapshotVersion()); + Assert.Equal(snapshotVersion, sut.Headers["SnapshotVersion"].ToInt64(culture)); } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs index 6c0261cf5..82f6b0946 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs @@ -11,17 +11,13 @@ using System.Linq; using Newtonsoft.Json; using NodaTime; using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.EventSourcing { public class EventDataFormatterTests { - public sealed class MyEvent : IEvent - { - public string MyProperty { get; set; } - } - public sealed class MyOldEvent : IEvent, IMigratedEvent { public string MyProperty { get; set; } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 85f873321..0fa7113b7 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -12,16 +12,13 @@ using FakeItEasy; using FluentAssertions; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.EventSourcing.Grains { public class EventConsumerGrainTests { - public sealed class MyEvent : IEvent - { - } - public sealed class MyEventConsumerGrain : EventConsumerGrain { public MyEventConsumerGrain(IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) @@ -67,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.Name) .Returns(consumerName); - A.CallTo(() => persistence.ReadAsync(ExpectedVersion.Any)) + A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) .Invokes(new Action(s => apply(state))); A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) diff --git a/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs b/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs index 4ebdd6038..7f2b1c5be 100644 --- a/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs +++ b/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure Assert.True(bag.Contains("NewKey")); Assert.Equal(1, bag.Count); - Assert.Equal(123, bag["NewKey"].ToInt32(c)); + Assert.Equal(123, bag["NewKey"].ToInt64(c)); Assert.False(bag.Contains("OldKey")); } @@ -174,7 +174,7 @@ namespace Squidex.Infrastructure { bag.Set("Key", "abc"); - Assert.Throws(() => bag["Key"].ToInt32(CultureInfo.InvariantCulture)); + Assert.Throws(() => bag["Key"].ToInt64(CultureInfo.InvariantCulture)); } [Fact] @@ -214,7 +214,7 @@ namespace Squidex.Infrastructure { bag.Set("Key", long.MaxValue); - Assert.Throws(() => bag["Key"].ToInt32(c)); + Assert.Throws(() => bag["Key"].ToInt64(c)); } [Fact] @@ -347,7 +347,7 @@ namespace Squidex.Infrastructure private void AssertNumber() { - AssertInt32(123); + AssertInt64(123); AssertInt64(123); AssertSingle(123); AssertDouble(123); @@ -420,10 +420,10 @@ namespace Squidex.Infrastructure Assert.Equal(expected, (long?)dynamicBag.Key); } - private void AssertInt32(int expected) + private void AssertInt64(int expected) { - Assert.Equal(expected, bag["Key"].ToInt32(c)); - Assert.Equal(expected, bag["Key"].ToNullableInt32(c)); + Assert.Equal(expected, bag["Key"].ToInt64(c)); + Assert.Equal(expected, bag["Key"].ToNullableInt64(c)); Assert.Equal(expected, (int)dynamicBag.Key); Assert.Equal(expected, (int?)dynamicBag.Key); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index c7ae719ce..0ed94cac8 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -15,16 +15,13 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.States { public class StateEventSourcingTests { - public sealed class MyEvent : IEvent - { - } - private class MyStatefulObject : IStatefulObject { private readonly List appliedEvents = new List(); @@ -115,7 +112,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_read_events_from_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; + statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -131,7 +128,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; + statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -144,7 +141,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; + statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -177,7 +174,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(0); @@ -187,7 +184,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(0); @@ -200,7 +197,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(0); @@ -218,7 +215,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_position() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; InvalidateMessage message = null; @@ -248,7 +245,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(3); @@ -263,7 +260,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -275,7 +272,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index b7d91bb0c..9ccb867df 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -100,7 +100,7 @@ namespace Squidex.Infrastructure.States statefulObject.ExpectedVersion = 0; A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((0, -1)); + .Returns((0, EtagVersion.Empty)); await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } @@ -119,10 +119,10 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((0, -1)); + .Returns((0, EtagVersion.Empty)); await sut.GetSingleAsync(key); } @@ -130,7 +130,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -141,7 +141,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject1 = await sut.GetSingleAsync(key); @@ -157,7 +157,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_serve_next_request_from_cache_when_detached() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject1 = await sut.CreateAsync(key); @@ -173,7 +173,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_version() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; InvalidateMessage message = null; @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); @@ -220,7 +220,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -232,7 +232,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyCommand.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs similarity index 80% rename from tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyCommand.cs rename to tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs index c610d9d08..6a5ee2a63 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyCommand.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs @@ -8,14 +8,15 @@ using System; using NodaTime; +using Squidex.Infrastructure.Commands; -namespace Squidex.Infrastructure.Commands.TestHelpers +namespace Squidex.Infrastructure.TestHelpers { internal sealed class MyCommand : IAggregateCommand, ITimestampCommand { public Guid AggregateId { get; set; } - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public Instant Timestamp { get; set; } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs similarity index 78% rename from tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs rename to tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index a725a7a13..23a0d0917 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -6,11 +6,11 @@ // All rights reserved. // ========================================================================== -using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Commands; -namespace Squidex.Infrastructure.Commands.TestHelpers +namespace Squidex.Infrastructure.TestHelpers { - internal sealed class MyDomainObject : DomainObjectBase + internal sealed class MyDomainObject : DomainObjectBase { } } diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs new file mode 100644 index 000000000..ce0665b42 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// MyDomainState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.Commands; + +namespace Squidex.Infrastructure.TestHelpers +{ + public class MyDomainState : IDomainState + { + public long Version { get; set; } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyEvent.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs similarity index 82% rename from tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyEvent.cs rename to tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs index e8d7e0f4b..cf2d048ce 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyEvent.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs @@ -8,9 +8,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.Commands.TestHelpers +namespace Squidex.Infrastructure.TestHelpers { internal sealed class MyEvent : IEvent { + public string MyProperty { get; set; } } } \ No newline at end of file From 598af534577c937fd1c242ee77b49eae4839f829 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 18:51:50 +0100 Subject: [PATCH 14/30] Tests fixed --- .../Commands/DomainObjectBase.cs | 8 +++--- .../Commands/AggregateHandlerTests.cs | 20 +++++++++++--- .../Commands/DomainObjectBaseTests.cs | 26 +++++-------------- .../PropertiesBagTests.cs | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 06c81f26f..c5800711f 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -84,17 +84,17 @@ namespace Squidex.Infrastructure.Commands public async Task WriteAsync(ISemanticLog log) { - var events = uncomittedEvents; + var events = uncomittedEvents.ToArray(); - if (events.Count > 0) + if (events.Length > 0) { - state.Version += events.Count; + state.Version += events.Length; await persistence.WriteSnapshotAsync(state); try { - await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + await persistence.WriteEventsAsync(events); } catch (Exception ex) { diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index 5ca90705e..943e02bd8 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.Commands private readonly IServiceProvider serviceProvider = A.Fake(); private readonly IStore store = A.Fake(); private readonly IStateFactory stateFactory = A.Fake(); - private readonly IPersistence persistence = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); private readonly CommandContext context; @@ -37,10 +37,10 @@ namespace Squidex.Infrastructure.Commands public AggregateHandlerTests() { - command = new MyCommand { AggregateId = domainObjectId }; + command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any }; context = new CommandContext(command); - A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) + A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) .Returns(persistence); A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) @@ -111,12 +111,14 @@ namespace Squidex.Infrastructure.Commands await sut.CreateSyncedAsync(context, async x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); await Task.Yield(); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result>()); @@ -132,10 +134,12 @@ namespace Squidex.Infrastructure.Commands await sut.CreateAsync(context, x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result>()); @@ -151,10 +155,12 @@ namespace Squidex.Infrastructure.Commands await sut.CreateSyncedAsync(context, x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result>()); @@ -198,12 +204,14 @@ namespace Squidex.Infrastructure.Commands await sut.UpdateAsync(context, async x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); await Task.Yield(); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result()); @@ -219,12 +227,14 @@ namespace Squidex.Infrastructure.Commands await sut.UpdateSyncedAsync(context, async x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); await Task.Yield(); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result()); @@ -240,10 +250,12 @@ namespace Squidex.Infrastructure.Commands await sut.UpdateAsync(context, x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result()); @@ -259,10 +271,12 @@ namespace Squidex.Infrastructure.Commands await sut.UpdateSyncedAsync(context, x => { x.RaiseEvent(new MyEvent()); + x.RaiseEvent(new MyEvent()); passedDomainObject = x; }); + Assert.Equal(1, domainObject.State.Version); Assert.Equal(domainObject, passedDomainObject); Assert.NotNull(context.Result()); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index bbd13e61d..539cd839f 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -22,20 +22,20 @@ namespace Squidex.Infrastructure.Commands public class DomainObjectBaseTests { private readonly IStore store = A.Fake(); - private readonly IPersistence persistence = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); private readonly Guid id = Guid.NewGuid(); private readonly MyDomainObject sut = new MyDomainObject(); public DomainObjectBaseTests() { - A.CallTo(() => store.WithSnapshots(id.ToString(), A>.Ignored)) + A.CallTo(() => store.WithSnapshots(id.ToString(), A>.Ignored)) .Returns(persistence); } [Fact] public void Should_instantiate() { - Assert.Equal(EtagVersion.NotFound, sut.Version); + Assert.Equal(EtagVersion.Empty, sut.Version); } [Fact] @@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - Assert.Equal(EtagVersion.NotFound, sut.Version); + Assert.Equal(EtagVersion.Empty, sut.Version); Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); sut.ClearUncommittedEvents(); @@ -58,21 +58,14 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_write_state_and_events_when_saved() { - A.CallTo(() => persistence.Version) - .Returns(100); - await sut.ActivateAsync(id.ToString(), store); - Assert.Equal(100, sut.Version); - var event1 = new MyEvent(); var event2 = new MyEvent(); + var newState = new MyDomainState(); sut.RaiseEvent(event1); sut.RaiseEvent(event2); - - var newState = new MyDomainState(); - sut.UpdateState(newState); await sut.WriteAsync(A.Fake()); @@ -88,24 +81,17 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_ignore_exception_when_saving() { - A.CallTo(() => persistence.Version) - .Returns(100); - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .Throws(new InvalidOperationException()); await sut.ActivateAsync(id.ToString(), store); - Assert.Equal(100, sut.Version); - var event1 = new MyEvent(); var event2 = new MyEvent(); + var newState = new MyDomainState(); sut.RaiseEvent(event1); sut.RaiseEvent(event2); - - var newState = new MyDomainState(); - sut.UpdateState(newState); await sut.WriteAsync(A.Fake()); diff --git a/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs b/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs index 7f2b1c5be..a833217e5 100644 --- a/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs +++ b/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs @@ -214,7 +214,7 @@ namespace Squidex.Infrastructure { bag.Set("Key", long.MaxValue); - Assert.Throws(() => bag["Key"].ToInt64(c)); + Assert.Throws(() => bag["Key"].ToInt32(c)); } [Fact] From 394b9ebdaba24f866df05adda2785f6e27c558be Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 18:55:30 +0100 Subject: [PATCH 15/30] Tests improved again. --- .../States/StateSnapshotTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index 9ccb867df..2465e5e53 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -28,6 +28,11 @@ namespace Squidex.Infrastructure.States public long ExpectedVersion { get; set; } + public long Version + { + get { return persistence.Version; } + } + public int State { get { return state; } @@ -94,6 +99,20 @@ namespace Squidex.Infrastructure.States Assert.Equal(123, statefulObject.State); } + [Fact] + public async Task Should_set_to_empty_when_store_returns_not_found() + { + statefulObject.ExpectedVersion = EtagVersion.Any; + + A.CallTo(() => snapshotStore.ReadAsync(key)) + .Returns((123, EtagVersion.NotFound)); + + var actualObject = await sut.GetSingleAsync(key); + + Assert.Equal(-1, statefulObject.Version); + Assert.Equal( 0, statefulObject.State); + } + [Fact] public async Task Should_throw_exception_if_not_found() { From f5d5f762289d4953a99a1e8600b5fc329250c22b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 18:57:19 +0100 Subject: [PATCH 16/30] Build files updated. --- Dockerfile | 3 +-- Dockerfile.build | 3 +-- tests/RunCoverage.ps1 | 22 +++++----------------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3dd086889..be4c10abb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,8 +47,7 @@ RUN cp -a /tmp/node_modules /src/Squidex/ \ RUN dotnet restore \ && dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ - && dotnet test tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj \ - && dotnet test tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj \ + && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj # Publish diff --git a/Dockerfile.build b/Dockerfile.build index 83f9e11b3..f09cbb2bb 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -44,8 +44,7 @@ RUN cp -a /tmp/node_modules /src/Squidex/ \ RUN dotnet restore \ && dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ - && dotnet test tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj \ - && dotnet test tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj \ + && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj # Publish diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index 594623942..ae66df3c9 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -1,8 +1,7 @@ Param( [switch]$infrastructure, [switch]$appsCore, - [switch]$appsRead, - [switch]$appsWrite, + [switch]$appsEntities, [switch]$users, [switch]$all ) @@ -43,25 +42,14 @@ if ($all -Or $appsCore) { -oldStyle } -if ($all -Or $appsRead) { +if ($all -Or $appsEntities) { &"$folderHome\.nuget\packages\OpenCover\4.6.519\tools\OpenCover.Console.exe" ` -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` - -targetargs:"test $folderWorking\Squidex.Domain.Apps.Read.Tests\Squidex.Domain.Apps.Read.Tests.csproj" ` - -filter:"+[Squidex.Domain.Apps.Read*]*" ` + -targetargs:"test $folderWorking\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj" ` + -filter:"+[Squidex.Domain.Apps.Entities*]*" ` -skipautoprops ` - -output:"$folderWorking\$folderReports\Read.xml" ` - -oldStyle -} - -if ($all -Or $appsWrite) { - &"$folderHome\.nuget\packages\OpenCover\4.6.519\tools\OpenCover.Console.exe" ` - -register:user ` - -target:"C:\Program Files\dotnet\dotnet.exe" ` - -targetargs:"test $folderWorking\Squidex.Domain.Apps.Write.Tests\Squidex.Domain.Apps.Write.Tests.csproj" ` - -filter:"+[Squidex.Domain.Apps.Write*]*" ` - -skipautoprops ` - -output:"$folderWorking\$folderReports\Write.xml" ` + -output:"$folderWorking\$folderReports\Entities.xml" ` -oldStyle } From a1d7d2bc94aad64748e49db21c8b954c5a236457 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 19:55:09 +0100 Subject: [PATCH 17/30] More tests fixed --- .../Apps/AppCommandMiddleware.cs | 4 +- .../Assets/AssetCommandMiddleware.cs | 6 +- .../Rules/State/RuleState.cs | 5 + .../Schemas/SchemaCommandMiddleware.cs | 4 +- .../Schemas/SchemaDomainObject.cs | 4 +- .../Schemas/State/SchemaState.cs | 12 +- .../Apps/AppCommandMiddlewareTests.cs | 5 + .../Apps/AppDomainObjectTests.cs | 5 + .../Assets/AssetCommandMiddlewareTests.cs | 17 +- .../Assets/AssetDomainObjectTests.cs | 5 + .../Contents/ContentCommandMiddlewareTests.cs | 7 +- .../Contents/ContentDomainObjectTests.cs | 7 +- .../Contents/GraphQLTests.cs | 2 +- .../Rules/RuleCommandMiddlewareTests.cs | 9 +- .../Rules/RuleDomainObjectTests.cs | 9 +- .../AssetsFieldPropertiesTests.cs | 104 +++ .../BooleanFieldPropertiesTests.cs | 34 + .../DateTimeFieldPropertiesTests.cs | 126 ++++ .../GeolocationFieldPropertiesTests.cs | 34 + .../JsonFieldPropertiesTests.cs | 27 + .../NumberFieldPropertiesTests.cs | 134 ++++ .../ReferencesFieldPropertiesTests.cs | 34 + .../StringFieldPropertiesTests.cs | 105 +++ .../TagsFieldPropertiesTests.cs | 34 + .../Schemas/Guards/GuardSchemaFieldTests.cs | 247 +++++++ .../Schemas/Guards/GuardSchemaTests.cs | 201 ++++++ .../Schemas/SchemaCommandMiddlewareTests.cs | 281 ++++++++ .../Schemas/SchemaDomainObjectTests.cs | 663 ++++++++++++++++++ .../TestHelpers/HandlerTestBase.cs | 6 + 29 files changed, 2107 insertions(+), 24 deletions(-) create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 22ea628b2..5a3a7f1d3 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Apps a.Create(command); - context.Complete(EntityCreatedResult.Create(a.State.Id, a.Version)); + context.Complete(EntityCreatedResult.Create(command.AppId, a.Version)); }); } @@ -150,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Apps } else { - var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.State.Id, a.State.Name, command.PlanId); + var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, command.AppId.Id, a.State.Name, command.PlanId); if (result is PlanChangedResult) { diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 887c7acdb..dcc8a3029 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -50,10 +50,10 @@ namespace Squidex.Domain.Apps.Entities.Assets await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); - context.Complete(EntityCreatedResult.Create(a.State.Id, a.Version)); + context.Complete(EntityCreatedResult.Create(command.AssetId, a.Version)); }); - await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.State.Id.ToString(), asset.State.FileVersion, null); + await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), command.AssetId.ToString(), asset.State.FileVersion, null); } finally { @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Assets context.Complete(new AssetSavedResult(a.Version, a.State.FileVersion)); }); - await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.State.Id.ToString(), asset.State.FileVersion, null); + await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), command.AssetId.ToString(), asset.State.FileVersion, null); } finally { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index f08e8ddae..2e593c7b0 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -50,6 +50,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.State RuleDef = RuleDef.Disable(); } + protected void On(RuleDeleted @event) + { + IsDeleted = true; + } + public RuleState Apply(Envelope @event) { var payload = (SquidexEvent)@event.Payload; diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs index 87f0c4d4b..cb5e13e38 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Dispatching; -namespace Squidex.Domain.Apps.Entities.State.SchemaDefs +namespace Squidex.Domain.Apps.Entities.Schemas { public class SchemaCommandMiddleware : ICommandMiddleware { @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs s.Create(command); - context.Complete(EntityCreatedResult.Create(s.State.Id, s.Version)); + context.Complete(EntityCreatedResult.Create(command.SchemaId, s.Version)); }); } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index c3ec2642b..73becbd1b 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { VerifyNotCreated(); - var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(State.Id, command.Name) }); + var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(command.SchemaId, command.Name) }); if (command.Fields != null) { @@ -209,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas protected override void OnRaised(Envelope @event) { - UpdateState(State.Apply(@event)); + UpdateState(State.Apply(@event, registry)); } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 585e890e4..527c584ac 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State public Guid AppId { get; set; } [JsonProperty] - public int TotalFields { get; set; } = 1; + public int TotalFields { get; set; } = 0; [JsonProperty] public bool IsDeleted { get; set; } @@ -62,6 +62,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State protected void On(SchemaCreated @event, FieldRegistry registry) { + Name = @event.Name; + var schema = new Schema(@event.Name); if (@event.Properties != null) @@ -73,6 +75,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State { foreach (var eventField in @event.Fields) { + TotalFields++; + var partitioning = string.Equals(eventField.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? Partitioning.Language : @@ -96,8 +100,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State } schema = schema.AddField(field); - - TotalFields++; } } @@ -184,11 +186,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State SimpleMapper.Map(@event, this); } - public SchemaState Apply(Envelope @event) + public SchemaState Apply(Envelope @event, FieldRegistry registry) { var payload = (SquidexEvent)@event.Payload; - return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload, registry)); } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index 14a93859f..bb778526d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -32,6 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string clientName = "client"; private readonly AppCommandMiddleware sut; + protected override Guid Id + { + get { return AppId; } + } + public AppCommandMiddlewareTests() { A.CallTo(() => appProvider.GetAppAsync(AppName)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index 4dc8b389d..bea62e007 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -26,6 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string planId = "premium"; private readonly AppDomainObject sut = new AppDomainObject(); + protected override Guid Id + { + get { return AppId; } + } + [Fact] public void Create_should_throw_exception_if_created() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index fbd1bbd7a..04c41ff74 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -30,6 +30,11 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly AssetFile file; private readonly AssetCommandMiddleware sut; + protected override Guid Id + { + get { return assetId; } + } + public AssetCommandMiddlewareTests() { file = new AssetFile("my-image.png", "image/png", 1024, () => stream); @@ -124,14 +129,18 @@ namespace Squidex.Domain.Apps.Entities.Assets private void AssertAssetImageChecked() { - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)).MustHaveHappened(); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .MustHaveHappened(); } private void AssertAssetHasBeenUploaded(long version, Guid commitId) { - A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)).MustHaveHappened(); - A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).MustHaveHappened(); - A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())).MustHaveHappened(); + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)) + .MustHaveHappened(); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)) + .MustHaveHappened(); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) + .MustHaveHappened(); } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs index 081ffecd4..ff14541a9 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs @@ -24,6 +24,11 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); private readonly AssetDomainObject sut = new AssetDomainObject(); + protected override Guid Id + { + get { return assetId; } + } + [Fact] public void Create_should_throw_exception_if_created() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs index b94d97994..96ade7d26 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -39,6 +39,11 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly ContentDomainObject content = new ContentDomainObject(); private readonly ContentCommandMiddleware sut; + protected override Guid Id + { + get { return contentId; } + } + private readonly NamedContentData invalidData = new NamedContentData() .AddField("my-field1", new ContentFieldData() @@ -238,7 +243,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void CreateContent() { - content.Create(new CreateContent { Data = data }); + content.Create(CreateCommand(new CreateContent { Data = data })); } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 400964524..a92c0570f 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -32,10 +32,15 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly Guid contentId = Guid.NewGuid(); private readonly ContentDomainObject sut = new ContentDomainObject(); + protected override Guid Id + { + get { return contentId; } + } + [Fact] public void Create_should_throw_exception_if_created() { - sut.Create(new CreateContent { Data = data }); + sut.Create(CreateCommand(new CreateContent { Data = data })); Assert.Throws(() => { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs index 3ace057fe..abad6c454 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs @@ -391,7 +391,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs index f721df368..61ab2de50 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -29,6 +29,11 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly Guid ruleId = Guid.NewGuid(); private readonly RuleCommandMiddleware sut; + protected override Guid Id + { + get { return ruleId; } + } + public RuleCommandMiddlewareTests() { A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) @@ -103,12 +108,12 @@ namespace Squidex.Domain.Apps.Entities.Rules private void DisableRule() { - rule.Disable(new DisableRule()); + rule.Disable(CreateCommand(new DisableRule())); } private void CreateRule() { - rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + rule.Create(CreateCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); } } } \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index 4f4376e56..0b11c525d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -26,10 +26,15 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; private readonly RuleDomainObject sut = new RuleDomainObject(); + protected override Guid Id + { + get { return ruleId; } + } + [Fact] public void Create_should_throw_exception_if_created() { - sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); Assert.Throws(() => { @@ -44,6 +49,8 @@ namespace Squidex.Domain.Apps.Entities.Rules sut.Create(CreateRuleCommand(command)); + Assert.Equal(AppId, sut.State.AppId); + Assert.Same(ruleTrigger, sut.State.RuleDef.Trigger); Assert.Same(ruleAction, sut.State.RuleDef.Action); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs new file mode 100644 index 000000000..9f4f4c7b8 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// AssetsFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class AssetsFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_min_items_greater_than_max_items() + { + var sut = new AssetsFieldProperties { MinItems = 10, MaxItems = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems") + }); + } + + [Fact] + public void Should_add_error_if_min_width_greater_than_max_width() + { + var sut = new AssetsFieldProperties { MinWidth = 10, MaxWidth = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max width must be greater than min width.", "MinWidth", "MaxWidth") + }); + } + + [Fact] + public void Should_add_error_if_min_height_greater_than_max_height() + { + var sut = new AssetsFieldProperties { MinHeight = 10, MaxHeight = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max height must be greater than min height.", "MinHeight", "MaxHeight") + }); + } + + [Fact] + public void Should_add_error_if_min_size_greater_than_max_size() + { + var sut = new AssetsFieldProperties { MinSize = 10, MaxSize = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max size must be greater than min size.", "MinSize", "MaxSize") + }); + } + + [Fact] + public void Should_add_error_if_only_aspect_width_is_defined() + { + var sut = new AssetsFieldProperties { AspectWidth = 10 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Aspect width and height must be defined.", "AspectWidth", "AspectHeight") + }); + } + + [Fact] + public void Should_add_error_if_only_aspect_height_is_defined() + { + var sut = new AssetsFieldProperties { AspectHeight = 10 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Aspect width and height must be defined.", "AspectWidth", "AspectHeight") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs new file mode 100644 index 000000000..0acb80581 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// BooleanFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class BooleanFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new BooleanFieldProperties { Editor = (BooleanFieldEditor)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs new file mode 100644 index 000000000..24791ae69 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// DateTimeFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class DateTimeFieldPropertiesTests + { + [Fact] + public void Should_not_add_error_if_sut_is_valid() + { + var sut = new DateTimeFieldProperties + { + MinValue = FutureDays(10), + MaxValue = FutureDays(20), + DefaultValue = FutureDays(15) + }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_default_value_is_less_than_min() + { + var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), DefaultValue = FutureDays(5) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Default value must be greater than min value.", "DefaultValue") + }); + } + + [Fact] + public void Should_add_error_if_default_value_is_greater_than_min() + { + var sut = new DateTimeFieldProperties { MaxValue = FutureDays(10), DefaultValue = FutureDays(15) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Default value must be less than max value.", "DefaultValue") + }); + } + + [Fact] + public void Should_add_error_if_min_greater_than_max() + { + var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") + }); + } + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new DateTimeFieldProperties { Editor = (DateTimeFieldEditor)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } + + [Fact] + public void Should_add_error_if_calculated_default_value_is_not_valid() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = (DateTimeCalculatedDefaultValue)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Calculated default value is not valid.", "CalculatedDefaultValue") + }); + } + + [Fact] + public void Should_add_error_if_calculated_default_value_default_value_is_defined() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now, DefaultValue = FutureDays(10) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Calculated default value and default value cannot be used together.", "CalculatedDefaultValue", "DefaultValue") + }); + } + + private static Instant FutureDays(int days) + { + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs new file mode 100644 index 000000000..ae2e794f5 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// GeolocationPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class GeolocationFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs new file mode 100644 index 000000000..52e4601ed --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// JsonFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class JsonFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new JsonFieldProperties(); + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs new file mode 100644 index 000000000..add452cb4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs @@ -0,0 +1,134 @@ +// ========================================================================== +// NumberFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class NumberFieldPropertiesTests + { + [Fact] + public void Should_not_add_error_if_sut_is_valid() + { + var sut = new NumberFieldProperties + { + MinValue = 0, + MaxValue = 100, + DefaultValue = 5 + }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_default_value_is_less_than_min() + { + var sut = new NumberFieldProperties { MinValue = 10, DefaultValue = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Default value must be greater than min value.", "DefaultValue") + }); + } + + [Fact] + public void Should_add_error_if_default_value_is_greater_than_min() + { + var sut = new NumberFieldProperties { MaxValue = 0, DefaultValue = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Default value must be less than max value.", "DefaultValue") + }); + } + + [Fact] + public void Should_add_error_if_min_greater_than_max() + { + var sut = new NumberFieldProperties { MinValue = 10, MaxValue = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") + }); + } + + [Fact] + public void Should_add_error_if_allowed_values_and_max_value_is_specified() + { + var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = ImmutableList.Create(4d) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue") + }); + } + + [Fact] + public void Should_add_error_if_allowed_values_and_min_value_is_specified() + { + var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = ImmutableList.Create(4d) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue") + }); + } + + [Fact] + public void Should_add_error_if_radio_button_has_no_allowed_values() + { + var sut = new NumberFieldProperties { Editor = NumberFieldEditor.Radio }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") + }); + } + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new NumberFieldProperties { Editor = (NumberFieldEditor)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs new file mode 100644 index 000000000..8465de44c --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// ReferencesFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class ReferencesFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_min_greater_than_max() + { + var sut = new ReferencesFieldProperties { MinItems = 10, MaxItems = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs new file mode 100644 index 000000000..3d6585883 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// StringFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class StringFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_min_greater_than_max() + { + var sut = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max length must be greater than min length.", "MinLength", "MaxLength") + }); + } + + [Fact] + public void Should_add_error_if_allowed_values_and_max_value_is_specified() + { + var sut = new StringFieldProperties { MinLength = 10, AllowedValues = ImmutableList.Create("4") }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Either allowed values or min and max length can be defined.", "AllowedValues", "MinLength", "MaxLength") + }); + } + + [Fact] + public void Should_add_error_if_allowed_values_and_min_value_is_specified() + { + var sut = new StringFieldProperties { MaxLength = 10, AllowedValues = ImmutableList.Create("4") }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Either allowed values or min and max length can be defined.", "AllowedValues", "MinLength", "MaxLength") + }); + } + + [Fact] + public void Should_add_error_if_radio_button_has_no_allowed_values() + { + var sut = new StringFieldProperties { Editor = StringFieldEditor.Radio }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") + }); + } + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new StringFieldProperties { Editor = (StringFieldEditor)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } + + [Fact] + public void Should_add_error_if_pattern_is_not_valid_regex() + { + var sut = new StringFieldProperties { Pattern = "[0-9{1}" }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Pattern is not a valid expression.", "Pattern") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs new file mode 100644 index 000000000..6e413d0c9 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// TagsFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties +{ + public class TagsFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_min_greater_than_max() + { + var sut = new TagsFieldProperties { MinItems = 10, MaxItems = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems") + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs new file mode 100644 index 000000000..9770155e8 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaFieldTests.cs @@ -0,0 +1,247 @@ +// ========================================================================== +// GuardSchemaFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public class GuardSchemaFieldTests + { + private readonly Schema schema_0; + private readonly StringFieldProperties validProperties = new StringFieldProperties(); + private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; + + public GuardSchemaFieldTests() + { + schema_0 = + new Schema("my-schema") + .AddField(new StringField(1, "field1", Partitioning.Invariant)) + .AddField(new StringField(2, "field2", Partitioning.Invariant)); + } + + [Fact] + public void CanHide_should_throw_exception_if_already_hidden() + { + var command = new HideField { FieldId = 1 }; + + var schema_1 = schema_0.HideField(1); + + Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_not_found() + { + var command = new HideField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); + } + + [Fact] + public void CanHide_hould_not_throw_exception_if_visible() + { + var command = new HideField { FieldId = 1 }; + + GuardSchemaField.CanHide(schema_0, command); + } + + [Fact] + public void CanDisable_should_throw_exception_if_already_disabled() + { + var command = new DisableField { FieldId = 1 }; + + var schema_1 = schema_0.DisableField(1); + + Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); + } + + [Fact] + public void CanDisable_should_throw_exception_if_not_found() + { + var command = new DisableField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); + } + + [Fact] + public void CanDisable_Should_not_throw_exception_if_enabled() + { + var command = new DisableField { FieldId = 1 }; + + GuardSchemaField.CanDisable(schema_0, command); + } + + [Fact] + public void CanShow_should_throw_exception_if_already_shown() + { + var command = new ShowField { FieldId = 1 }; + + Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); + } + + [Fact] + public void CanShow_should_throw_exception_if_not_found() + { + var command = new ShowField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); + } + + [Fact] + public void CanShow_should_not_throw_exception_if_hidden() + { + var command = new ShowField { FieldId = 1 }; + + var schema_1 = schema_0.HideField(1); + + GuardSchemaField.CanShow(schema_1, command); + } + + [Fact] + public void CanEnable_should_throw_exception_if_already_enabled() + { + var command = new EnableField { FieldId = 1 }; + + Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); + } + + [Fact] + public void CanEnable_should_throw_exception_if_not_found() + { + var command = new EnableField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_disabled() + { + var command = new EnableField { FieldId = 1 }; + + var schema_1 = schema_0.DisableField(1); + + GuardSchemaField.CanEnable(schema_1, command); + } + + [Fact] + public void CanLock_should_throw_exception_if_already_locked() + { + var command = new LockField { FieldId = 1 }; + + var schema_1 = schema_0.LockField(1); + + Assert.Throws(() => GuardSchemaField.CanLock(schema_1, command)); + } + + [Fact] + public void LockField_should_throw_exception_if_not_found() + { + var command = new LockField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanLock(schema_0, command)); + } + + [Fact] + public void CanLock_should_not_throw_exception_if_not_locked() + { + var command = new LockField { FieldId = 1 }; + + GuardSchemaField.CanLock(schema_0, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_not_found() + { + var command = new DeleteField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanDelete(schema_0, command)); + } + + [Fact] + public void CanDelete_should_throw_exception_if_locked() + { + var command = new DeleteField { FieldId = 1 }; + + var schema_1 = schema_0.LockField(1); + + Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_not_locked() + { + var command = new DeleteField { FieldId = 1 }; + + GuardSchemaField.CanDelete(schema_0, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_locked() + { + var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; + + var schema_1 = schema_0.LockField(1); + + Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_not_locked() + { + var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; + + GuardSchemaField.CanUpdate(schema_0, command); + } + + [Fact] + public void CanAdd_should_throw_exception_if_field_already_exists() + { + var command = new AddField { Name = "field1", Properties = new StringFieldProperties() }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_not_valid() + { + var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_not_valid() + { + var command = new AddField { Name = "field3", Properties = invalidProperties }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_partitioning_not_valid() + { + var command = new AddField { Name = "field3", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_not_exists() + { + var command = new AddField { Name = "field3", Properties = new StringFieldProperties() }; + + GuardSchemaField.CanAdd(schema_0, command); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs new file mode 100644 index 000000000..326149595 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -0,0 +1,201 @@ +// ========================================================================== +// GuardSchemaTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +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; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Entities.Schemas.Guards +{ + public class GuardSchemaTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly Schema schema_0; + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + + public GuardSchemaTests() + { + schema_0 = + new Schema("my-schema") + .AddField(new StringField(1, "field1", Partitioning.Invariant)) + .AddField(new StringField(2, "field2", Partitioning.Invariant)); + + A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, "new-schema", false)) + .Returns(Task.FromResult(null)); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; + + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_name_already_in_use() + { + A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, "new-schema", false)) + .Returns(Task.FromResult(A.Fake())); + + var command = new CreateSchema { AppId = appId, Name = "new-schema" }; + + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_fields_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new CreateSchemaField + { + Name = null, + Properties = null, + Partitioning = "invalid" + }, + new CreateSchemaField + { + Name = null, + Properties = InvalidProperties(), + Partitioning = "invalid" + } + }, + Name = "new-schema" + }; + + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_fields_contain_duplicate_names() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new CreateSchemaField + { + Name = "field1", + Properties = ValidProperties(), + Partitioning = "invariant" + }, + new CreateSchemaField + { + Name = "field1", + Properties = ValidProperties(), + Partitioning = "invariant" + } + }, + Name = "new-schema" + }; + + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); + } + + [Fact] + public Task CanCreate_should_not_throw_exception_if_command_is_valid() + { + var command = new CreateSchema { AppId = appId, Name = "new-schema" }; + + return GuardSchema.CanCreate(command, appProvider); + } + + [Fact] + public void CanPublish_should_throw_exception_if_already_published() + { + var command = new PublishSchema(); + + var schema_1 = schema_0.Publish(); + + Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); + } + + [Fact] + public void CanPublish_should_not_throw_exception_if_not_published() + { + var command = new PublishSchema(); + + GuardSchema.CanPublish(schema_0, command); + } + + [Fact] + public void CanUnpublish_should_throw_exception_if_already_unpublished() + { + var command = new UnpublishSchema(); + + Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); + } + + [Fact] + public void CanUnpublish_should_not_throw_exception_if_already_published() + { + var command = new UnpublishSchema(); + + var schema_1 = schema_0.Publish(); + + GuardSchema.CanUnpublish(schema_1, command); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() + { + var command = new ReorderFields { FieldIds = new List { 1, 3 } }; + + Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() + { + var command = new ReorderFields { FieldIds = new List { 1 } }; + + Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); + } + + [Fact] + public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 } }; + + GuardSchema.CanReorder(schema_0, command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteSchema(); + + GuardSchema.CanDelete(schema_0, command); + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } + + private static StringFieldProperties InvalidProperties() + { + return new StringFieldProperties { MinLength = 20, MaxLength = 10 }; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs new file mode 100644 index 000000000..1cd2a9a1d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs @@ -0,0 +1,281 @@ +// ========================================================================== +// SchemaCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaCommandMiddlewareTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly SchemaCommandMiddleware sut; + private readonly SchemaDomainObject schema; + private readonly FieldRegistry registry = new FieldRegistry(new TypeNameRegistry()); + private readonly string fieldName = "age"; + + protected override Guid Id + { + get { return SchemaId; } + } + + public SchemaCommandMiddlewareTests() + { + schema = new SchemaDomainObject(registry); + + sut = new SchemaCommandMiddleware(Handler, appProvider); + + A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName, false)) + .Returns((ISchemaEntity)null); + } + + [Fact] + public async Task Create_should_create_schema_domain_object() + { + var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); + + await TestCreate(schema, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(SchemaId, context.Result>().IdOrValue); + + A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName, false)).MustHaveHappened(); + } + + [Fact] + public async Task UpdateSchema_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new UpdateSchema { Properties = new SchemaProperties() }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task ReorderSchema_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new ReorderFields { FieldIds = new List() }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task PublishSchema_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new PublishSchema()); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task UnpublishSchema_should_update_domain_object() + { + CreateSchema(); + PublishSchema(); + + var context = CreateContextForCommand(new UnpublishSchema()); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task ConfigureScripts_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new ConfigureScripts()); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task DeleteSchema_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new DeleteSchema()); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Add_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(1, context.Result>().IdOrValue); + } + + [Fact] + public async Task UpdateField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + var context = CreateContextForCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task LockField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + var context = CreateContextForCommand(new LockField { FieldId = 1 }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task HideField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + var context = CreateContextForCommand(new HideField { FieldId = 1 }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task ShowField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + HideField(); + + var context = CreateContextForCommand(new ShowField { FieldId = 1 }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task DisableField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + var context = CreateContextForCommand(new DisableField { FieldId = 1 }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task EnableField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + DisableField(); + + var context = CreateContextForCommand(new EnableField { FieldId = 1 }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task DeleteField_should_update_domain_object() + { + CreateSchema(); + CreateField(); + + var context = CreateContextForCommand(new DeleteField { FieldId = 1 }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + + private void CreateSchema() + { + schema.Create(CreateCommand(new CreateSchema { Name = SchemaName })); + } + + private void PublishSchema() + { + schema.Publish(CreateCommand(new PublishSchema())); + } + + private void CreateField() + { + schema.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); + } + + private void HideField() + { + schema.HideField(CreateCommand(new HideField { FieldId = 1 })); + } + + private void DisableField() + { + schema.DisableField(CreateCommand(new DisableField { FieldId = 1 })); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs new file mode 100644 index 000000000..5275eafc5 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs @@ -0,0 +1,663 @@ +// ========================================================================== +// SchemaDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaDomainObjectTests : HandlerTestBase + { + private readonly string fieldName = "age"; + private readonly NamedId fieldId; + private readonly SchemaDomainObject sut; + + protected override Guid Id + { + get { return SchemaId; } + } + + public SchemaDomainObjectTests() + { + fieldId = new NamedId(1, fieldName); + + var fieldRegistry = new FieldRegistry(new TypeNameRegistry()); + + sut = new SchemaDomainObject(fieldRegistry); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); + + Assert.Throws(() => + { + sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); + }); + } + + [Fact] + public void Create_should_create_schema_and_create_events() + { + var properties = new SchemaProperties(); + + sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties })); + + Assert.Equal(AppId, sut.State.AppId); + + Assert.Equal(SchemaName, sut.State.Name); + Assert.Equal(SchemaName, sut.State.SchemaDef.Name); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties }) + ); + } + + [Fact] + public void Create_should_create_schema_with_initial_fields() + { + var properties = new SchemaProperties(); + + var fields = new List + { + new CreateSchemaField { Name = "field1", Properties = ValidProperties() }, + new CreateSchemaField { Name = "field2", Properties = ValidProperties() } + }; + + sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties, Fields = fields })); + + var @event = (SchemaCreated)sut.GetUncomittedEvents().Single().Payload; + + Assert.Equal(AppId, sut.State.AppId); + Assert.Equal(SchemaName, sut.State.Name); + Assert.Equal(SchemaName, sut.State.SchemaDef.Name); + + Assert.Equal(2, @event.Fields.Count); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); + }); + } + + [Fact] + public void Update_should_refresh_properties_and_create_events() + { + var properties = new SchemaProperties(); + + CreateSchema(); + + sut.Update(CreateCommand(new UpdateSchema { Properties = properties })); + + Assert.Equal(properties, sut.State.SchemaDef.Properties); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaUpdated { Properties = properties }) + ); + } + + [Fact] + public void ConfigureScripts_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.ConfigureScripts(CreateCommand(new ConfigureScripts())); + }); + } + + [Fact] + public void ConfigureScripts_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.ConfigureScripts(CreateCommand(new ConfigureScripts())); + }); + } + + [Fact] + public void ConfigureScripts_should_create_events() + { + CreateSchema(); + + sut.ConfigureScripts(CreateCommand(new ConfigureScripts + { + ScriptQuery = "", + ScriptCreate = "", + ScriptUpdate = "", + ScriptDelete = "", + ScriptChange = "" + })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new ScriptsConfigured + { + ScriptQuery = "", + ScriptCreate = "", + ScriptUpdate = "", + ScriptDelete = "", + ScriptChange = "" + }) + ); + } + + [Fact] + public void Reorder_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); + }); + } + + [Fact] + public void Reorder_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); + }); + } + + [Fact] + public void Reorder_should_refresh_properties_and_create_events() + { + var fieldIds = new List { 1, 2 }; + + CreateSchema(); + + sut.Add(CreateCommand(new AddField { Name = "field1", Properties = ValidProperties() })); + sut.Add(CreateCommand(new AddField { Name = "field2", Properties = ValidProperties() })); + + sut.ClearUncommittedEvents(); + + sut.Reorder(CreateCommand(new ReorderFields { FieldIds = fieldIds })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldsReordered { FieldIds = fieldIds }) + ); + } + + [Fact] + public void Publish_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Publish(CreateCommand(new PublishSchema())); + }); + } + + [Fact] + public void Publish_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Publish(CreateCommand(new PublishSchema())); + }); + } + + [Fact] + public void Publish_should_refresh_properties_and_create_events() + { + CreateSchema(); + + sut.Publish(CreateCommand(new PublishSchema())); + + Assert.True(sut.State.SchemaDef.IsPublished); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaPublished()) + ); + } + + [Fact] + public void Unpublish_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Unpublish(CreateCommand(new UnpublishSchema())); + }); + } + + [Fact] + public void Unpublish_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Unpublish(CreateCommand(new UnpublishSchema())); + }); + } + + [Fact] + public void Unpublish_should_refresh_properties_and_create_events() + { + CreateSchema(); + PublishSchema(); + + sut.Unpublish(CreateCommand(new UnpublishSchema())); + + Assert.False(sut.State.SchemaDef.IsPublished); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaUnpublished()) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateCommand(new DeleteSchema())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Delete(CreateCommand(new DeleteSchema())); + }); + } + + [Fact] + public void Delete_should_refresh_properties_and_create_events() + { + CreateSchema(); + + sut.Delete(CreateCommand(new DeleteSchema())); + + Assert.True(sut.State.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaDeleted()) + ); + } + + [Fact] + public void AddField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = ValidProperties() })); + }); + } + + [Fact] + public void AddField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); + }); + } + + [Fact] + public void Add_should_update_schema_and_create_events() + { + var properties = new NumberFieldProperties(); + + CreateSchema(); + + sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = properties })); + + Assert.Equal(properties, sut.State.SchemaDef.FieldsById[1].RawProperties); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = properties }) + ); + } + + [Fact] + public void UpdateField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); + }); + } + + [Fact] + public void UpdateField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); + }); + } + + [Fact] + public void UpdateField_should_update_schema_and_create_events() + { + var properties = new NumberFieldProperties(); + + CreateSchema(); + CreateField(); + + sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = properties })); + + Assert.Equal(properties, sut.State.SchemaDef.FieldsById[1].RawProperties); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = properties }) + ); + } + + [Fact] + public void LockField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.LockField(CreateCommand(new LockField { FieldId = 1 })); + }); + } + + [Fact] + public void LockField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.LockField(CreateCommand(new LockField { FieldId = 1 })); + }); + } + + [Fact] + public void LockField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.LockField(CreateCommand(new LockField { FieldId = 1 })); + + Assert.False(sut.State.SchemaDef.FieldsById[1].IsDisabled); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new FieldLocked { FieldId = fieldId }) + ); + } + + [Fact] + public void HideField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.HideField(CreateCommand(new HideField { FieldId = 1 })); + }); + } + + [Fact] + public void HideField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.HideField(CreateCommand(new HideField { FieldId = 1 })); + }); + } + + [Fact] + public void HideField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.HideField(CreateCommand(new HideField { FieldId = 1 })); + + Assert.True(sut.State.SchemaDef.FieldsById[1].IsHidden); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new FieldHidden { FieldId = fieldId }) + ); + } + + [Fact] + public void ShowField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); + }); + } + + [Fact] + public void ShowField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); + }); + } + + [Fact] + public void ShowField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.HideField(CreateCommand(new HideField { FieldId = 1 })); + sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); + + Assert.False(sut.State.SchemaDef.FieldsById[1].IsHidden); + + sut.GetUncomittedEvents().Skip(1) + .ShouldHaveSameEvents( + CreateEvent(new FieldShown { FieldId = fieldId }) + ); + } + + [Fact] + public void DisableField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); + }); + } + + [Fact] + public void DisableField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); + }); + } + + [Fact] + public void DisableField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); + + Assert.True(sut.State.SchemaDef.FieldsById[1].IsDisabled); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new FieldDisabled { FieldId = fieldId }) + ); + } + + [Fact] + public void EnableField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); + }); + } + + [Fact] + public void EnableField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); + }); + } + + [Fact] + public void EnableField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); + sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); + + Assert.False(sut.State.SchemaDef.FieldsById[1].IsDisabled); + + sut.GetUncomittedEvents().Skip(1) + .ShouldHaveSameEvents( + CreateEvent(new FieldEnabled { FieldId = fieldId }) + ); + } + + [Fact] + public void DeleteField_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); + }); + } + + [Fact] + public void DeleteField_should_throw_exception_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); + }); + } + + [Fact] + public void DeleteField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); + + Assert.False(sut.State.SchemaDef.FieldsById.ContainsKey(1)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new FieldDeleted { FieldId = fieldId }) + ); + } + + private void CreateField() + { + sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); + sut.ClearUncommittedEvents(); + } + + private void CreateSchema() + { + sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); + sut.ClearUncommittedEvents(); + } + + private void PublishSchema() + { + sut.Publish(CreateCommand(new PublishSchema())); + sut.ClearUncommittedEvents(); + } + + private void DeleteSchema() + { + sut.Delete(CreateCommand(new DeleteSchema())); + sut.ClearUncommittedEvents(); + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } + + private static StringFieldProperties InvalidProperties() + { + return new StringFieldProperties { MinLength = 20, MaxLength = 10 }; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index c97d25f1c..80e4db5e7 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -8,9 +8,11 @@ using System; using System.Threading.Tasks; +using FakeItEasy; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; #pragma warning disable IDE0019 // Use pattern matching @@ -74,6 +76,8 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers protected Guid SchemaId { get; } = Guid.NewGuid(); + protected abstract Guid Id { get; } + protected string AppName { get; } = "my-app"; protected string SchemaName { get; } = "my-schema"; @@ -102,6 +106,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { handler.Init(domainObject); + await domainObject.ActivateAsync(Id.ToString(), A.Fake()); await action(domainObject); if (!handler.IsCreated && shouldCreate) @@ -114,6 +119,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { handler.Init(domainObject); + await domainObject.ActivateAsync(Id.ToString(), A.Fake()); await action(domainObject); if (!handler.IsUpdated && shouldUpdate) From 03a5fbe09b66a146576bfa9e8c2a6e83572a1cd1 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 21:19:24 +0100 Subject: [PATCH 18/30] Mongo Queries fixed --- .../Contents/MongoContentEntity.cs | 3 ++- .../Contents/MongoContentRepository.cs | 25 ++++++++++++++++--- .../Apps/State/AppState.cs | 3 ++- .../Contents/QueryContext.cs | 4 +-- .../Contents/State/ContentState.cs | 4 ++- .../Controllers/Content/ContentsController.cs | 2 +- src/Squidex/Config/Domain/WriteServices.cs | 1 - 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index c1b815624..f5127af9e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -85,7 +85,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonJson] public IdContentData DataByIds { get; set; } - NamedContentData IContentEntity.Data + [BsonIgnore] + public NamedContentData Data { get { return data; } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 720cab81b..b2e894f4f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -50,6 +50,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents protected override async Task SetupCollectionAsync(IMongoCollection collection) { + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.Id) + .Ascending(x => x.Version)); + await collection.Indexes.CreateOneAsync( Index .Ascending(x => x.Id) @@ -84,12 +89,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents DocumentId = documentId, DataText = idData?.ToFullText(), DataByIds = idData, + IsLatest = true, ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef), }); try { await Collection.InsertOneAsync(document); + + await Collection.UpdateManyAsync(x => x.Id == value.Id && x.Version < value.Version, Update.Set(x => x.IsLatest, false)); } catch (MongoWriteException ex) { @@ -115,13 +123,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var id = Guid.Parse(key); - var existing = + var contentEntity = await Collection.Find(x => x.Id == id && x.IsLatest) .FirstOrDefaultAsync(); - if (existing != null) + if (contentEntity != null) { - return (SimpleMapper.Map(existing, new ContentState()), existing.Version); + var schema = await appProvider.GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId); + + if (schema == null) + { + throw new InvalidOperationException($"Cannot find schema {contentEntity.SchemaId}"); + } + + contentEntity?.ParseData(schema.SchemaDef); + + return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); } return (null, EtagVersion.NotFound); @@ -212,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) { var contentEntity = - await Collection.Find(x => x.Id == id && x.Version == version) + await Collection.Find(x => x.Id == id && x.Version >= version).SortBy(x => x.Version) .FirstOrDefaultAsync(); contentEntity?.ParseData(schema.SchemaDef); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 34988ff0f..35ed457a5 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -17,7 +17,8 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps.State { - public class AppState : DomainObjectState, IAppEntity + public class AppState : DomainObjectState, + IAppEntity { private static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs index fc5ccd4ea..01288799e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); + return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); } public async Task> GetReferencedContentsAsync(Guid schemaId, ICollection ids) @@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); + return ids.Select(cachedContents.GetOrDefault).Where(x => x != null).ToList(); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 356137ea9..f3d09dca4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -16,7 +16,9 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Contents.State { - public class ContentState : DomainObjectState, IContentEntity + public class ContentState : DomainObjectState, + IContentEntity, + IUpdateableEntityWithAppRef { [JsonProperty] public NamedContentData Data { get; set; } diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index c01ee14ec..875b451cc 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -151,7 +151,7 @@ namespace Squidex.Areas.Api.Controllers.Contents Response.Headers["ETag"] = new StringValues(version.ToString()); - return Ok(response); + return Ok(response.Data); } [MustBeAppEditor] diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 56dba6359..6a910e333 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.State.SchemaDefs; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; using Squidex.Pipeline.CommandMiddlewares; From 9c15648fd37ddd1885235abae56927a9af0ac485 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 22:30:37 +0100 Subject: [PATCH 19/30] Generic methods improved. --- .../Apps/MongoAppEntity.cs | 3 +- .../Apps/MongoAppRepository.cs | 15 +- .../Assets/MongoAssetEntity.cs | 3 +- .../Assets/MongoAssetRepository.cs | 10 +- .../Contents/MongoContentRepository.cs | 12 +- .../Rules/MongoRuleEntity.cs | 2 +- .../Rules/MongoRuleRepository.cs | 6 +- .../Schemas/MongoSchemaEntity.cs | 2 +- .../Schemas/MongoSchemaRepository.cs | 6 +- .../AppProvider.cs | 16 +- .../Apps/AppDomainObject.cs | 2 +- .../Assets/AssetDomainObject.cs | 2 +- .../Contents/ContentDomainObject.cs | 2 +- .../Rules/RuleDomainObject.cs | 2 +- .../Schemas/SchemaDomainObject.cs | 2 +- .../States/MongoSnapshotStore.cs | 12 +- .../States/MongoState.cs | 4 +- .../Commands/AggregateHandler.cs | 4 +- .../Commands/CommandExtensions.cs | 28 +-- .../Commands/DomainObjectBase.cs | 18 +- .../Commands/IDomainObject.cs | 3 +- .../Grains/EventConsumerGrain.cs | 6 +- .../States/IPersistence.cs | 4 + .../States/ISnapshotStore.cs | 6 +- .../States/IStateFactory.cs | 13 +- .../States/IStatefulObject.cs | 4 +- src/Squidex.Infrastructure/States/IStore.cs | 8 +- .../States/Persistence.cs | 210 +--------------- .../States/Persistence{TOwner,TState,TKey}.cs | 232 ++++++++++++++++++ .../States/StateFactory.cs | 38 ++- src/Squidex.Infrastructure/States/Store.cs | 26 +- .../States/StoreExtensions.cs | 32 +-- .../Tasks/TaskExtensions.cs | 11 + src/Squidex/Config/Domain/StoreServices.cs | 15 +- .../TestHelpers/HandlerTestBase.cs | 4 +- .../Commands/AggregateHandlerTests.cs | 10 +- .../Commands/DomainObjectBaseTests.cs | 8 +- .../Grains/EventConsumerGrainTests.cs | 4 +- .../States/StateEventSourcingTests.cs | 18 +- .../States/StateSnapshotTests.cs | 38 +-- .../TestHelpers/MyDomainObject.cs | 2 +- 41 files changed, 437 insertions(+), 406 deletions(-) create mode 100644 src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs index bee8118bb..e5b359397 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Entities.Apps.State; @@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 1c65bb489..77fec730e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Apps { - public sealed class MongoAppRepository : MongoRepositoryBase, IAppRepository, ISnapshotStore + public sealed class MongoAppRepository : MongoRepositoryBase, IAppRepository, ISnapshotStore { public MongoAppRepository(IMongoDatabase database) : base(database) @@ -55,16 +55,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } - public async Task> QueryUserAppNamesAsync(string userId) - { - var appEntities = - await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)) - .ToListAsync(); - - return appEntities.Select(x => x.Id).ToList(); - } - - public async Task<(AppState Value, long Version)> ReadAsync(string key) + public async Task<(AppState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -78,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return (null, EtagVersion.NotFound); } - public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, AppState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index fb6af615b..923669eb0 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Entities.Assets.State; @@ -17,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index a470fe74b..440d82e3f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public sealed class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, ISnapshotStore + public sealed class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, ISnapshotStore { public MongoAssetRepository(IMongoDatabase database) : base(database) @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .Descending(x => x.State.LastModified)); } - public async Task<(AssetState Value, long Version)> ReadAsync(string key) + public async Task<(AssetState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task FindAssetAsync(Guid id) { - var (state, etag) = await ReadAsync(id.ToString()); + var (state, etag) = await ReadAsync(id); return state; } @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets if (ids != null && ids.Count > 0) { - filters.Add(Filter.In(x => x.Id, ids.Select(x => x.ToString()))); + filters.Add(Filter.In(x => x.Id, ids)); } if (mimeTypes != null && mimeTypes.Count > 0) @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return filter; } - public async Task WriteAsync(string key, AssetState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index b2e894f4f..ca185a3c0 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public partial class MongoContentRepository : MongoRepositoryBase, IEventConsumer, IContentRepository, - ISnapshotStore + ISnapshotStore { private readonly IAppProvider appProvider; @@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); } - public async Task WriteAsync(string key, ContentState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) { - var documentId = $"{key}_{oldVersion}"; + var documentId = $"{key}_{newVersion}"; var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId); @@ -119,12 +119,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task<(ContentState Value, long Version)> ReadAsync(string key) + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) { - var id = Guid.Parse(key); - var contentEntity = - await Collection.Find(x => x.Id == id && x.IsLatest) + await Collection.Find(x => x.Id == key && x.IsLatest) .FirstOrDefaultAsync(); if (contentEntity != null) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs index c1ba83cbe..1eeba5a13 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index fe6a69c05..4ad5b8bd8 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { - public sealed class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, ISnapshotStore + public sealed class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, ISnapshotStore { public MongoRuleRepository(IMongoDatabase database) : base(database) @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); } - public async Task<(RuleState Value, long Version)> ReadAsync(string key) + public async Task<(RuleState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } - public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, RuleState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs index 3efb1a83f..a6b75820c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 23ccea311..5f373c18c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { - public sealed class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISnapshotStore + public sealed class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISnapshotStore { public MongoSchemaRepository(IMongoDatabase database) : base(database) @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); } - public async Task<(SchemaState Value, long Version)> ReadAsync(string key) + public async Task<(SchemaState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } - public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, SchemaState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index dd9fc35ee..0f35e958f 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -48,14 +48,14 @@ namespace Squidex.Domain.Apps.Entities public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - var app = await stateFactory.GetSingleAsync(appId.ToString()); + var app = await stateFactory.GetSingleAsync(appId); if (IsNotFound(app)) { return (null, null); } - var schema = await stateFactory.GetSingleAsync(id.ToString()); + var schema = await stateFactory.GetSingleAsync(id); return IsNotFound(false, schema) ? (null, null) : (app.State, schema.State); } @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities { var appId = await GetAppIdAsync(appName); - var app = await stateFactory.GetSingleAsync(appId.ToString()); + var app = await stateFactory.GetSingleAsync(appId); return IsNotFound(app) ? null : app.State; } @@ -73,14 +73,14 @@ namespace Squidex.Domain.Apps.Entities { var schemaId = await GetSchemaIdAsync(appId, name); - var schema = await stateFactory.GetSingleAsync(schemaId.ToString()); + var schema = await stateFactory.GetSingleAsync(schemaId); return IsNotFound(provideDeleted, schema) ? null : schema.State; } public async Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false) { - var schema = await stateFactory.GetSingleAsync(id.ToString()); + var schema = await stateFactory.GetSingleAsync(id); return IsNotFound(provideDeleted, schema) ? null : schema.State; } @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities var schemas = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return schemas.Select(a => (ISchemaEntity)a.State).ToList(); } @@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities var rules = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return rules.Select(a => (IRuleEntity)a.State).ToList(); } @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities var apps = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return apps.Select(a => (IAppEntity)a.State).ToList(); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index d39102795..fc8ddec2e 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppDomainObject : DomainObjectBase + public class AppDomainObject : DomainObjectBase { public AppDomainObject Create(CreateApp command) { diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index 52677b5c4..b9bd87fb4 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetDomainObject : DomainObjectBase + public class AssetDomainObject : DomainObjectBase { public AssetDomainObject Create(CreateAsset command) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 6284d11a7..fe4114a0e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentDomainObject : DomainObjectBase + public class ContentDomainObject : DomainObjectBase { public ContentDomainObject Create(CreateContent command) { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index 6aa105077..7ce3be03d 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDomainObject : DomainObjectBase + public class RuleDomainObject : DomainObjectBase { public void Create(CreateRule command) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index 73becbd1b..15aa08588 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas { - public class SchemaDomainObject : DomainObjectBase + public class SchemaDomainObject : DomainObjectBase { private readonly FieldRegistry registry; diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index c6f6cee6b..8628d75ec 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem { private readonly JsonSerializer serializer; @@ -30,10 +30,10 @@ namespace Squidex.Infrastructure.States return $"States_{typeof(T).Name}"; } - public async Task<(T Value, long Version)> ReadAsync(string key) + public async Task<(T Value, long Version)> ReadAsync(TKey key) { var existing = - await Collection.Find(x => x.Id == key) + await Collection.Find(x => Equals(x.Id, key)) .FirstOrDefaultAsync(); if (existing != null) @@ -44,11 +44,11 @@ namespace Squidex.Infrastructure.States return (default(T), EtagVersion.NotFound); } - public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) + public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion) { try { - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + await Collection.UpdateOneAsync(x => Equals(x.Id, key) && x.Version == oldVersion, Update .Set(x => x.Doc, value) .Set(x => x.Version, newVersion), @@ -59,7 +59,7 @@ namespace Squidex.Infrastructure.States if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + await Collection.Find(x => Equals(x.Id, key)).Only(x => x.Id, x => x.Version) .FirstOrDefaultAsync(); if (existingVersion != null) diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index 448bb45f6..6f73b1043 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -12,12 +12,12 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public sealed class MongoState + public sealed class MongoState { [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public TKey Id { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index c51c651bf..cdfb05cd6 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -67,7 +67,7 @@ namespace Squidex.Infrastructure.Commands var domainCommand = GetCommand(context); var domainObjectId = domainCommand.AggregateId; - var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); + var domainObject = await stateFactory.CreateAsync(domainObjectId); if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) { @@ -102,7 +102,7 @@ namespace Squidex.Infrastructure.Commands using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) { - var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + var domainObject = await stateFactory.GetSingleAsync(domainObjectId); if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) { diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 2c5d721e5..f6d174866 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -16,42 +16,22 @@ namespace Squidex.Infrastructure.Commands { public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject { - return handler.CreateAsync(context, x => - { - creator(x); - - return TaskHelper.Done; - }); + return handler.CreateAsync(context, creator.ToAsync()); } public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject { - return handler.UpdateAsync(context, x => - { - updater(x); - - return TaskHelper.Done; - }); + return handler.UpdateAsync(context, updater.ToAsync()); } public static Task CreateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject { - return handler.CreateSyncedAsync(context, x => - { - creator(x); - - return TaskHelper.Done; - }); + return handler.CreateSyncedAsync(context, creator.ToAsync()); } public static Task UpdateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject { - return handler.UpdateSyncedAsync(context, x => - { - updater(x); - - return TaskHelper.Done; - }); + return handler.UpdateSyncedAsync(context, updater.ToAsync()); } public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index c5800711f..b8351396c 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -15,26 +15,26 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectBase : IDomainObject where TState : IDomainState, new() + public abstract class DomainObjectBase : IDomainObject where T : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private Guid id; - private TState state; - private IPersistence persistence; + private T state; + private IPersistence persistence; public long Version { get { return state.Version; } } - public TState State + public T State { get { return state; } } protected DomainObjectBase() { - state = new TState(); + state = new T(); state.Version = EtagVersion.Empty; } @@ -48,11 +48,11 @@ namespace Squidex.Infrastructure.Commands uncomittedEvents.Clear(); } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(Guid key, IStore store) { - id = Guid.Parse(key); + id = key; - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(); } @@ -73,7 +73,7 @@ namespace Squidex.Infrastructure.Commands uncomittedEvents.Add(@event.To()); } - public void UpdateState(TState newState) + public void UpdateState(T newState) { state = newState; } diff --git a/src/Squidex.Infrastructure/Commands/IDomainObject.cs b/src/Squidex.Infrastructure/Commands/IDomainObject.cs index f1df5c41e..5877dd2e1 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainObject.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainObject.cs @@ -6,13 +6,14 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public interface IDomainObject : IStatefulObject + public interface IDomainObject : IStatefulObject { long Version { get; } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index eff3b2f72..ef4cd9978 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.EventSourcing.Grains { - public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber + public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber { private readonly IEventDataFormatter eventDataFormatter; private readonly IEventStore eventStore; @@ -49,9 +49,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(); } diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 462cf5cc3..8e9e24ba2 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -12,6 +12,10 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { + public interface IPersistence : IPersistence + { + } + public interface IPersistence { long Version { get; } diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 38d62d737..094b375ad 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -10,10 +10,10 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.States { - public interface ISnapshotStore + public interface ISnapshotStore { - Task WriteAsync(string key, T value, long oldVersion, long newVersion); + Task WriteAsync(TKey key, T value, long oldVersion, long newVersion); - Task<(T Value, long Version)> ReadAsync(string key); + Task<(T Value, long Version)> ReadAsync(TKey key); } } diff --git a/src/Squidex.Infrastructure/States/IStateFactory.cs b/src/Squidex.Infrastructure/States/IStateFactory.cs index b76180150..68a99e1a2 100644 --- a/src/Squidex.Infrastructure/States/IStateFactory.cs +++ b/src/Squidex.Infrastructure/States/IStateFactory.cs @@ -6,14 +6,23 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Infrastructure.States { public interface IStateFactory { - Task GetSingleAsync(string key) where T : IStatefulObject; + Task GetSingleAsync(string key) where T : IStatefulObject; - Task CreateAsync(string key) where T : IStatefulObject; + Task GetSingleAsync(Guid key) where T : IStatefulObject; + + Task GetSingleAsync(TKey key) where T : IStatefulObject; + + Task CreateAsync(string key) where T : IStatefulObject; + + Task CreateAsync(Guid key) where T : IStatefulObject; + + Task CreateAsync(TKey key) where T : IStatefulObject; } } diff --git a/src/Squidex.Infrastructure/States/IStatefulObject.cs b/src/Squidex.Infrastructure/States/IStatefulObject.cs index 0c1cbd9cd..b033bb435 100644 --- a/src/Squidex.Infrastructure/States/IStatefulObject.cs +++ b/src/Squidex.Infrastructure/States/IStatefulObject.cs @@ -10,8 +10,8 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.States { - public interface IStatefulObject + public interface IStatefulObject { - Task ActivateAsync(string key, IStore store); + Task ActivateAsync(TKey key, IStore store); } } diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs index 72017044c..ab9feb98f 100644 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ b/src/Squidex.Infrastructure/States/IStore.cs @@ -12,12 +12,12 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public interface IStore + public interface IStore { - IPersistence WithEventSourcing(string key, Func, Task> applyEvent); + IPersistence WithEventSourcing(TKey key, Func, Task> applyEvent); - IPersistence WithSnapshots(string key, Func applySnapshot); + IPersistence WithSnapshots(TKey key, Func applySnapshot); - IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent); + IPersistence WithSnapshotsAndEventSourcing(TKey key, Func applySnapshot, Func, Task> applyEvent); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 4e633c967..7a6738d92 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -7,8 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; @@ -16,217 +14,17 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal sealed class Persistence : IPersistence + internal sealed class Persistence : Persistence, IPersistence { - private readonly string ownerKey; - private readonly ISnapshotStore snapshotStore; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventDataFormatter eventDataFormatter; - private readonly PersistenceMode persistenceMode; - private readonly Action invalidate; - private readonly Func applyState; - private readonly Func, Task> applyEvent; - private long versionSnapshot = EtagVersion.Empty; - private long versionEvents = EtagVersion.Empty; - private long version; - - public long Version - { - get { return version; } - } - - public Persistence(string ownerKey, + public Persistence(TKey ownerKey, Action invalidate, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, + ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, - PersistenceMode persistenceMode, - Func applyState, Func, Task> applyEvent) + : base(ownerKey, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) { - this.ownerKey = ownerKey; - this.applyState = applyState; - this.applyEvent = applyEvent; - this.invalidate = invalidate; - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.persistenceMode = persistenceMode; - this.snapshotStore = snapshotStore; - this.streamNameResolver = streamNameResolver; - } - - public async Task ReadAsync(long expectedVersion = EtagVersion.Any) - { - versionSnapshot = EtagVersion.Empty; - versionEvents = EtagVersion.Empty; - - await ReadSnapshotAsync(); - await ReadEventsAsync(); - - UpdateVersion(); - - if (expectedVersion != EtagVersion.Any && expectedVersion != version) - { - if (version == EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); - } - else - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), version, expectedVersion); - } - } - } - - private async Task ReadSnapshotAsync() - { - if (UseSnapshots()) - { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); - - if (position < EtagVersion.Empty) - { - position = EtagVersion.Empty; - } - - versionSnapshot = position; - versionEvents = position; - - if (applyState != null && position >= 0) - { - await applyState(state); - } - } - } - - private async Task ReadEventsAsync() - { - if (UseEventSourcing()) - { - var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); - - foreach (var @event in events) - { - versionEvents++; - - if (@event.EventStreamNumber != versionEvents) - { - throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); - } - - var parsedEvent = ParseKnownEvent(@event); - - if (parsedEvent != null && applyEvent != null) - { - await applyEvent(parsedEvent); - } - } - } - } - - public async Task WriteSnapshotAsync(TState state) - { - var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - - if (newVersion != versionSnapshot) - { - try - { - await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); - } - catch (InconsistentStateException ex) - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); - } - - versionSnapshot = newVersion; - } - - UpdateVersion(); - - invalidate?.Invoke(); - } - - public async Task WriteEventsAsync(IEnumerable> events) - { - Guard.NotNull(events, nameof(@events)); - - var eventArray = events.ToArray(); - - if (eventArray.Length > 0) - { - var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; - - var commitId = Guid.NewGuid(); - - var eventStream = GetStreamName(); - var eventData = GetEventData(eventArray, commitId); - - try - { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); - } - catch (WrongEventVersionException ex) - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); - } - - versionEvents += eventArray.Length; - } - - UpdateVersion(); - - invalidate?.Invoke(); - } - - private EventData[] GetEventData(Envelope[] events, Guid commitId) - { - return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray(); - } - - private string GetStreamName() - { - return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey); - } - - private bool UseSnapshots() - { - return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private bool UseEventSourcing() - { - return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private Envelope ParseKnownEvent(StoredEvent storedEvent) - { - try - { - return eventDataFormatter.Parse(storedEvent.Data); - } - catch (TypeNameNotFoundException) - { - return null; - } - } - - private void UpdateVersion() - { - if (persistenceMode == PersistenceMode.Snapshots) - { - version = versionSnapshot; - } - else if (persistenceMode == PersistenceMode.EventSourcing) - { - version = versionEvents; - } - else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) - { - version = Math.Max(versionEvents, versionSnapshot); - } } } } diff --git a/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs new file mode 100644 index 000000000..ccc80d3aa --- /dev/null +++ b/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs @@ -0,0 +1,232 @@ +// ========================================================================== +// Persistence.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; + +#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement + +namespace Squidex.Infrastructure.States +{ + internal class Persistence : IPersistence + { + private readonly TKey ownerKey; + private readonly ISnapshotStore snapshotStore; + private readonly IStreamNameResolver streamNameResolver; + private readonly IEventStore eventStore; + private readonly IEventDataFormatter eventDataFormatter; + private readonly PersistenceMode persistenceMode; + private readonly Action invalidate; + private readonly Func applyState; + private readonly Func, Task> applyEvent; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; + private long version; + + public long Version + { + get { return version; } + } + + public Persistence(TKey ownerKey, + Action invalidate, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISnapshotStore snapshotStore, + IStreamNameResolver streamNameResolver, + PersistenceMode persistenceMode, + Func applyState, + Func, Task> applyEvent) + { + this.ownerKey = ownerKey; + this.applyState = applyState; + this.applyEvent = applyEvent; + this.invalidate = invalidate; + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.persistenceMode = persistenceMode; + this.snapshotStore = snapshotStore; + this.streamNameResolver = streamNameResolver; + } + + public async Task ReadAsync(long expectedVersion = EtagVersion.Any) + { + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; + + await ReadSnapshotAsync(); + await ReadEventsAsync(); + + UpdateVersion(); + + if (expectedVersion != EtagVersion.Any && expectedVersion != version) + { + if (version == EtagVersion.Empty) + { + throw new DomainObjectNotFoundException(ownerKey.ToString(), typeof(TOwner)); + } + else + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), version, expectedVersion); + } + } + } + + private async Task ReadSnapshotAsync() + { + if (UseSnapshots()) + { + var (state, position) = await snapshotStore.ReadAsync(ownerKey); + + if (position < EtagVersion.Empty) + { + position = EtagVersion.Empty; + } + + versionSnapshot = position; + versionEvents = position; + + if (applyState != null && position >= 0) + { + await applyState(state); + } + } + } + + private async Task ReadEventsAsync() + { + if (UseEventSourcing()) + { + var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); + + foreach (var @event in events) + { + versionEvents++; + + if (@event.EventStreamNumber != versionEvents) + { + throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + } + + var parsedEvent = ParseKnownEvent(@event); + + if (parsedEvent != null && applyEvent != null) + { + await applyEvent(parsedEvent); + } + } + } + } + + public async Task WriteSnapshotAsync(TState state) + { + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; + + if (newVersion != versionSnapshot) + { + try + { + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); + } + catch (InconsistentStateException ex) + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + } + + versionSnapshot = newVersion; + } + + UpdateVersion(); + + invalidate?.Invoke(); + } + + public async Task WriteEventsAsync(IEnumerable> events) + { + Guard.NotNull(events, nameof(@events)); + + var eventArray = events.ToArray(); + + if (eventArray.Length > 0) + { + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; + + var commitId = Guid.NewGuid(); + + var eventStream = GetStreamName(); + var eventData = GetEventData(eventArray, commitId); + + try + { + await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); + } + catch (WrongEventVersionException ex) + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + } + + versionEvents += eventArray.Length; + } + + UpdateVersion(); + + invalidate?.Invoke(); + } + + private EventData[] GetEventData(Envelope[] events, Guid commitId) + { + return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray(); + } + + private string GetStreamName() + { + return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey.ToString()); + } + + private bool UseSnapshots() + { + return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private bool UseEventSourcing() + { + return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private Envelope ParseKnownEvent(StoredEvent storedEvent) + { + try + { + return eventDataFormatter.Parse(storedEvent.Data); + } + catch (TypeNameNotFoundException) + { + return null; + } + } + + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; + } + else if (persistenceMode == PersistenceMode.EventSourcing) + { + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); + } + } + } +} diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index a2a9ce661..9ac202d72 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -27,12 +27,12 @@ namespace Squidex.Infrastructure.States private readonly object lockObject = new object(); private IDisposable pubSubSubscription; - public sealed class ObjectHolder where T : IStatefulObject + public sealed class ObjectHolder where T : IStatefulObject { private readonly Task activationTask; private readonly T obj; - public ObjectHolder(T obj, string key, IStore store) + public ObjectHolder(T obj, TKey key, IStore store) { this.obj = obj; @@ -81,11 +81,21 @@ namespace Squidex.Infrastructure.States }); } - public async Task CreateAsync(string key) where T : IStatefulObject + public Task CreateAsync(string key) where T : IStatefulObject + { + return CreateAsync(key); + } + + public Task CreateAsync(Guid key) where T : IStatefulObject + { + return CreateAsync(key); + } + + public async Task CreateAsync(TKey key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); var state = (T)services.GetService(typeof(T)); await state.ActivateAsync(key, stateStore); @@ -93,25 +103,35 @@ namespace Squidex.Infrastructure.States return state; } - public Task GetSingleAsync(string key) where T : IStatefulObject + public Task GetSingleAsync(string key) where T : IStatefulObject + { + return GetSingleAsync(key); + } + + public Task GetSingleAsync(Guid key) where T : IStatefulObject + { + return GetSingleAsync(key); + } + + public Task GetSingleAsync(TKey key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); lock (lockObject) { - if (statesCache.TryGetValue>(key, out var stateObj)) + if (statesCache.TryGetValue>(key, out var stateObj)) { return stateObj.ActivateAsync(); } var state = (T)services.GetService(typeof(T)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => { - pubSub.Publish(new InvalidateMessage { Key = key }, false); + pubSub.Publish(new InvalidateMessage { Key = key.ToString() }, false); }); - stateObj = new ObjectHolder(state, key, stateStore); + stateObj = new ObjectHolder(state, key, stateStore); statesCache.CreateEntry(key) .SetValue(stateObj) diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 578762dc5..e200a9257 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public sealed class Store : IStore + internal sealed class Store : IStore { private readonly Action invalidate; private readonly IServiceProvider services; @@ -34,28 +34,32 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public IPersistence WithEventSourcing(string key, Func, Task> applyEvent) + public IPersistence WithSnapshots(TKey key, Func applySnapshot) { - return CreatePersistence(key, PersistenceMode.EventSourcing, null, applyEvent); + return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); } - public IPersistence WithSnapshots(string key, Func applySnapshot) + public IPersistence WithSnapshotsAndEventSourcing(TKey key, Func applySnapshot, Func, Task> applyEvent) { - return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); + return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } - public IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent) + public IPersistence WithEventSourcing(TKey key, Func, Task> applyEvent) { - return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + Guard.NotDefault(key, nameof(key)); + + var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); } - private IPersistence CreatePersistence(string key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) + private IPersistence CreatePersistence(TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) { - Guard.NotNullOrEmpty(key, nameof(key)); + Guard.NotDefault(key, nameof(key)); - var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } } } diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 9fd7a8945..53c2fa929 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -14,39 +14,19 @@ namespace Squidex.Infrastructure.States { public static class StoreExtensions { - public static IPersistence WithEventSourcing(this IStore store, string key, Action> applyEvent) + public static IPersistence WithEventSourcing(this IStore store, TKey key, Action> applyEvent) { - return store.WithEventSourcing(key, x => - { - applyEvent(x); - - return TaskHelper.Done; - }); + return store.WithEventSourcing(key, applyEvent.ToAsync()); } - public static IPersistence WithSnapshots(this IStore store, string key, Action applySnapshot) + public static IPersistence WithSnapshots(this IStore store, TKey key, Action applySnapshot) { - return store.WithSnapshots(key, x => - { - applySnapshot(x); - - return TaskHelper.Done; - }); + return store.WithSnapshots(key, applySnapshot.ToAsync()); } - public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, string key, Action applySnapshot, Action> applyEvent) + public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Action applySnapshot, Action> applyEvent) { - return store.WithSnapshotsAndEventSourcing(key, x => - { - applySnapshot(x); - - return TaskHelper.Done; - }, x => - { - applyEvent(x); - - return TaskHelper.Done; - }); + return store.WithSnapshotsAndEventSourcing(key, applySnapshot.ToAsync(), applyEvent.ToAsync()); } } } diff --git a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index 944e6db4c..e8cb83234 100644 --- a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Infrastructure.Tasks @@ -15,5 +16,15 @@ namespace Squidex.Infrastructure.Tasks public static void Forget(this Task task) { } + + public static Func ToAsync(this Action action) + { + return x => + { + action(x); + + return TaskHelper.Done; + }; + } } } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 06a5257c7..6f743c789 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using IdentityServer4.Stores; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Identity; @@ -64,8 +65,8 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As>() + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) + .As>() .As(); services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) @@ -93,27 +94,27 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) .As() - .As>() + .As>() .As() .As(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 80e4db5e7..b3b48dc28 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { handler.Init(domainObject); - await domainObject.ActivateAsync(Id.ToString(), A.Fake()); + await domainObject.ActivateAsync(Id, A.Fake>()); await action(domainObject); if (!handler.IsCreated && shouldCreate) @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { handler.Init(domainObject); - await domainObject.ActivateAsync(Id.ToString(), A.Fake()); + await domainObject.ActivateAsync(Id, A.Fake>()); await action(domainObject); if (!handler.IsUpdated && shouldUpdate) diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index 943e02bd8..805d24005 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Infrastructure.Commands { private readonly ISemanticLog log = A.Fake(); private readonly IServiceProvider serviceProvider = A.Fake(); - private readonly IStore store = A.Fake(); + private readonly IStore store = A.Fake>(); private readonly IStateFactory stateFactory = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly Envelope event1 = new Envelope(new MyEvent()); @@ -40,18 +40,18 @@ namespace Squidex.Infrastructure.Commands command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any }; context = new CommandContext(command); - A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) + A.CallTo(() => store.WithSnapshots(domainObjectId, A>.Ignored)) .Returns(persistence); - A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) + A.CallTo(() => stateFactory.CreateAsync(domainObjectId)) .Returns(Task.FromResult(domainObject)); - A.CallTo(() => stateFactory.GetSingleAsync(domainObjectId.ToString())) + A.CallTo(() => stateFactory.GetSingleAsync(domainObjectId)) .Returns(Task.FromResult(domainObject)); sut = new AggregateHandler(stateFactory, serviceProvider, log); - domainObject.ActivateAsync(domainObjectId.ToString(), store).Wait(); + domainObject.ActivateAsync(domainObjectId, store).Wait(); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 539cd839f..9d6478232 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -21,14 +21,14 @@ namespace Squidex.Infrastructure.Commands { public class DomainObjectBaseTests { - private readonly IStore store = A.Fake(); + private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); private readonly Guid id = Guid.NewGuid(); private readonly MyDomainObject sut = new MyDomainObject(); public DomainObjectBaseTests() { - A.CallTo(() => store.WithSnapshots(id.ToString(), A>.Ignored)) + A.CallTo(() => store.WithSnapshots(id, A>.Ignored)) .Returns(persistence); } @@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_write_state_and_events_when_saved() { - await sut.ActivateAsync(id.ToString(), store); + await sut.ActivateAsync(id, store); var event1 = new MyEvent(); var event2 = new MyEvent(); @@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .Throws(new InvalidOperationException()); - await sut.ActivateAsync(id.ToString(), store); + await sut.ActivateAsync(id, store); var event1 = new MyEvent(); var event2 = new MyEvent(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 0fa7113b7..55a908e72 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private readonly IEventSubscription eventSubscription = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly ISemanticLog log = A.Fake(); - private readonly IStore store = A.Fake(); + private readonly IStore store = A.Fake>(); private readonly IEventDataFormatter formatter = A.Fake(); private readonly EventData eventData = new EventData(); private readonly Envelope envelope = new Envelope(new MyEvent()); @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains consumerName = eventConsumer.GetType().Name; - A.CallTo(() => store.WithSnapshots(consumerName, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(consumerName, A>.Ignored)) .Invokes(new Action>((key, a) => apply = a)) .Returns(persistence); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index 0ed94cac8..eae17945f 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -22,10 +22,10 @@ namespace Squidex.Infrastructure.States { public class StateEventSourcingTests { - private class MyStatefulObject : IStatefulObject + private class MyStatefulObject : IStatefulObject { private readonly List appliedEvents = new List(); - private IPersistence persistence; + private IPersistence persistence; public long ExpectedVersion { get; set; } @@ -34,9 +34,9 @@ namespace Squidex.Infrastructure.States get { return appliedEvents; } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload)); + persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload)); return persistence.ReadAsync(ExpectedVersion); } @@ -47,15 +47,15 @@ namespace Squidex.Infrastructure.States } } - private class MyStatefulObjectWithSnapshot : IStatefulObject + private class MyStatefulObjectWithSnapshot : IStatefulObject { private IPersistence persistence; public long ExpectedVersion { get; set; } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); + persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); return persistence.ReadAsync(ExpectedVersion); } @@ -69,7 +69,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.States .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot))) .Returns(statefulObjectWithSnapShot); - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key)) diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index 2465e5e53..f003c6ef6 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.States { public class StateSnapshotTests : IDisposable { - private class MyStatefulObject : IStatefulObject + private class MyStatefulObject : IStatefulObject { private IPersistence persistence; private int state; @@ -38,9 +38,9 @@ namespace Squidex.Infrastructure.States get { return state; } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(ExpectedVersion); } @@ -63,7 +63,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.States { A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .Returns(statefulObject); - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); @@ -91,7 +91,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 1)); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -107,7 +107,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, EtagVersion.NotFound)); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Equal(-1, statefulObject.Version); Assert.Equal( 0, statefulObject.State); @@ -121,7 +121,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2)); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -143,7 +143,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); - await sut.GetSingleAsync(key); + await sut.GetSingleAsync(key); } [Fact] @@ -151,7 +151,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -162,12 +162,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject1 = await sut.GetSingleAsync(key); + var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); - var actualObject2 = await sut.GetSingleAsync(key); + var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); @@ -178,12 +178,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject1 = await sut.CreateAsync(key); + var actualObject1 = await sut.CreateAsync(key); Assert.Same(statefulObject, actualObject1); Assert.Null(cache.Get(key)); - var actualObject2 = await sut.CreateAsync(key); + var actualObject2 = await sut.CreateAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Twice); @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.Equal(123, statefulObject.State); @@ -231,7 +231,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); } @@ -241,7 +241,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -260,7 +260,7 @@ namespace Squidex.Infrastructure.States for (var i = 0; i < 1000; i++) { - tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); + tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index 23a0d0917..5ee760e7b 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Infrastructure.TestHelpers { - internal sealed class MyDomainObject : DomainObjectBase + internal sealed class MyDomainObject : DomainObjectBase { } } From 2db4ab91d3940c40f5253a44a304201f4b60fe6a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 11 Dec 2017 09:02:05 +0100 Subject: [PATCH 20/30] Migrator. --- Squidex.sln | 2 +- .../AppProvider.cs | 1 - .../Schemas/SchemaCommandMiddleware.cs | 1 - .../Migrations/MongoMigrationEntity.cs | 29 +++++ .../Migrations/MongoMigrationStatus.cs | 66 +++++++++++ .../MongoDb/MongoRepositoryBase.cs | 2 +- .../Migrations/IMigration.cs | 21 ++++ .../Migrations/IMigrationStatus.cs | 21 ++++ .../Migrations/Migrator.cs | 100 ++++++++++++++++ src/Squidex/Config/Domain/ReadServices.cs | 1 - src/Squidex/Config/Domain/StoreServices.cs | 5 + src/Squidex/Config/Domain/SystemExtensions.cs | 8 ++ src/Squidex/WebStartup.cs | 1 + .../Schemas/Guards/GuardSchemaTests.cs | 2 - .../Migrations/MigratorTests.cs | 111 ++++++++++++++++++ .../Migrate_00.csproj} | 0 tools/{Migrate_01 => Migrate_00}/Program.cs | 2 +- 17 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs create mode 100644 src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs create mode 100644 src/Squidex.Infrastructure/Migrations/IMigration.cs create mode 100644 src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs create mode 100644 src/Squidex.Infrastructure/Migrations/Migrator.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs rename tools/{Migrate_01/Migrate_01.csproj => Migrate_00/Migrate_00.csproj} (100%) rename tools/{Migrate_01 => Migrate_00}/Program.cs (99%) diff --git a/Squidex.sln b/Squidex.sln index 4d49d682e..cb23f22ff 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -36,7 +36,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.Goog EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "migrations", "migrations", "{94207AA6-4923-4183-A558-E0F8196B8CA3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{B51126A8-0D75-4A79-867D-10724EC6AC84}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_00", "tools\Migrate_00\Migrate_00.csproj", "{B51126A8-0D75-4A79-867D-10724EC6AC84}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Shared", "src\Squidex.Shared\Squidex.Shared.csproj", "{5E75AB7D-6F01-4313-AFF1-7F7128FFD71F}" EndProject diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 0f35e958f..55c030912 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs index cb5e13e38..bcb2c632d 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs @@ -9,7 +9,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Guards; using Squidex.Infrastructure; diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs new file mode 100644 index 000000000..1a7161f68 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// MongoMigrationEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Infrastructure.Migrations +{ + public sealed class MongoMigrationEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public bool IsLocked { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs new file mode 100644 index 000000000..e81626185 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// MongoMigrationStatus.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.Migrations +{ + public sealed class MongoMigrationStatus : MongoRepositoryBase, IMigrationStatus + { + private const string DefaultId = "Default"; + + public MongoMigrationStatus(IMongoDatabase database) + : base(database) + { + } + + public override void Connect() + { + base.Connect(); + } + + public async Task GetVersionAsync() + { + var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(); + + if (entity == null) + { + try + { + await Collection.InsertOneAsync(new MongoMigrationEntity { Id = DefaultId }); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category != ServerErrorCategory.DuplicateKey) + { + throw; + } + } + } + + return entity?.Version ?? 0; + } + + public async Task TryLockAsync() + { + var entity = await Collection.FindOneAndUpdateAsync(x => x.Id == DefaultId, Update.Set(x => x.IsLocked, true)); + + return entity?.IsLocked == false; + } + + public Task UnlockAsync(int newVersion) + { + return Collection.UpdateOneAsync(x => x.Id == DefaultId, + Update + .Set(x => x.IsLocked, false) + .Set(x => x.Version, newVersion)); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 056dd8595..5183bc696 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -106,7 +106,7 @@ namespace Squidex.Infrastructure.MongoDb } } - public void Connect() + public virtual void Connect() { try { diff --git a/src/Squidex.Infrastructure/Migrations/IMigration.cs b/src/Squidex.Infrastructure/Migrations/IMigration.cs new file mode 100644 index 000000000..5d6fa7a6a --- /dev/null +++ b/src/Squidex.Infrastructure/Migrations/IMigration.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IMigration.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Migrations +{ + public interface IMigration + { + int FromVersion { get; } + + int ToVersion { get; } + + Task UpdateAsync(); + } +} diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs b/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs new file mode 100644 index 000000000..96f7e2043 --- /dev/null +++ b/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IMigrationState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Migrations +{ + public interface IMigrationStatus + { + Task GetVersionAsync(); + + Task TryLockAsync(); + + Task UnlockAsync(int newVersion); + } +} diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs new file mode 100644 index 000000000..0be44904a --- /dev/null +++ b/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Migrator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Migrations +{ + public sealed class Migrator + { + private readonly IMigrationStatus migrationStatus; + private readonly IEnumerable migrations; + private readonly ISemanticLog log; + + public Migrator(IMigrationStatus migrationStatus, IEnumerable migrations, ISemanticLog log) + { + Guard.NotNull(migrationStatus, nameof(migrationStatus)); + Guard.NotNull(migrations, nameof(migrations)); + Guard.NotNull(log, nameof(log)); + + this.migrationStatus = migrationStatus; + this.migrations = migrations.OrderByDescending(x => x.ToVersion).ToList(); + + this.log = log; + } + + public async Task MigrateAsync() + { + var version = await migrationStatus.GetVersionAsync(); + + var lastMigrator = migrations.FirstOrDefault(); + + if (lastMigrator != null && lastMigrator.ToVersion != version) + { + while (!await migrationStatus.TryLockAsync()) + { + log.LogInformation(w => w + .WriteProperty("action", "Migrate") + .WriteProperty("mesage", "Waiting 5sec to acquire lock.")); + + await Task.Delay(5000); + } + + try + { + var migrationPath = FindMigratorPath(version, lastMigrator.ToVersion).ToList(); + + foreach (var migrator in migrationPath) + { + using (log.MeasureInformation(w => w + .WriteProperty("action", "Migration") + .WriteProperty("migrator", migrator.GetType().ToString()))) + { + await migrator.UpdateAsync(); + + version = migrator.ToVersion; + } + } + } + finally + { + await migrationStatus.UnlockAsync(version); + } + } + } + + private IEnumerable FindMigratorPath(int fromVersion, int toVersion) + { + var addedMigrators = new HashSet(); + + while (true) + { + var bestMigrator = migrations.Where(x => x.FromVersion < x.ToVersion).FirstOrDefault(x => x.FromVersion == fromVersion); + + if (bestMigrator != null && addedMigrators.Add(bestMigrator)) + { + fromVersion = bestMigrator.ToVersion; + + yield return bestMigrator; + } + else if (fromVersion != toVersion) + { + throw new InvalidOperationException($"There is no migration path from {fromVersion} to {toVersion}."); + } + else + { + break; + } + } + } + } +} diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index d11421a78..2fd571e14 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -17,7 +17,6 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.GraphQL; diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 6f743c789..416402e05 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -39,6 +39,7 @@ 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; using Squidex.Shared.Users; @@ -65,6 +66,10 @@ namespace Squidex.Config.Domain .As() .As(); + services.AddSingletonAs(c => new MongoMigrationStatus(mongoDatabase)) + .As() + .As(); + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) .As>() .As(); diff --git a/src/Squidex/Config/Domain/SystemExtensions.cs b/src/Squidex/Config/Domain/SystemExtensions.cs index ed8a4bccd..95538ef4c 100644 --- a/src/Squidex/Config/Domain/SystemExtensions.cs +++ b/src/Squidex/Config/Domain/SystemExtensions.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure; +using Squidex.Infrastructure.Migrations; namespace Squidex.Config.Domain { @@ -24,5 +25,12 @@ namespace Squidex.Config.Domain system.Connect(); } } + + public static void Migrate(this IServiceProvider services) + { + var migrator = services.GetRequiredService(); + + migrator.MigrateAsync().Wait(); + } } } diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 2abdbadc3..fef509b0b 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -40,6 +40,7 @@ namespace Squidex { app.ApplicationServices.LogConfiguration(); app.ApplicationServices.TestExternalSystems(); + app.ApplicationServices.Migrate(); app.UseMyCors(); app.UseMyForwardingRules(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs index 326149595..be2bf2775 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -12,8 +12,6 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure; using Xunit; diff --git a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs new file mode 100644 index 000000000..adf71d3a5 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -0,0 +1,111 @@ +// ========================================================================== +// MigratorTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.Log; +using Xunit; + +namespace Squidex.Infrastructure.Migrations +{ + public sealed class MigratorTests + { + private readonly IMigrationStatus status = A.Fake(); + + public MigratorTests() + { + A.CallTo(() => status.GetVersionAsync()).Returns(0); + A.CallTo(() => status.TryLockAsync()).Returns(true); + } + + [Fact] + public async Task Should_migrate_step_by_step() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var migrator = new Migrator(status, new[] { migrator_0_1, migrator_1_2, migrator_2_3 }, A.Fake()); + + await migrator.MigrateAsync(); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); + + A.CallTo(() => status.UnlockAsync(3)).MustHaveHappened(); + } + + [Fact] + public async Task Should_unlock_when_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var migrator = new Migrator(status, new[] { migrator_0_1, migrator_1_2, migrator_2_3 }, A.Fake()); + + A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); + + await Assert.ThrowsAsync(migrator.MigrateAsync); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); + + A.CallTo(() => status.UnlockAsync(1)).MustHaveHappened(); + } + + [Fact] + public async Task Should_migrate_with_fastest_path() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_0_2 = BuildMigration(0, 2); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); + + var migrator = new Migrator(status, new[] { migrator_0_1, migrator_0_2, migrator_1_2, migrator_2_3 }, A.Fake()); + + await migrator.MigrateAsync(); + + A.CallTo(() => migrator_0_2.UpdateAsync()).MustHaveHappened(); + A.CallTo(() => migrator_0_1.UpdateAsync()).MustNotHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync()).MustNotHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustHaveHappened(); + + A.CallTo(() => status.UnlockAsync(3)).MustHaveHappened(); + } + + [Fact] + public async Task Should_throw_if_no_path_found() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_2_3 = BuildMigration(2, 3); + + var migrator = new Migrator(status, new[] { migrator_0_1, migrator_2_3 }, A.Fake()); + + await Assert.ThrowsAsync(migrator.MigrateAsync); + + A.CallTo(() => migrator_0_1.UpdateAsync()).MustNotHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync()).MustNotHaveHappened(); + + A.CallTo(() => status.UnlockAsync(0)).MustHaveHappened(); + } + + private IMigration BuildMigration(int fromVersion, int toVersion) + { + var migration = A.Fake(); + + A.CallTo(() => migration.FromVersion).Returns(fromVersion); + A.CallTo(() => migration.ToVersion).Returns(toVersion); + + return migration; + } + } +} diff --git a/tools/Migrate_01/Migrate_01.csproj b/tools/Migrate_00/Migrate_00.csproj similarity index 100% rename from tools/Migrate_01/Migrate_01.csproj rename to tools/Migrate_00/Migrate_00.csproj diff --git a/tools/Migrate_01/Program.cs b/tools/Migrate_00/Program.cs similarity index 99% rename from tools/Migrate_01/Program.cs rename to tools/Migrate_00/Program.cs index 522763767..1429070b3 100644 --- a/tools/Migrate_01/Program.cs +++ b/tools/Migrate_00/Program.cs @@ -10,7 +10,7 @@ using System; using MongoDB.Bson; using MongoDB.Driver; -namespace Migrate_01 +namespace Migrate_00 { public class Program { From ee713b7bb40f55848e0b87c948800d46aa55db07 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 11 Dec 2017 21:21:21 +0100 Subject: [PATCH 21/30] Cleanup --- tests/Benchmarks/Benchmark.cs | 31 - tests/Benchmarks/Benchmarks.csproj | 26 - tests/Benchmarks/Program.cs | 96 --- .../Benchmarks/Properties/launchSettings.json | 7 - tests/Benchmarks/Services.cs | 145 ---- tests/Benchmarks/Tests/AppendToEventStore.cs | 53 -- .../AppendToEventStoreWithManyWriters.cs | 52 -- tests/Benchmarks/Tests/HandleEvents.cs | 63 -- .../Tests/HandleEventsWithManyWriters.cs | 70 -- tests/Benchmarks/Tests/ReadSchemaState.cs | 102 --- tests/Benchmarks/Tests/TestData/MyAppState.cs | 42 -- tests/Benchmarks/Tests/TestData/MyEvent.cs | 19 - .../Tests/TestData/MyEventConsumer.cs | 79 -- tests/Benchmarks/Utils/Helper.cs | 21 - .../Apps/AppEventTests.cs | 51 -- .../Contents/ContentEventTests.cs | 70 -- .../Contents/{ => GraphQL}/GraphQLTests.cs | 3 +- .../Contents/{ => OData}/ODataQueryTests.cs | 2 +- .../Apps/ConfigAppLimitsProviderTests.cs | 150 ---- .../Apps/NoopAppPlanBillingManagerTests.cs | 38 - .../Contents/ContentQueryServiceTests.cs | 220 ------ .../Contents/GraphQLTests.cs | 689 ------------------ .../Contents/ODataQueryTests.cs | 392 ---------- .../Contents/TestData/FakeAssetEntity.cs | 50 -- .../Contents/TestData/FakeContentEntity.cs | 33 - .../Contents/TestData/FakeUrlGenerator.cs | 40 - .../Rules/RuleDequeuerTests.cs | 99 --- .../Rules/RuleEnqueuerTests.cs | 102 --- .../Squidex.Domain.Apps.Read.Tests.csproj | 37 - .../Apps/AppCommandMiddlewareTests.cs | 244 ------- .../Apps/AppDomainObjectTests.cs | 288 -------- .../Apps/AppEventTests.cs | 51 -- .../Apps/Guards/GuardAppClientsTests.cs | 142 ---- .../Apps/Guards/GuardAppContributorsTests.cs | 158 ---- .../Apps/Guards/GuardAppLanguagesTests.cs | 131 ---- .../Apps/Guards/GuardAppTests.cs | 120 --- .../Assets/AssetCommandMiddlewareTests.cs | 139 ---- .../Assets/AssetDomainObjectTests.cs | 213 ------ .../Assets/Guards/GuardAssetTests.cs | 65 -- .../Contents/ContentCommandMiddlewareTests.cs | 247 ------- .../Contents/ContentDomainObjectTests.cs | 280 ------- .../Contents/ContentEventTests.cs | 70 -- .../Contents/ContentVersionLoaderTests.cs | 140 ---- .../Contents/Guard/GuardContentTests.cs | 99 --- .../Guards/Actions/WebhookActionTests.cs | 48 -- .../Rules/Guards/GuardRuleTests.cs | 168 ----- .../Triggers/ContentChangedTriggerTests.cs | 84 --- .../Rules/RuleCommandMiddlewareTests.cs | 117 --- .../Rules/RuleDomainObjectTests.cs | 240 ------ .../AssetsFieldPropertiesTests.cs | 104 --- .../BooleanFieldPropertiesTests.cs | 34 - .../DateTimeFieldPropertiesTests.cs | 126 ---- .../GeolocationFieldPropertiesTests.cs | 34 - .../JsonFieldPropertiesTests.cs | 27 - .../NumberFieldPropertiesTests.cs | 134 ---- .../ReferencesFieldPropertiesTests.cs | 34 - .../StringFieldPropertiesTests.cs | 105 --- .../TagsFieldPropertiesTests.cs | 34 - .../Schemas/Guards/GuardSchemaFieldTests.cs | 247 ------- .../Schemas/Guards/GuardSchemaTests.cs | 201 ----- .../Schemas/SchemaCommandMiddlewareTests.cs | 278 ------- .../Schemas/SchemaDomainObjectTests.cs | 656 ----------------- .../Squidex.Domain.Apps.Write.Tests.csproj | 29 - .../TestHelpers/AssertHelper.cs | 45 -- .../TestHelpers/HandlerTestBase.cs | 162 ---- 65 files changed, 2 insertions(+), 8074 deletions(-) delete mode 100644 tests/Benchmarks/Benchmark.cs delete mode 100644 tests/Benchmarks/Benchmarks.csproj delete mode 100644 tests/Benchmarks/Program.cs delete mode 100644 tests/Benchmarks/Properties/launchSettings.json delete mode 100644 tests/Benchmarks/Services.cs delete mode 100644 tests/Benchmarks/Tests/AppendToEventStore.cs delete mode 100644 tests/Benchmarks/Tests/AppendToEventStoreWithManyWriters.cs delete mode 100644 tests/Benchmarks/Tests/HandleEvents.cs delete mode 100644 tests/Benchmarks/Tests/HandleEventsWithManyWriters.cs delete mode 100644 tests/Benchmarks/Tests/ReadSchemaState.cs delete mode 100644 tests/Benchmarks/Tests/TestData/MyAppState.cs delete mode 100644 tests/Benchmarks/Tests/TestData/MyEvent.cs delete mode 100644 tests/Benchmarks/Tests/TestData/MyEventConsumer.cs delete mode 100644 tests/Benchmarks/Utils/Helper.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs rename tests/Squidex.Domain.Apps.Entities.Tests/Contents/{ => GraphQL}/GraphQLTests.cs (99%) rename tests/Squidex.Domain.Apps.Entities.Tests/Contents/{ => OData}/ODataQueryTests.cs (99%) delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Apps/NoopAppPlanBillingManagerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/AssertHelper.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/HandlerTestBase.cs diff --git a/tests/Benchmarks/Benchmark.cs b/tests/Benchmarks/Benchmark.cs deleted file mode 100644 index 2bf549ceb..000000000 --- a/tests/Benchmarks/Benchmark.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// IBenchmark.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Benchmarks -{ - public abstract class Benchmark - { - public virtual void Initialize() - { - } - - public virtual void RunInitialize() - { - } - - public virtual void RunCleanup() - { - } - - public virtual void Cleanup() - { - } - - public abstract long Run(); - } -} diff --git a/tests/Benchmarks/Benchmarks.csproj b/tests/Benchmarks/Benchmarks.csproj deleted file mode 100644 index e8ecbf1d7..000000000 --- a/tests/Benchmarks/Benchmarks.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - Exe - netcoreapp2.0 - - - - - - - - - - - - - - - - C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.dependencyinjection\2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll - - - - ..\..\Squidex.ruleset - - diff --git a/tests/Benchmarks/Program.cs b/tests/Benchmarks/Program.cs deleted file mode 100644 index c04991e0f..000000000 --- a/tests/Benchmarks/Program.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Program.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Benchmarks.Tests; - -namespace Benchmarks -{ - public static class Program - { - private static readonly List<(string Name, Benchmark Benchmark)> Benchmarks = new Benchmark[] - { - new AppendToEventStore(), - new AppendToEventStoreWithManyWriters(), - new HandleEvents(), - new HandleEventsWithManyWriters(), - new ReadSchemaState() - }.Select(x => (x.GetType().Name, x)).ToList(); - - public static void Main(string[] args) - { - var name = "ReadSchemaState"; - - var selected = Benchmarks.Find(x => x.Name == name); - - if (selected.Benchmark == null) - { - Console.WriteLine($"'{name}' is not a valid benchmark, please try: "); - - foreach (var b in Benchmarks) - { - Console.WriteLine($" * {b.Name}"); - } - } - else - { - const int numRuns = 3; - - try - { - var elapsed = 0d; - var count = 0L; - - Console.WriteLine($"{selected.Name}: Initialized"); - - selected.Benchmark.Initialize(); - - for (var run = 0; run < numRuns; run++) - { - try - { - selected.Benchmark.RunInitialize(); - - var watch = Stopwatch.StartNew(); - - count += selected.Benchmark.Run(); - - watch.Stop(); - - elapsed += watch.ElapsedMilliseconds; - - Console.WriteLine($"{selected.Name}: Run {run + 1} finished"); - } - finally - { - selected.Benchmark.RunCleanup(); - } - } - - var averageElapsed = TimeSpan.FromMilliseconds(elapsed / numRuns); - var averageSeconds = Math.Round(count / (numRuns * averageElapsed.TotalSeconds), 2); - - Console.WriteLine($"{selected.Name}: Completed after {averageElapsed}, {averageSeconds} items/s"); - } - catch (Exception e) - { - Console.WriteLine($"Benchmark failed with '{e.Message}'"); - } - finally - { - selected.Benchmark.Cleanup(); - } - } - - Console.ReadLine(); - } - } -} diff --git a/tests/Benchmarks/Properties/launchSettings.json b/tests/Benchmarks/Properties/launchSettings.json deleted file mode 100644 index 05fb17fa5..000000000 --- a/tests/Benchmarks/Properties/launchSettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "profiles": { - "Benchmarks": { - "commandName": "Project" - } - } -} \ No newline at end of file diff --git a/tests/Benchmarks/Services.cs b/tests/Benchmarks/Services.cs deleted file mode 100644 index 1c7a770d0..000000000 --- a/tests/Benchmarks/Services.cs +++ /dev/null @@ -1,145 +0,0 @@ -// ========================================================================== -// Services.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Benchmarks.Tests.TestData; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using MongoDB.Driver; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using NodaTime; -using NodaTime.Serialization.JsonNet; -using Squidex.Domain.Apps.Core.Apps.Json; -using Squidex.Domain.Apps.Core.Rules.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.States; - -namespace Benchmarks -{ - public static class Services - { - public static IServiceProvider Create() - { - var services = new ServiceCollection(); - - services.AddSingleton(CreateTypeNameRegistry()); - - services.AddSingleton(); - - services.AddTransient(); - - services.AddSingleton( - new MongoClient("mongodb://localhost")); - - services.AddSingleton( - new SemanticLog(new ILogChannel[0], new ILogAppender[0], () => new JsonLogWriter())); - - services.AddSingleton( - new MemoryCache(Options.Create(new MemoryCacheOptions()))); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(c => - JsonSerializer.Create(c.GetRequiredService())); - - services.AddSingleton(c => - CreateJsonSerializerSettings(c.GetRequiredService(), c.GetRequiredService())); - - services.AddSingleton(c => - c.GetRequiredService().GetDatabase(Guid.NewGuid().ToString())); - - return services.BuildServiceProvider(); - } - - public static void Cleanup(this IServiceProvider services) - { - var mongoClient = services.GetRequiredService(); - var mongoDatabase = services.GetRequiredService(); - - mongoClient.DropDatabase(mongoDatabase.DatabaseNamespace.DatabaseName); - - if (services is IDisposable disposable) - { - disposable.Dispose(); - } - } - - private static TypeNameRegistry CreateTypeNameRegistry() - { - var result = new TypeNameRegistry(); - - result.Map(typeof(MyEvent)); - - return result; - } - - private static JsonSerializerSettings CreateJsonSerializerSettings(TypeNameRegistry typeNameRegistry, FieldRegistry fieldRegistry) - { - var settings = new JsonSerializerSettings(); - - settings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - - settings.ContractResolver = new ConverterContractResolver( - new AppClientsConverter(), - new AppContributorsConverter(), - new ClaimsPrincipalConverter(), - new InstantConverter(), - new LanguageConverter(), - new LanguagesConfigConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new PropertiesBagConverter(), - new PropertiesBagConverter(), - new RefTokenConverter(), - new RuleConverter(), - new SchemaConverter(fieldRegistry), - new StringEnumConverter()); - - settings.NullValueHandling = NullValueHandling.Ignore; - - settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; - settings.DateParseHandling = DateParseHandling.None; - - settings.TypeNameHandling = TypeNameHandling.Auto; - - settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - - BsonJsonConvention.Register(JsonSerializer.Create(settings)); - - return settings; - } - } -} diff --git a/tests/Benchmarks/Tests/AppendToEventStore.cs b/tests/Benchmarks/Tests/AppendToEventStore.cs deleted file mode 100644 index 679330ade..000000000 --- a/tests/Benchmarks/Tests/AppendToEventStore.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// AppendToEventStore.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Benchmarks.Utils; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.EventSourcing; - -namespace Benchmarks.Tests -{ - public sealed class AppendToEventStore : Benchmark - { - private IServiceProvider services; - private IEventStore eventStore; - - public override void RunInitialize() - { - services = Services.Create(); - - eventStore = services.GetRequiredService(); - } - - public override long Run() - { - const long numCommits = 100; - const long numStreams = 20; - - for (var streamId = 0; streamId < numStreams; streamId++) - { - var eventOffset = -1; - var streamName = streamId.ToString(); - - for (var commitId = 0; commitId < numCommits; commitId++) - { - eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, eventOffset, new[] { Helper.CreateEventData() }).Wait(); - eventOffset++; - } - } - - return numCommits * numStreams; - } - - public override void RunCleanup() - { - services.Cleanup(); - } - } -} diff --git a/tests/Benchmarks/Tests/AppendToEventStoreWithManyWriters.cs b/tests/Benchmarks/Tests/AppendToEventStoreWithManyWriters.cs deleted file mode 100644 index 93ca46661..000000000 --- a/tests/Benchmarks/Tests/AppendToEventStoreWithManyWriters.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// AppendToEventStoreWithManyWriters.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Benchmarks.Utils; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.EventSourcing; - -namespace Benchmarks.Tests -{ - public sealed class AppendToEventStoreWithManyWriters : Benchmark - { - private IServiceProvider services; - private IEventStore eventStore; - - public override void RunInitialize() - { - services = Services.Create(); - - eventStore = services.GetRequiredService(); - } - - public override long Run() - { - const long numCommits = 200; - const long numStreams = 100; - - Parallel.For(0, numStreams, streamId => - { - var streamName = streamId.ToString(); - - for (var commitId = 0; commitId < numCommits; commitId++) - { - eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, new[] { Helper.CreateEventData() }).Wait(); - } - }); - - return numCommits * numStreams; - } - - public override void RunCleanup() - { - services.Cleanup(); - } - } -} diff --git a/tests/Benchmarks/Tests/HandleEvents.cs b/tests/Benchmarks/Tests/HandleEvents.cs deleted file mode 100644 index d49c37b7b..000000000 --- a/tests/Benchmarks/Tests/HandleEvents.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// HandleEvents.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Benchmarks.Tests.TestData; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.States; - -namespace Benchmarks.Tests -{ - public sealed class HandleEvents : Benchmark - { - private const int NumEvents = 5000; - private IServiceProvider services; - private IEventStore eventStore; - private IEventDataFormatter eventDataFormatter; - private EventConsumerGrain eventConsumerGrain; - private MyEventConsumer eventConsumer; - - public override void RunInitialize() - { - services = Services.Create(); - - eventConsumer = new MyEventConsumer(NumEvents); - - eventStore = services.GetRequiredService(); - - eventDataFormatter = services.GetRequiredService(); - eventConsumerGrain = services.GetRequiredService(); - - eventConsumerGrain.ActivateAsync("Test", services.GetRequiredService()).Wait(); - eventConsumerGrain.Activate(eventConsumer); - } - - public override long Run() - { - var streamName = Guid.NewGuid().ToString(); - - for (var eventId = 0; eventId < NumEvents; eventId++) - { - var eventData = eventDataFormatter.ToEventData(new Envelope(new MyEvent { EventNumber = eventId + 1 }), Guid.NewGuid()); - - eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, eventId - 1, new[] { eventData }).Wait(); - } - - eventConsumer.WaitAndVerify(); - - return NumEvents; - } - - public override void RunCleanup() - { - services.Cleanup(); - } - } -} diff --git a/tests/Benchmarks/Tests/HandleEventsWithManyWriters.cs b/tests/Benchmarks/Tests/HandleEventsWithManyWriters.cs deleted file mode 100644 index c98899c91..000000000 --- a/tests/Benchmarks/Tests/HandleEventsWithManyWriters.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// HandleEventsWithManyWriters.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Benchmarks.Tests.TestData; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.States; - -namespace Benchmarks.Tests -{ - public sealed class HandleEventsWithManyWriters : Benchmark - { - private const int NumCommits = 200; - private const int NumStreams = 10; - private IServiceProvider services; - private IEventStore eventStore; - private IEventDataFormatter eventDataFormatter; - private EventConsumerGrain eventConsumerGrain; - private MyEventConsumer eventConsumer; - - public override void RunInitialize() - { - services = Services.Create(); - - eventConsumer = new MyEventConsumer(NumStreams * NumCommits); - - eventStore = services.GetRequiredService(); - eventDataFormatter = services.GetRequiredService(); - - eventConsumerGrain = services.GetRequiredService(); - - eventConsumerGrain.ActivateAsync("Test", services.GetRequiredService()).Wait(); - eventConsumerGrain.Activate(eventConsumer); - } - - public override long Run() - { - Parallel.For(0, NumStreams, streamId => - { - var eventOffset = -1; - var streamName = streamId.ToString(); - - for (var commitId = 0; commitId < NumCommits; commitId++) - { - var eventData = eventDataFormatter.ToEventData(new Envelope(new MyEvent()), Guid.NewGuid()); - - eventStore.AppendEventsAsync(Guid.NewGuid(), streamName, eventOffset - 1, new[] { eventData }).Wait(); - eventOffset++; - } - }); - - eventConsumer.WaitAndVerify(); - - return NumStreams * NumCommits; - } - - public override void RunCleanup() - { - services.Cleanup(); - } - } -} diff --git a/tests/Benchmarks/Tests/ReadSchemaState.cs b/tests/Benchmarks/Tests/ReadSchemaState.cs deleted file mode 100644 index f02f50ae4..000000000 --- a/tests/Benchmarks/Tests/ReadSchemaState.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// ReadSchemaState.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using Benchmarks.Tests.TestData; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.State.Grains; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; - -namespace Benchmarks.Tests -{ - public class ReadSchemaState : Benchmark - { - private IServiceProvider services; - private MyAppState grain; - - public override void Initialize() - { - services = Services.Create(); - - grain = services.GetRequiredService().GetSynchronizedAsync("DEFAULT").Result; - - var state = new AppStateGrainState - { - App = new JsonAppEntity - { - Id = Guid.NewGuid() - } - }; - - state.Schemas = ImmutableDictionary.Empty; - - for (var i = 1; i <= 100; i++) - { - var schema = new JsonSchemaEntity - { - Id = Guid.NewGuid(), - Created = SystemClock.Instance.GetCurrentInstant(), - CreatedBy = new RefToken("user", "1"), - LastModified = SystemClock.Instance.GetCurrentInstant(), - LastModifiedBy = new RefToken("user", "1"), - SchemaDef = new Schema("Name") - }; - - for (var j = 1; j < 30; j++) - { - schema.SchemaDef = schema.SchemaDef.AddField(new StringField(j, j.ToString(), Partitioning.Invariant)); - } - - state.Schemas = state.Schemas.Add(schema.Id, schema); - } - - state.Rules = ImmutableDictionary.Empty; - - for (var i = 0; i < 100; i++) - { - var rule = new JsonRuleEntity - { - Id = Guid.NewGuid(), - Created = SystemClock.Instance.GetCurrentInstant(), - CreatedBy = new RefToken("user", "1"), - LastModified = SystemClock.Instance.GetCurrentInstant(), - LastModifiedBy = new RefToken("user", "1"), - RuleDef = new Rule(new ContentChangedTrigger(), new WebhookAction()) - }; - - state.Rules = state.Rules.Add(rule.Id, rule); - } - - grain.SetState(state); - grain.WriteStateAsync().Wait(); - } - - public override long Run() - { - for (var i = 0; i < 10; i++) - { - grain.ReadStateAsync().Wait(); - } - - return 10; - } - - public override void Cleanup() - { - services.Cleanup(); - } - } -} diff --git a/tests/Benchmarks/Tests/TestData/MyAppState.cs b/tests/Benchmarks/Tests/TestData/MyAppState.cs deleted file mode 100644 index 1340b9f7d..000000000 --- a/tests/Benchmarks/Tests/TestData/MyAppState.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// MyAppState.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.State.Grains; -using Squidex.Infrastructure.States; - -namespace Benchmarks.Tests.TestData -{ - public sealed class MyAppState : IStatefulObject - { - private IPersistence persistence; - private AppStateGrainState state; - - public Task ActivateAsync(string key, IStore store) - { - persistence = store.WithSnapshots(key, s => state = s); - - return persistence.ReadAsync(); - } - - public void SetState(AppStateGrainState state) - { - this.state = state; - } - - public Task WriteStateAsync() - { - return persistence.WriteSnapshotAsync(state); - } - - public Task ReadStateAsync() - { - return persistence.ReadAsync(); - } - } -} diff --git a/tests/Benchmarks/Tests/TestData/MyEvent.cs b/tests/Benchmarks/Tests/TestData/MyEvent.cs deleted file mode 100644 index a11bd99bd..000000000 --- a/tests/Benchmarks/Tests/TestData/MyEvent.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// MyEvent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Benchmarks.Tests.TestData -{ - [TypeName("MyEvent")] - public sealed class MyEvent : IEvent - { - public int EventNumber { get; set; } - } -} \ No newline at end of file diff --git a/tests/Benchmarks/Tests/TestData/MyEventConsumer.cs b/tests/Benchmarks/Tests/TestData/MyEventConsumer.cs deleted file mode 100644 index 153910c55..000000000 --- a/tests/Benchmarks/Tests/TestData/MyEventConsumer.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ========================================================================== -// MyEventConsumer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Benchmarks.Tests.TestData -{ - public sealed class MyEventConsumer : IEventConsumer - { - private readonly TaskCompletionSource completion = new TaskCompletionSource(); - private readonly int numEvents; - - public List EventNumbers { get; } = new List(); - - public string Name - { - get { return typeof(MyEventConsumer).Name; } - } - - public string EventsFilter - { - get { return string.Empty; } - } - - public MyEventConsumer(int numEvents) - { - this.numEvents = numEvents; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public void WaitAndVerify() - { - completion.Task.Wait(); - - if (EventNumbers.Count != numEvents) - { - throw new InvalidOperationException($"{EventNumbers.Count} Events have been handled"); - } - - for (var i = 0; i < EventNumbers.Count; i++) - { - var value = EventNumbers[i]; - - if (value != i + 1) - { - throw new InvalidOperationException($"Event[{i}] != value"); - } - } - } - - public Task On(Envelope @event) - { - if (@event.Payload is MyEvent myEvent) - { - EventNumbers.Add(myEvent.EventNumber); - - if (myEvent.EventNumber == numEvents) - { - completion.SetResult(true); - } - } - - return TaskHelper.Done; - } - } -} \ No newline at end of file diff --git a/tests/Benchmarks/Utils/Helper.cs b/tests/Benchmarks/Utils/Helper.cs deleted file mode 100644 index 68da44773..000000000 --- a/tests/Benchmarks/Utils/Helper.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Helper.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.EventSourcing; - -namespace Benchmarks.Utils -{ - public static class Helper - { - public static EventData CreateEventData() - { - return new EventData { EventId = Guid.NewGuid(), Metadata = "EventMetdata", Payload = "EventPayload", Type = "MyEvent" }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs deleted file mode 100644 index 7e590590e..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// AppEventTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Apps.Old; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppEventTests - { - private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - - [Fact] - public void Should_migrate_client_changed_as_reader_to_client_updated() - { - var source = CreateEvent(new AppClientChanged { IsReader = true }); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); - } - - [Fact] - public void Should_migrate_client_changed_as_writer_to_client_updated() - { - var source = CreateEvent(new AppClientChanged { IsReader = false }); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); - } - - private T CreateEvent(T contentEvent) where T : AppEvent - { - contentEvent.Actor = actor; - contentEvent.AppId = appId; - - return contentEvent; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs deleted file mode 100644 index 0dec9d79f..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// ContentEventTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Events.Contents.Old; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentEventTests - { - private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); - private readonly Guid contentId = Guid.NewGuid(); - - [Fact] - public void Should_migrate_content_published_to_content_status_changed() - { - var source = CreateEvent(new ContentPublished()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); - } - - [Fact] - public void Should_migrate_content_unpublished_to_content_status_changed() - { - var source = CreateEvent(new ContentUnpublished()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); - } - - [Fact] - public void Should_migrate_content_restored_to_content_status_changed() - { - var source = CreateEvent(new ContentRestored()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); - } - - [Fact] - public void Should_migrate_content_archived_to_content_status_changed() - { - var source = CreateEvent(new ContentArchived()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); - } - - private T CreateEvent(T contentEvent) where T : ContentEvent - { - contentEvent.Actor = actor; - contentEvent.AppId = appId; - contentEvent.SchemaId = schemaId; - contentEvent.ContentId = contentId; - - return contentEvent; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs similarity index 99% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs index abad6c454..62379fac4 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs @@ -23,7 +23,6 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Contents.TestData; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -31,7 +30,7 @@ using Xunit; #pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLTests { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs similarity index 99% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs index 1f4d4d239..3af662a17 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs @@ -26,7 +26,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents.OData { public class ODataQueryTests { diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs deleted file mode 100644 index f81da552b..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// ConfigAppLimitsProviderTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using FakeItEasy; -using FluentAssertions; -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Apps -{ - public class ConfigAppLimitsProviderTests - { - private static readonly ConfigAppLimitsPlan InfinitePlan = new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1 - }; - - private static readonly ConfigAppLimitsPlan FreePlan = new ConfigAppLimitsPlan - { - Id = "free", - Name = "Free", - MaxApiCalls = 50000, - MaxAssetSize = 1024 * 1024 * 10, - MaxContributors = 2 - }; - - private static readonly ConfigAppLimitsPlan BasicPlan = new ConfigAppLimitsPlan - { - Id = "basic", - Name = "Basic", - MaxApiCalls = 150000, - MaxAssetSize = 1024 * 1024 * 2, - MaxContributors = 5 - }; - - private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; - - [Fact] - public void Should_return_plans() - { - var sut = new ConfigAppPlansProvider(Plans); - - Plans.OrderBy(x => x.MaxApiCalls).ShouldBeEquivalentTo(sut.GetAvailablePlans()); - } - - [Theory] - [InlineData(null)] - [InlineData("my-plan")] - public void Should_return_infinite_if_nothing_configured(string planId) - { - var sut = new ConfigAppPlansProvider(Enumerable.Empty()); - - var plan = sut.GetPlanForApp(CreateApp(planId)); - - plan.ShouldBeEquivalentTo(InfinitePlan); - } - - [Fact] - public void Should_return_fitting_app_plan() - { - var sut = new ConfigAppPlansProvider(Plans); - - var plan = sut.GetPlanForApp(CreateApp("basic")); - - plan.ShouldBeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_smallest_plan_if_none_fits() - { - var sut = new ConfigAppPlansProvider(Plans); - - var plan = sut.GetPlanForApp(CreateApp("enterprise")); - - plan.ShouldBeEquivalentTo(FreePlan); - } - - [Fact] - public void Should_return_second_plan_for_upgrade_if_plan_is_null() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgrade(null); - - upgradePlan.ShouldBeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_return_second_plan_for_upgrade_if_plan_not_found() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("enterprise")); - - upgradePlan.ShouldBeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_not_return_plan_for_upgrade_if_plan_is_highest_plan() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("basic")); - - Assert.Null(upgradePlan); - } - - [Fact] - public void Should_return_next_plan_if_plan_is_upgradeable() - { - var sut = new ConfigAppPlansProvider(Plans); - - var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("free")); - - upgradePlan.ShouldBeEquivalentTo(BasicPlan); - } - - [Fact] - public void Should_check_plan_exists() - { - var sut = new ConfigAppPlansProvider(Plans); - - Assert.True(sut.IsConfiguredPlan("basic")); - Assert.True(sut.IsConfiguredPlan("free")); - - Assert.False(sut.IsConfiguredPlan("infinite")); - Assert.False(sut.IsConfiguredPlan("invalid")); - Assert.False(sut.IsConfiguredPlan(null)); - } - - private static IAppEntity CreateApp(string plan) - { - var app = A.Dummy(); - - A.CallTo(() => app.PlanId).Returns(plan); - - return app; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Apps/NoopAppPlanBillingManagerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Apps/NoopAppPlanBillingManagerTests.cs deleted file mode 100644 index 17dd3d413..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Apps/NoopAppPlanBillingManagerTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// NoopAppPlanBillingManagerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Apps -{ - public class NoopAppPlanBillingManagerTests - { - private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); - - [Fact] - public void Should_not_have_portal() - { - Assert.False(sut.HasPortal); - } - - [Fact] - public async Task Should_do_nothing_when_changing_plan() - { - await sut.ChangePlanAsync(null, Guid.Empty, null, null); - } - - [Fact] - public async Task Should_not_return_portal_link() - { - Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs deleted file mode 100644 index e2cea0ad6..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs +++ /dev/null @@ -1,220 +0,0 @@ -// ========================================================================== -// ContentQueryServiceTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.OData.UriParser; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Contents.Edm; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public class ContentQueryServiceTests - { - private readonly IContentRepository contentRepository = A.Fake(); - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly ISchemaEntity schema = A.Fake(); - private readonly IContentEntity content = A.Fake(); - private readonly IAppEntity app = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid contentId = Guid.NewGuid(); - private readonly string appName = "my-app"; - private readonly NamedContentData data = new NamedContentData(); - private readonly NamedContentData transformedData = new NamedContentData(); - private readonly ClaimsPrincipal user; - private readonly ClaimsIdentity identity = new ClaimsIdentity(); - private readonly EdmModelBuilder modelBuilder = A.Fake(); - private readonly ContentQueryService sut; - - public ContentQueryServiceTests() - { - user = new ClaimsPrincipal(identity); - - A.CallTo(() => app.Id).Returns(appId); - A.CallTo(() => app.Name).Returns(appName); - - A.CallTo(() => content.Id).Returns(contentId); - A.CallTo(() => content.Data).Returns(data); - A.CallTo(() => content.Status).Returns(Status.Published); - - sut = new ContentQueryService(contentRepository, appProvider, scriptEngine, modelBuilder); - } - - [Fact] - public async Task Should_return_schema_from_id_if_string_is_guid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) - .Returns(schema); - - var result = await sut.FindSchemaAsync(app, schemaId.ToString()); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_return_schema_from_name_if_string_not_guid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) - .Returns(schema); - - var result = await sut.FindSchemaAsync(app, "my-schema"); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_throw_if_schema_not_found() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) - .Returns((ISchemaEntity)null); - - await Assert.ThrowsAsync(() => sut.FindSchemaAsync(app, "my-schema")); - } - - [Fact] - public async Task Should_return_content_from_repository_and_transform() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) - .Returns(schema); - A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) - .Returns(content); - - A.CallTo(() => schema.ScriptQuery) - .Returns(""); - - A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "")) - .Returns(transformedData); - - var result = await sut.FindContentAsync(app, schemaId.ToString(), user, contentId); - - Assert.Equal(schema, result.Schema); - Assert.Equal(data, result.Content.Data); - Assert.Equal(content.Id, result.Content.Id); - } - - [Fact] - public async Task Should_throw_if_content_to_find_does_not_exist() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) - .Returns(schema); - A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) - .Returns((IContentEntity)null); - - await Assert.ThrowsAsync(async () => await sut.FindContentAsync(app, schemaId.ToString(), user, contentId)); - } - - [Fact] - public async Task Should_return_contents_with_ids_from_repository_and_transform() - { - await TestManyIdRequest(true, false, new HashSet { Guid.NewGuid() }, Status.Draft, Status.Published); - } - - [Fact] - public async Task Should_return_non_archived_contents_from_repository_and_transform() - { - await TestManyRequest(true, false, Status.Draft, Status.Published); - } - - [Fact] - public async Task Should_return_archived_contents_from_repository_and_transform() - { - await TestManyRequest(true, true, Status.Archived); - } - - [Fact] - public async Task Should_return_draft_contents_from_repository_and_transform() - { - await TestManyRequest(false, false, Status.Published); - } - - [Fact] - public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend() - { - await TestManyRequest(false, true, Status.Published); - } - - private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status) - { - SetupClaims(isFrontend); - - SetupFakeWithOdataQuery(status); - SetupFakeWithScripting(); - - var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, string.Empty); - - Assert.Equal(123, result.Total); - Assert.Equal(schema, result.Schema); - Assert.Equal(data, result.Items[0].Data); - Assert.Equal(content.Id, result.Items[0].Id); - } - - private async Task TestManyIdRequest(bool isFrontend, bool archive, HashSet ids, params Status[] status) - { - SetupClaims(isFrontend); - - SetupFakeWithIdQuery(status, ids); - SetupFakeWithScripting(); - - var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, ids); - - Assert.Equal(123, result.Total); - Assert.Equal(schema, result.Schema); - Assert.Equal(data, result.Items[0].Data); - Assert.Equal(content.Id, result.Items[0].Id); - } - - private void SetupClaims(bool isFrontend) - { - if (isFrontend) - { - identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend")); - } - } - - private void SetupFakeWithIdQuery(Status[] status, HashSet ids) - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) - .Returns(schema); - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) - .Returns(new List { content }); - A.CallTo(() => contentRepository.CountAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) - .Returns(123); - } - - private void SetupFakeWithOdataQuery(Status[] status) - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) - .Returns(schema); - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) - .Returns(new List { content }); - A.CallTo(() => contentRepository.CountAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) - .Returns(123); - } - - private void SetupFakeWithScripting() - { - A.CallTo(() => schema.ScriptQuery) - .Returns(""); - - A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "")) - .Returns(transformedData); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs deleted file mode 100644 index db70d4f1f..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs +++ /dev/null @@ -1,689 +0,0 @@ -// ========================================================================== -// GraphQLTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NodaTime.Extensions; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Read.Contents.TestData; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter - -namespace Squidex.Domain.Apps.Read.Contents -{ - public class GraphQLTests - { - private static readonly Guid schemaId = Guid.NewGuid(); - private static readonly Guid appId = Guid.NewGuid(); - private static readonly string appName = "my-app"; - private readonly Schema schemaDef; - private readonly IContentQueryService contentQuery = A.Fake(); - private readonly IAssetRepository assetRepository = A.Fake(); - private readonly ISchemaEntity schema = A.Fake(); - private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAppEntity app = A.Dummy(); - private readonly ClaimsPrincipal user = new ClaimsPrincipal(); - private readonly IGraphQLService sut; - - public GraphQLTests() - { - schemaDef = - new Schema("my-schema") - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())) - .AddField(new StringField(2, "my-string", Partitioning.Language, - new StringFieldProperties())) - .AddField(new NumberField(3, "my-number", Partitioning.Invariant, - new NumberFieldProperties())) - .AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties())) - .AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties())) - .AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId })) - .AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) - .AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())) - .AddField(new TagsField(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties())); - - A.CallTo(() => app.Id).Returns(appId); - A.CallTo(() => app.Name).Returns(appName); - A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE)); - - A.CallTo(() => schema.Id).Returns(schemaId); - A.CallTo(() => schema.Name).Returns(schemaDef.Name); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - A.CallTo(() => schema.IsPublished).Returns(true); - A.CallTo(() => schema.ScriptQuery).Returns(""); - - var allSchemas = new List { schema }; - - A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas); - - sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator()); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_empty_object_for_empty_query(string query) - { - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_return_multiple_assets_when_querying_assets() - { - const string query = @" - query { - queryAssets(search: ""my-query"", top: 30, skip: 5) { - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - } - }"; - - var asset = CreateAsset(Guid.NewGuid()); - - var assets = new List { asset }; - - A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) - .Returns(assets); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryAssets = new dynamic[] - { - new - { - id = asset.Id, - version = 1, - created = asset.Created.ToDateTimeUtc(), - createdBy = "subject:user1", - lastModified = asset.LastModified.ToDateTimeUtc(), - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600 - } - } - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_return_single_asset_when_finding_asset() - { - var assetId = Guid.NewGuid(); - var asset = CreateAsset(Guid.NewGuid()); - - var query = $@" - query {{ - findAsset(id: ""{assetId}"") {{ - id - version - created - createdBy - lastModified - lastModifiedBy - url - thumbnailUrl - sourceUrl - mimeType - fileName - fileSize - fileVersion - isImage - pixelWidth - pixelHeight - }} - }}"; - - A.CallTo(() => assetRepository.FindAssetAsync(assetId)) - .Returns(asset); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findAsset = new - { - id = asset.Id, - version = 1, - created = asset.Created.ToDateTimeUtc(), - createdBy = "subject:user1", - lastModified = asset.LastModified.ToDateTimeUtc(), - lastModifiedBy = "subject:user2", - url = $"assets/{asset.Id}", - thumbnailUrl = $"assets/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = "image/png", - fileName = "MyFile.png", - fileSize = 1024, - fileVersion = 123, - isImage = true, - pixelWidth = 800, - pixelHeight = 600 - } - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_return_multiple_contents_when_querying_contents() - { - const string query = @" - query { - queryMySchemaContents(top: 30, skip: 5) { - id - version - created - createdBy - lastModified - lastModifiedBy - url - data { - myString { - de - } - myNumber { - iv - } - myBoolean { - iv - } - myDatetime { - iv - } - myJson { - iv - } - myGeolocation { - iv - } - myTags { - iv - } - } - } - }"; - - var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - - var contents = new List { content }; - - A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, "?$top=30&$skip=5")) - .Returns((schema, 0L, (IReadOnlyList)contents)); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - queryMySchemaContents = new dynamic[] - { - new - { - id = content.Id, - version = 1, - created = content.Created.ToDateTimeUtc(), - createdBy = "subject:user1", - lastModified = content.LastModified.ToDateTimeUtc(), - lastModifiedBy = "subject:user2", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified.ToDateTimeUtc() - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - } - } - } - } - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_return_single_content_when_finding_content() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty); - - var query = $@" - query {{ - findMySchemaContent(id: ""{contentId}"") {{ - id - version - created - createdBy - lastModified - lastModifiedBy - url - data {{ - myString {{ - de - }} - myNumber {{ - iv - }} - myBoolean {{ - iv - }} - myDatetime {{ - iv - }} - myJson {{ - iv - }} - myGeolocation {{ - iv - }} - myTags {{ - iv - }} - }} - }} - }}"; - - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) - .Returns((schema, content)); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - version = 1, - created = content.Created.ToDateTimeUtc(), - createdBy = "subject:user1", - lastModified = content.LastModified.ToDateTimeUtc(), - lastModifiedBy = "subject:user2", - url = $"contents/my-schema/{content.Id}", - data = new - { - myString = new - { - de = "value" - }, - myNumber = new - { - iv = 1 - }, - myBoolean = new - { - iv = true - }, - myDatetime = new - { - iv = content.LastModified.ToDateTimeUtc() - }, - myJson = new - { - iv = new - { - value = 1 - } - }, - myGeolocation = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - myTags = new - { - iv = new[] - { - "tag1", - "tag2" - } - } - } - } - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() - { - var contentRefId = Guid.NewGuid(); - var contentRef = CreateContent(contentRefId, Guid.Empty, Guid.Empty); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, contentRefId, Guid.Empty); - - var query = $@" - query {{ - findMySchemaContent(id: ""{contentId}"") {{ - id - data {{ - myReferences {{ - iv {{ - id - }} - }} - }} - }} - }}"; - - var refContents = new List { contentRef }; - - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) - .Returns((schema, content)); - - A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) - .Returns((schema, 0L, (IReadOnlyList)refContents)); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myReferences = new - { - iv = new[] - { - new - { - id = contentRefId - } - } - } - } - } - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() - { - var assetRefId = Guid.NewGuid(); - var assetRef = CreateAsset(assetRefId); - - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, assetRefId); - - var query = $@" - query {{ - findMySchemaContent(id: ""{contentId}"") {{ - id - data {{ - myAssets {{ - iv {{ - id - }} - }} - }} - }} - }}"; - - var refAssets = new List { assetRef }; - - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) - .Returns((schema, content)); - - A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) - .Returns(refAssets); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = new - { - findMySchemaContent = new - { - id = content.Id, - data = new - { - myAssets = new - { - iv = new[] - { - new - { - id = assetRefId - } - } - } - } - } - } - }; - - AssertJson(expected, new { data = result.Data }); - } - - [Fact] - public async Task Should_not_return_data_when_field_not_part_of_content() - { - var contentId = Guid.NewGuid(); - var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); - - var query = $@" - query {{ - findMySchemaContent(id: ""{contentId}"") {{ - id - version - created - createdBy - lastModified - lastModifiedBy - url - data {{ - myInvalid {{ - iv - }} - }} - }} - }}"; - - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) - .Returns((schema, content)); - - var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); - - var expected = new - { - data = (object)null - }; - - AssertJson(expected, new { data = result.Data }); - } - - private static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null) - { - var now = DateTime.UtcNow.ToInstant(); - - data = data ?? - new NamedContentData() - .AddField("my-json", - new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 }))) - .AddField("my-string", - new ContentFieldData().AddValue("de", "value")) - .AddField("my-assets", - new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId }))) - .AddField("my-number", - new ContentFieldData().AddValue("iv", 1)) - .AddField("my-boolean", - new ContentFieldData().AddValue("iv", true)) - .AddField("my-datetime", - new ContentFieldData().AddValue("iv", now.ToDateTimeUtc())) - .AddField("my-tags", - new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" }))) - .AddField("my-references", - new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId }))) - .AddField("my-geolocation", - new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); - - var content = new FakeContentEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken("subject", "user1"), - LastModified = now, - LastModifiedBy = new RefToken("subject", "user2"), - Data = data - }; - - return content; - } - - private static IAssetEntity CreateAsset(Guid id) - { - var now = DateTime.UtcNow.ToInstant(); - - var asset = new FakeAssetEntity - { - Id = id, - Version = 1, - Created = now, - CreatedBy = new RefToken("subject", "user1"), - LastModified = now, - LastModifiedBy = new RefToken("subject", "user2"), - FileName = "MyFile.png", - FileSize = 1024, - FileVersion = 123, - MimeType = "image/png", - IsImage = true, - PixelWidth = 800, - PixelHeight = 600 - }; - - return asset; - } - - private static void AssertJson(object expected, object result) - { - var resultJson = JsonConvert.SerializeObject(result, Formatting.Indented); - var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented); - - Assert.Equal(expectJson, resultJson); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs deleted file mode 100644 index 099b61d0f..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs +++ /dev/null @@ -1,392 +0,0 @@ -// ========================================================================== -// ODataQueryTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Microsoft.OData.Edm; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Contents.Edm; -using Squidex.Domain.Apps.Read.MongoDb.Contents; -using Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public class ODataQueryTests - { - private readonly Schema schemaDef; - private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; - private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); - private readonly IEdmModel edmModel; - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - - static ODataQueryTests() - { - InstantSerializer.Register(); - } - - public ODataQueryTests() - { - schemaDef = - new Schema("user") - .AddField(new StringField(1, "firstName", Partitioning.Language, - new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })) - .AddField(new StringField(2, "lastName", Partitioning.Language, - new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })) - .AddField(new BooleanField(3, "isAdmin", Partitioning.Invariant, - new BooleanFieldProperties())) - .AddField(new NumberField(4, "age", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddField(new DateTimeField(5, "birthday", Partitioning.Invariant, - new DateTimeFieldProperties())) - .AddField(new AssetsField(6, "pictures", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new ReferencesField(7, "friends", Partitioning.Invariant, - new ReferencesFieldProperties())) - .AddField(new StringField(8, "dashed-field", Partitioning.Invariant, - new StringFieldProperties())) - .Update(new SchemaProperties { Hints = "The User" }); - - var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); - - var schema = A.Dummy(); - A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); - A.CallTo(() => schema.Version).Returns(3); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - - var app = A.Dummy(); - A.CallTo(() => app.Id).Returns(Guid.NewGuid()); - A.CallTo(() => app.Version).Returns(3); - A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); - - edmModel = builder.BuildEdmModel(schema, app); - } - - [Fact] - public void Should_parse_query() - { - var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); - - Assert.NotNull(parser); - } - - [Fact] - public void Should_make_query_with_underscore_field() - { - var i = F("$filter=data/dashed_field/iv eq 'Value'"); - var o = C("{ 'do.8.iv' : 'Value' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_not_operator() - { - var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')"); - var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_starts_with_query() - { - var i = F("$filter=startswith(data/firstName/de, 'Sebastian')"); - var o = C("{ 'do.1.de' : /^Sebastian/i }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_ends_with_query() - { - var i = F("$filter=endswith(data/firstName/de, 'Sebastian')"); - var o = C("{ 'do.1.de' : /Sebastian$/i }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_contains_query() - { - var i = F("$filter=contains(data/firstName/de, 'Sebastian')"); - var o = C("{ 'do.1.de' : /Sebastian/i }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_contains_query_with_equals() - { - var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true"); - var o = C("{ 'do.1.de' : /Sebastian/i }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_negated_contains_query_with_equals() - { - var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false"); - var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_negated_contains_query_and_other() - { - var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true"); - var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_string_equals_query() - { - var i = F("$filter=data/firstName/de eq 'Sebastian'"); - var o = C("{ 'do.1.de' : 'Sebastian' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_datetime_equals_query() - { - var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z"); - var o = C("{ 'do.5.iv' : ISODate(\"1988-01-19T12:00:00Z\") }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_boolean_equals_query() - { - var i = F("$filter=data/isAdmin/iv eq true"); - var o = C("{ 'do.3.iv' : true }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_string_not_equals_query() - { - var i = F("$filter=data/firstName/de ne 'Sebastian'"); - var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_number_less_than_query() - { - var i = F("$filter=data/age/iv lt 1"); - var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_number_less_equals_query() - { - var i = F("$filter=data/age/iv le 1"); - var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_number_greater_than_query() - { - var i = F("$filter=data/age/iv gt 1"); - var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_number_greater_equals_query() - { - var i = F("$filter=data/age/iv ge 1"); - var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_equals_query_for_assets() - { - var i = F("$filter=data/pictures/iv eq 'guid'"); - var o = C("{ 'do.6.iv' : 'guid' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_equals_query_for_references() - { - var i = F("$filter=data/friends/iv eq 'guid'"); - var o = C("{ 'do.7.iv' : 'guid' }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_and_query() - { - var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2"); - var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_or_query() - { - var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2"); - var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_full_text_query() - { - var i = F("$search=Hello my World"); - var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_create_full_text_query_with_and() - { - var i = F("$search=A and B"); - var o = C("{ '$text' : { '$search' : 'A and B' } }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_convert_orderby_with_single_statements() - { - var i = S("$orderby=data/age/iv desc"); - var o = C("{ 'do.4.iv' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_convert_orderby_with_multiple_statements() - { - var i = S("$orderby=data/age/iv, data/firstName/en desc"); - var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); - - Assert.Equal(o, i); - } - - [Fact] - public void Should_set_top() - { - var parser = edmModel.ParseQuery("$top=3"); - var cursor = A.Fake>(); - - cursor.Take(parser); - - A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); - } - - [Fact] - public void Should_set_max_top_if_larger() - { - var parser = edmModel.ParseQuery("$top=300"); - var cursor = A.Fake>(); - - cursor.Take(parser); - - A.CallTo(() => cursor.Limit(200)).MustHaveHappened(); - } - - [Fact] - public void Should_set_default_top() - { - var parser = edmModel.ParseQuery(string.Empty); - var cursor = A.Fake>(); - - cursor.Take(parser); - - A.CallTo(() => cursor.Limit(20)).MustHaveHappened(); - } - - [Fact] - public void Should_set_skip() - { - var parser = edmModel.ParseQuery("$skip=3"); - var cursor = A.Fake>(); - - cursor.Skip(parser); - - A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); - } - - [Fact] - public void Should_not_set_skip() - { - var parser = edmModel.ParseQuery(string.Empty); - var cursor = A.Fake>(); - - cursor.Take(parser); - - A.CallTo(() => cursor.Skip(A.Ignored)).MustNotHaveHappened(); - } - - private static string C(string value) - { - return value.Replace('\'', '"'); - } - - private string S(string value) - { - var parser = edmModel.ParseQuery(value); - var cursor = A.Fake>(); - - var i = string.Empty; - - A.CallTo(() => cursor.Sort(A>.Ignored)) - .Invokes((SortDefinition sortDefinition) => - { - i = sortDefinition.Render(serializer, registry).ToString(); - }); - - cursor.Sort(parser, schemaDef); - - return i; - } - - private string F(string value) - { - var parser = edmModel.ParseQuery(value); - - var query = FilterBuilder.Build(parser, schemaDef).Render(serializer, registry).ToString(); - - return query; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs deleted file mode 100644 index e1373ad67..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// MockupAssetEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.TestData -{ - public sealed class FakeAssetEntity : IAssetEntity - { - public Guid Id { get; set; } - - public Guid AppId { get; set; } - - public Guid AssetId { get; set; } - - public Instant Created { get; set; } - - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - - public RefToken LastModifiedBy { get; set; } - - public long Version { get; set; } - - public string MimeType { get; set; } - - public string FileName { get; set; } - - public long FileSize { get; set; } - - public long FileVersion { get; set; } - - public bool IsImage { get; set; } - - public bool IsDeleted { get; set; } - - public int? PixelWidth { get; set; } - - public int? PixelHeight { get; set; } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs deleted file mode 100644 index b2dc5cecd..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// FakeContentEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.TestData -{ - public sealed class FakeContentEntity : IContentEntity - { - public Guid Id { get; set; } - public Guid AppId { get; set; } - - public long Version { get; set; } - - public Instant Created { get; set; } - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - public RefToken LastModifiedBy { get; set; } - - public NamedContentData Data { get; set; } - - public Status Status { get; set; } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs deleted file mode 100644 index d501bcd4c..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// FakeUrlGenerator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Read.Schemas; - -namespace Squidex.Domain.Apps.Read.Contents.TestData -{ - public sealed class FakeUrlGenerator : IGraphQLUrlGenerator - { - public bool CanGenerateAssetSourceUrl { get; } = true; - - public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) - { - return $"assets/{asset.Id}"; - } - - public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) - { - return $"assets/{asset.Id}?width=100"; - } - - public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) - { - return $"assets/source/{asset.Id}"; - } - - public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) - { - return $"contents/{schema.Name}/{content.Id}"; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs deleted file mode 100644 index 8a43c6345..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// RuleDequeuerGrainTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Infrastructure.Log; -using Xunit; - -#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void - -namespace Squidex.Domain.Apps.Read.Rules -{ - public class RuleDequeuerTests - { - private readonly IClock clock = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly RuleService ruleService = A.Fake(); - private readonly RuleDequeuer sut; - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - - public RuleDequeuerTests() - { - A.CallTo(() => clock.GetCurrentInstant()).Returns(now); - - sut = new RuleDequeuer( - ruleService, - ruleEventRepository, - log, - clock); - } - - [Theory] - [InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] - [InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] - [InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] - [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] - [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] - [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] - public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) - { - var actionData = new RuleJobData(); - var actionName = "MyAction"; - - var @event = CreateEvent(calls, actionName, actionData); - - var requestElapsed = TimeSpan.FromMinutes(1); - var requestDump = "Dump"; - - A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) - .Returns((requestDump, result, requestElapsed)); - - Instant? nextCall = null; - - if (minutes > 0) - { - nextCall = now.Plus(Duration.FromMinutes(minutes)); - } - - await sut.HandleAsync(@event); - - sut.Dispose(); - - A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, requestDump, result, jobResult, requestElapsed, nextCall)) - .MustHaveHappened(); - } - - private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) - { - var @event = A.Fake(); - - var job = new RuleJob - { - RuleId = Guid.NewGuid(), - ActionData = actionData, - ActionName = actionName, - Created = now - }; - - A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); - A.CallTo(() => @event.Job).Returns(job); - A.CallTo(() => @event.Created).Returns(now); - A.CallTo(() => @event.NumCalls).Returns(numCalls); - - return @event; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs deleted file mode 100644 index 93a598752..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// RuleEnqueuerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Rules -{ - public class RuleEnqueuerTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly RuleService ruleService = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly RuleEnqueuer sut; - - public RuleEnqueuerTests() - { - sut = new RuleEnqueuer( - ruleEventRepository, - appProvider, - ruleService); - } - - [Fact] - public void Should_return_contents_filter_for_events_filter() - { - Assert.Equal(".*", sut.EventsFilter); - } - - [Fact] - public void Should_return_type_name_for_name() - { - Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); - } - - [Fact] - public Task Should_do_nothing_on_clear() - { - return sut.ClearAsync(); - } - - [Fact] - public async Task Should_update_repositories_on_with_jobs_from_sender() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId }); - - var rule1 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); - var rule2 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); - var rule3 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); - - var job1 = new RuleJob { Created = now }; - var job2 = new RuleJob { Created = now }; - - var ruleEntity1 = A.Fake(); - var ruleEntity2 = A.Fake(); - var ruleEntity3 = A.Fake(); - - A.CallTo(() => ruleEntity1.RuleDef).Returns(rule1); - A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); - A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); - - A.CallTo(() => appProvider.GetRulesAsync(appId.Name)) - .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); - - A.CallTo(() => ruleService.CreateJob(rule1, @event)) - .Returns(job1); - - A.CallTo(() => ruleService.CreateJob(rule2, @event)) - .Returns(job2); - - A.CallTo(() => ruleService.CreateJob(rule3, @event)) - .Returns(null); - - await sut.On(@event); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) - .MustHaveHappened(); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) - .MustHaveHappened(); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj deleted file mode 100644 index 8b14abdf4..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - Exe - netcoreapp2.0 - Squidex.Domain.Apps.Read - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs deleted file mode 100644 index 0f2cc3ccb..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs +++ /dev/null @@ -1,244 +0,0 @@ -// ========================================================================== -// AppCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly AppCommandMiddleware sut; - private readonly AppDomainObject app; - private readonly Language language = Language.DE; - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientName = "client"; - - public AppCommandMiddlewareTests() - { - app = new AppDomainObject(AppId, -1); - - A.CallTo(() => appProvider.GetAppAsync(AppName)) - .Returns((IAppEntity)null); - - A.CallTo(() => userResolver.FindByIdAsync(contributorId)) - .Returns(A.Fake()); - - sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); - - await TestCreate(app, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(AppId, context.Result>().IdOrValue); - } - - [Fact] - public async Task AssignContributor_should_assign_if_user_found() - { - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(new ConfigAppLimitsPlan { MaxContributors = -1 }); - - CreateApp(); - - var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RemoveContributor_should_update_domain_object() - { - CreateApp() - .AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); - - var context = CreateContextForCommand(new RemoveContributor { ContributorId = contributorId }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task AttachClient_should_update_domain_object() - { - CreateApp(); - - var context = CreateContextForCommand(new AttachClient { Id = clientName }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RenameClient_should_update_domain_object() - { - CreateApp() - .AttachClient(CreateCommand(new AttachClient { Id = clientName })); - - var context = CreateContextForCommand(new UpdateClient { Id = clientName, Name = "New Name" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RevokeClient_should_update_domain_object() - { - CreateApp() - .AttachClient(CreateCommand(new AttachClient { Id = clientName })); - - var context = CreateContextForCommand(new RevokeClient { Id = clientName }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ChangePlan_should_update_domain_object() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(true); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan")).MustHaveHappened(); - } - - [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_result() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(true); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan")) - .Returns(CreateRedirectResult()); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Null(app.Plan); - } - - [Fact] - public async Task ChangePlan_should_not_call_billing_manager_for_callback() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(true); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan")).MustNotHaveHappened(); - } - - [Fact] - public async Task AddLanguage_should_update_domain_object() - { - CreateApp(); - - var context = CreateContextForCommand(new AddLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RemoveLanguage_should_update_domain_object() - { - CreateApp() - .AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - var context = CreateContextForCommand(new RemoveLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task UpdateLanguage_should_update_domain_object() - { - CreateApp() - .AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - var context = CreateContextForCommand(new UpdateLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - private AppDomainObject CreateApp() - { - app.Create(CreateCommand(new CreateApp { Name = AppName })); - - return app; - } - - private static Task CreateRedirectResult() - { - return Task.FromResult(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs deleted file mode 100644 index c41ae0f9a..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs +++ /dev/null @@ -1,288 +0,0 @@ -// ========================================================================== -// AppDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppDomainObjectTests : HandlerTestBase - { - private readonly AppDomainObject sut; - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientId = "client"; - private readonly string clientNewName = "My Client"; - private readonly string planId = "premium"; - - public AppDomainObjectTests() - { - sut = new AppDomainObject(AppId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - CreateApp(); - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateApp { Name = AppName })); - }); - } - - [Fact] - public void Create_should_specify_name_and_owner() - { - sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); - - Assert.Equal(AppName, sut.Name); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }), - CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }) - ); - } - - [Fact] - public void ChangePlan_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - }); - } - - [Fact] - public void ChangePlan_should_create_events() - { - CreateApp(); - - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planId }) - ); - } - - [Fact] - public void AssignContributor_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); - }); - } - - [Fact] - public void AssignContributor_should_create_events() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) - ); - } - - [Fact] - public void RemoveContributor_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); - }); - } - - [Fact] - public void RemoveContributor_should_create_events_and_remove_contributor() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) - ); - } - - [Fact] - public void AttachClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - }); - } - - [Fact] - public void AttachClient_should_create_events() - { - var command = new AttachClient { Id = clientId }; - - CreateApp(); - - sut.AttachClient(CreateCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) - ); - } - - [Fact] - public void RevokeClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); - }); - } - - [Fact] - public void RevokeClient_should_create_events() - { - CreateApp(); - CreateClient(); - - sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientRevoked { Id = clientId }) - ); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); - }); - } - - [Fact] - public void UpdateClient_should_create_events() - { - CreateApp(); - CreateClient(); - - sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) - ); - } - - [Fact] - public void AddLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); - }); - } - - [Fact] - public void AddLanguage_should_create_events() - { - CreateApp(); - - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageAdded { Language = Language.DE }) - ); - } - - [Fact] - public void RemoveLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void RemoveLanguage_should_create_events() - { - CreateApp(); - CreateLanguage(Language.DE); - - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageRemoved { Language = Language.DE }) - ); - } - - [Fact] - public void UpdateLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void UpdateLanguage_should_create_events() - { - CreateApp(); - CreateLanguage(Language.DE); - - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) - ); - } - - private void CreateApp() - { - sut.Create(CreateCommand(new CreateApp { Name = AppName })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void CreateClient() - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void CreateLanguage(Language language) - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs deleted file mode 100644 index acb390b7e..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// AppEventTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Apps.Old; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppEventTests - { - private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - - [Fact] - public void Should_migrate_client_changed_as_reader_to_client_updated() - { - var source = CreateEvent(new AppClientChanged { IsReader = true }); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); - } - - [Fact] - public void Should_migrate_client_changed_as_writer_to_client_updated() - { - var source = CreateEvent(new AppClientChanged { IsReader = false }); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); - } - - private T CreateEvent(T contentEvent) where T : AppEvent - { - contentEvent.Actor = actor; - contentEvent.AppId = appId; - - return contentEvent; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs deleted file mode 100644 index bae80d9dc..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// ========================================================================== -// GuardAppClientsTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public class GuardAppClientsTests - { - private readonly AppClients clients_0 = AppClients.Empty; - - [Fact] - public void CanAttach_should_throw_execption_if_client_id_is_null() - { - var command = new AttachClient(); - - Assert.Throws(() => GuardAppClients.CanAttach(clients_0, command)); - } - - [Fact] - public void CanAttach_should_throw_exception_if_client_already_exists() - { - var command = new AttachClient { Id = "android" }; - - var clients_1 = clients_0.Add("android", "secret"); - - Assert.Throws(() => GuardAppClients.CanAttach(clients_1, command)); - } - - [Fact] - public void CanAttach_should_not_throw_exception_if_client_is_free() - { - var command = new AttachClient { Id = "ios" }; - - var clients_1 = clients_0.Add("android", "secret"); - - GuardAppClients.CanAttach(clients_1, command); - } - - [Fact] - public void CanRevoke_should_throw_execption_if_client_id_is_null() - { - var command = new RevokeClient(); - - Assert.Throws(() => GuardAppClients.CanRevoke(clients_0, command)); - } - - [Fact] - public void CanRevoke_should_throw_exception_if_client_is_not_found() - { - var command = new RevokeClient { Id = "ios" }; - - Assert.Throws(() => GuardAppClients.CanRevoke(clients_0, command)); - } - - [Fact] - public void CanRevoke_should_not_throw_exception_if_client_is_found() - { - var command = new RevokeClient { Id = "ios" }; - - var clients_1 = clients_0.Add("ios", "secret"); - - GuardAppClients.CanRevoke(clients_1, command); - } - - [Fact] - public void CanUpdate_should_throw_execption_if_client_id_is_null() - { - var command = new UpdateClient(); - - Assert.Throws(() => GuardAppClients.CanUpdate(clients_0, command)); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_is_not_found() - { - var command = new UpdateClient { Id = "ios", Name = "iOS" }; - - Assert.Throws(() => GuardAppClients.CanUpdate(clients_0, command)); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_no_name_and_permission() - { - var command = new UpdateClient { Id = "ios" }; - - var clients_1 = clients_0.Add("ios", "secret"); - - Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_invalid_permission() - { - var command = new UpdateClient { Id = "ios", Permission = (AppClientPermission)10 }; - - var clients_1 = clients_0.Add("ios", "secret"); - - Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_same_name() - { - var command = new UpdateClient { Id = "ios", Name = "ios" }; - - var clients_1 = clients_0.Add("ios", "secret"); - - Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_same_permission() - { - var command = new UpdateClient { Id = "ios", Permission = AppClientPermission.Editor }; - - var clients_1 = clients_0.Add("ios", "secret"); - - Assert.Throws(() => GuardAppClients.CanUpdate(clients_1, command)); - } - - [Fact] - public void UpdateClient_should_not_throw_exception_if_command_is_valid() - { - var command = new UpdateClient { Id = "ios", Name = "iOS", Permission = AppClientPermission.Reader }; - - var clients_1 = clients_0.Add("ios", "secret"); - - GuardAppClients.CanUpdate(clients_1, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs deleted file mode 100644 index 7942dcd96..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// GuardAppContributorsTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Shared.Users; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public class GuardAppContributorsTests - { - private readonly IUserResolver users = A.Fake(); - private readonly IAppLimitsPlan appPlan = A.Fake(); - private readonly AppContributors contributors_0 = AppContributors.Empty; - - public GuardAppContributorsTests() - { - A.CallTo(() => users.FindByIdAsync(A.Ignored)) - .Returns(A.Fake()); - - A.CallTo(() => appPlan.MaxContributors) - .Returns(10); - } - - [Fact] - public Task CanAssign_should_throw_exception_if_contributor_id_is_null() - { - var command = new AssignContributor(); - - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); - } - - [Fact] - public Task CanAssign_should_throw_exception_if_permission_not_valid() - { - var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; - - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); - } - - [Fact] - public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_permission() - { - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); - - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan)); - } - - [Fact] - public Task CanAssign_should_throw_exception_if_user_not_found() - { - A.CallTo(() => users.FindByIdAsync(A.Ignored)) - .Returns(Task.FromResult(null)); - - var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; - - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); - } - - [Fact] - public Task CanAssign_should_throw_exception_if_contributor_max_reached() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "3" }; - - var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); - var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); - - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan)); - } - - [Fact] - public Task CanAssign_should_not_throw_exception_if_user_found() - { - var command = new AssignContributor { ContributorId = "1" }; - - return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan); - } - - [Fact] - public Task CanAssign_should_not_throw_exception_if_contributor_has_another_permission() - { - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); - - return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan); - } - - [Fact] - public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_permission_changed() - { - A.CallTo(() => appPlan.MaxContributors) - .Returns(2); - - var command = new AssignContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); - var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); - - return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_id_is_null() - { - var command = new RemoveContributor(); - - Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_not_found() - { - var command = new RemoveContributor { ContributorId = "1" }; - - Assert.Throws(() => GuardAppContributors.CanRemove(contributors_0, command)); - } - - [Fact] - public void CanRemove_should_throw_exception_if_contributor_is_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); - var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); - - Assert.Throws(() => GuardAppContributors.CanRemove(contributors_2, command)); - } - - [Fact] - public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; - - var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); - var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Owner); - - GuardAppContributors.CanRemove(contributors_2, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs deleted file mode 100644 index ad4afb684..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// GuardAppLanguagesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public class GuardAppLanguagesTests - { - private readonly LanguagesConfig languages_0 = LanguagesConfig.Build(Language.DE); - - [Fact] - public void CanAddLanguage_should_throw_exception_if_language_is_null() - { - var command = new AddLanguage(); - - Assert.Throws(() => GuardAppLanguages.CanAdd(languages_0, command)); - } - - [Fact] - public void CanAddLanguage_should_throw_exception_if_language_already_added() - { - var command = new AddLanguage { Language = Language.DE }; - - Assert.Throws(() => GuardAppLanguages.CanAdd(languages_0, command)); - } - - [Fact] - public void CanAddLanguage_should_not_throw_exception_if_language_valid() - { - var command = new AddLanguage { Language = Language.EN }; - - GuardAppLanguages.CanAdd(languages_0, command); - } - - [Fact] - public void CanRemoveLanguage_should_throw_exception_if_language_is_null() - { - var command = new RemoveLanguage(); - - Assert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command)); - } - - [Fact] - public void CanRemoveLanguage_should_throw_exception_if_language_not_found() - { - var command = new RemoveLanguage { Language = Language.EN }; - - Assert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command)); - } - - [Fact] - public void CanRemoveLanguage_should_throw_exception_if_language_is_master() - { - var command = new RemoveLanguage { Language = Language.DE }; - - Assert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command)); - } - - [Fact] - public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() - { - var command = new RemoveLanguage { Language = Language.EN }; - - var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); - - GuardAppLanguages.CanRemove(languages_1, command); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_language_is_null() - { - var command = new UpdateLanguage(); - - var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); - - Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() - { - var command = new UpdateLanguage { Language = Language.DE, IsOptional = true }; - - var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); - - Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.IT } }; - - var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); - - Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_not_found() - { - var command = new UpdateLanguage { Language = Language.IT }; - - var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); - - Assert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command)); - } - - [Fact] - public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; - - var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); - - GuardAppLanguages.CanUpdate(languages_1, command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs deleted file mode 100644 index 891c014ef..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -// ========================================================================== -// GuardAppTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public class GuardAppTests - { - private readonly IAppProvider apps = A.Fake(); - private readonly IUserResolver users = A.Fake(); - private readonly IAppPlansProvider appPlans = A.Fake(); - - public GuardAppTests() - { - A.CallTo(() => apps.GetAppAsync("new-app")) - .Returns(Task.FromResult(null)); - - A.CallTo(() => users.FindByIdAsync(A.Ignored)) - .Returns(A.Fake()); - - A.CallTo(() => appPlans.GetPlan("free")) - .Returns(A.Fake()); - } - - [Fact] - public Task CanCreate_should_throw_exception_if_name_already_in_use() - { - A.CallTo(() => apps.GetAppAsync("new-app")) - .Returns(A.Fake()); - - var command = new CreateApp { Name = "new-app" }; - - return Assert.ThrowsAsync(() => GuardApp.CanCreate(command, apps)); - } - - [Fact] - public Task CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateApp { Name = "INVALID NAME" }; - - return Assert.ThrowsAsync(() => GuardApp.CanCreate(command, apps)); - } - - [Fact] - public Task CanCreate_should_not_throw_exception_if_app_name_is_free() - { - var command = new CreateApp { Name = "new-app" }; - - return GuardApp.CanCreate(command, apps); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_id_null() - { - var command = new ChangePlan { Actor = new RefToken("user", "me") }; - - AppPlan plan = null; - - Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_not_found() - { - A.CallTo(() => appPlans.GetPlan("free")) - .Returns(null); - - var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; - - AppPlan plan = null; - - Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() - { - var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(new RefToken("user", "other"), "premium"); - - Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_is_the_same() - { - var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(new RefToken("user", "me"), "free"); - - Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); - } - - [Fact] - public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() - { - var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; - - var plan = new AppPlan(new RefToken("user", "me"), "premium"); - - GuardApp.CanChangePlan(command, plan, appPlans); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs deleted file mode 100644 index 4a7d65a86..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// AssetCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetCommandMiddlewareTests : HandlerTestBase - { - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); - private readonly IAssetStore assetStore = A.Fake(); - private readonly AssetCommandMiddleware sut; - private readonly AssetDomainObject asset; - private readonly Guid assetId = Guid.NewGuid(); - private readonly Stream stream = new MemoryStream(); - private readonly ImageInfo image = new ImageInfo(2048, 2048); - private readonly AssetFile file; - - public AssetCommandMiddlewareTests() - { - file = new AssetFile("my-image.png", "image/png", 1024, () => stream); - - asset = new AssetDomainObject(assetId, -1); - - sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file }); - - SetupStore(0, context.ContextId); - SetupImageInfo(); - - await TestCreate(asset, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(assetId, context.Result>().IdOrValue); - - VerifyStore(0, context.ContextId); - VerifyImageInfo(); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file }); - - SetupStore(1, context.ContextId); - SetupImageInfo(); - - CreateAsset(); - - await TestUpdate(asset, async _ => - { - await sut.HandleAsync(context); - }); - - VerifyStore(1, context.ContextId); - VerifyImageInfo(); - } - - [Fact] - public async Task Rename_should_update_domain_object() - { - CreateAsset(); - - var context = CreateContextForCommand(new RenameAsset { AssetId = assetId, FileName = "my-new-image.png" }); - - await TestUpdate(asset, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateAsset(); - - var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId }); - - await TestUpdate(asset, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void CreateAsset() - { - asset.Create(new CreateAsset { File = file }); - } - - private void SetupImageInfo() - { - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(image); - } - - private void SetupStore(long version, Guid commitId) - { - A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)) - .Returns(TaskHelper.Done); - A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)) - .Returns(TaskHelper.Done); - A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) - .Returns(TaskHelper.Done); - } - - private void VerifyImageInfo() - { - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)).MustHaveHappened(); - } - - private void VerifyStore(long version, Guid commitId) - { - A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)).MustHaveHappened(); - A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).MustHaveHappened(); - A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())).MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs deleted file mode 100644 index cbca77d3b..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -// ========================================================================== -// AssetDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.IO; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetDomainObjectTests : HandlerTestBase - { - private readonly AssetDomainObject sut; - private readonly ImageInfo image = new ImageInfo(2048, 2048); - private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); - - public Guid AssetId { get; } = Guid.NewGuid(); - - public AssetDomainObjectTests() - { - sut = new AssetDomainObject(AssetId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateAsset { File = file }); - - Assert.Throws(() => - { - sut.Create(CreateAssetCommand(new CreateAsset { File = file })); - }); - } - - [Fact] - public void Create_should_create_events() - { - sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetCreated - { - IsImage = true, - FileName = file.FileName, - FileSize = file.FileSize, - FileVersion = 0, - MimeType = file.MimeType, - PixelWidth = image.PixelWidth, - PixelHeight = image.PixelHeight - }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_asset_is_deleted() - { - CreateAsset(); - DeleteAsset(); - - Assert.Throws(() => - { - sut.Update(CreateAssetCommand(new UpdateAsset())); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateAsset(); - - sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetUpdated - { - IsImage = true, - FileSize = file.FileSize, - FileVersion = 1, - MimeType = file.MimeType, - PixelWidth = image.PixelWidth, - PixelHeight = image.PixelHeight - }) - ); - } - - [Fact] - public void Rename_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" })); - }); - } - - [Fact] - public void Rename_should_throw_exception_if_asset_is_deleted() - { - CreateAsset(); - DeleteAsset(); - - Assert.Throws(() => - { - sut.Update(CreateAssetCommand(new UpdateAsset())); - }); - } - - [Fact] - public void Rename_should_create_events() - { - CreateAsset(); - - sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateAssetCommand(new DeleteAsset())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateAsset(); - DeleteAsset(); - - Assert.Throws(() => - { - sut.Delete(CreateAssetCommand(new DeleteAsset())); - }); - } - - [Fact] - public void Delete_should_create_events_with_total_file_size() - { - CreateAsset(); - UpdateAsset(); - - sut.Delete(CreateAssetCommand(new DeleteAsset())); - - Assert.True(sut.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) - ); - } - - private void CreateAsset() - { - sut.Create(CreateAssetCommand(new CreateAsset { File = file })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void UpdateAsset() - { - sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteAsset() - { - sut.Delete(CreateAssetCommand(new DeleteAsset())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateAssetEvent(T @event) where T : AssetEvent - { - @event.AssetId = AssetId; - - return CreateEvent(@event); - } - - protected T CreateAssetCommand(T command) where T : AssetAggregateCommand - { - command.AssetId = AssetId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs deleted file mode 100644 index aa34e4b0b..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// GuardAssetTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Assets.Guards -{ - public class GuardAssetTests - { - [Fact] - public void CanRename_should_throw_exception_if_name_not_defined() - { - var command = new RenameAsset(); - - Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); - } - - [Fact] - public void CanRename_should_throw_exception_if_name_are_the_same() - { - var command = new RenameAsset { FileName = "asset-name" }; - - Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); - } - - [Fact] - public void CanRename_not_should_throw_exception_if_name_are_different() - { - var command = new RenameAsset { FileName = "new-name" }; - - GuardAsset.CanRename(command, "asset-name"); - } - - [Fact] - public void CanCreate_should_not_throw_exception() - { - var command = new CreateAsset(); - - GuardAsset.CanCreate(command); - } - - [Fact] - public void CanUpdate_should_not_throw_exception() - { - var command = new UpdateAsset(); - - GuardAsset.CanUpdate(command); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteAsset(); - - GuardAsset.CanDelete(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs deleted file mode 100644 index 21d668f7e..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ /dev/null @@ -1,247 +0,0 @@ -// ========================================================================== -// ContentCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentCommandMiddlewareTests : HandlerTestBase - { - private readonly ContentCommandMiddleware sut; - private readonly ContentDomainObject content; - private readonly ISchemaEntity schema = A.Fake(); - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAppEntity app = A.Fake(); - private readonly ClaimsPrincipal user = new ClaimsPrincipal(); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); - private readonly Guid contentId = Guid.NewGuid(); - - private readonly NamedContentData invalidData = - new NamedContentData() - .AddField("my-field1", new ContentFieldData() - .AddValue(null)) - .AddField("my-field2", new ContentFieldData() - .AddValue(1)); - private readonly NamedContentData data = - new NamedContentData() - .AddField("my-field1", new ContentFieldData() - .AddValue(1)) - .AddField("my-field2", new ContentFieldData() - .AddValue(1)); - private readonly NamedContentData patch = - new NamedContentData() - .AddField("my-field1", new ContentFieldData() - .AddValue(1)); - - public ContentCommandMiddlewareTests() - { - var schemaDef = - new Schema("my-schema") - .AddField(new NumberField(1, "my-field1", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true })) - .AddField(new NumberField(2, "my-field2", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = false })); - - content = new ContentDomainObject(contentId, -1); - - sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), scriptEngine, A.Dummy()); - - A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); - - A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app); - - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - A.CallTo(() => schema.ScriptCreate).Returns(""); - A.CallTo(() => schema.ScriptChange).Returns(""); - A.CallTo(() => schema.ScriptUpdate).Returns(""); - A.CallTo(() => schema.ScriptDelete).Returns(""); - - A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppName, SchemaId)).Returns((app, schema)); - } - - [Fact] - public async Task Create_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestCreate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Create_should_create_content() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user }); - - await TestCreate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(data, context.Result>().IdOrValue); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); - } - - [Fact] - public async Task Create_should_also_invoke_publish_script_when_publishing() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true }); - - await TestCreate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(data, context.Result>().IdOrValue); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - CreateContent(); - - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestUpdate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - CreateContent(); - - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(data, context.Result().Data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - CreateContent(); - - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestUpdate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Patch_should_update_domain_object() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)).Returns(patch); - - CreateContent(); - - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.NotNull(context.Result().Data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_publish_domain_object() - { - CreateContent(); - - var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateContent(); - - var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(command); - }); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - private void CreateContent() - { - content.Create(new CreateContent { Data = data }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs deleted file mode 100644 index 8f612faf7..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -// ========================================================================== -// ContentDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentDomainObjectTests : HandlerTestBase - { - private readonly ContentDomainObject sut; - private readonly NamedContentData data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)); - private readonly NamedContentData otherData = - new NamedContentData() - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - public Guid ContentId { get; } = Guid.NewGuid(); - - public ContentDomainObjectTests() - { - sut = new ContentDomainObject(ContentId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateContent { Data = data }); - - Assert.Throws(() => - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - }); - } - - [Fact] - public void Create_should_create_events() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data }) - ); - } - - [Fact] - public void Create_should_also_publish_if_set_to_true() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Update(CreateContentCommand(new UpdateContent())); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateContent(); - - sut.Update(CreateContentCommand(new UpdateContent { Data = otherData })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); - } - - [Fact] - public void Update_should_not_create_event_for_same_data() - { - CreateContent(); - UpdateContent(); - - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - - sut.GetUncomittedEvents().Should().BeEmpty(); - } - - [Fact] - public void Patch_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Patch(CreateContentCommand(new PatchContent { Data = data })); - }); - } - - [Fact] - public void Patch_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Patch(CreateContentCommand(new PatchContent())); - }); - } - - [Fact] - public void Patch_should_create_events() - { - CreateContent(); - - sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); - } - - [Fact] - public void Patch_should_not_create_event_for_same_data() - { - CreateContent(); - UpdateContent(); - - sut.Patch(CreateContentCommand(new PatchContent { Data = data })); - - sut.GetUncomittedEvents().Should().BeEmpty(); - } - - [Fact] - public void ChangeStatus_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); - }); - } - - [Fact] - public void ChangeStatus_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); - }); - } - - [Fact] - public void ChangeStatus_should_refresh_properties_and_create_events() - { - CreateContent(); - - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - - Assert.Equal(Status.Published, sut.Status); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateContentCommand(new DeleteContent())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Delete(CreateContentCommand(new DeleteContent())); - }); - } - - [Fact] - public void Delete_should_update_properties_and_create_events() - { - CreateContent(); - - sut.Delete(CreateContentCommand(new DeleteContent())); - - Assert.True(sut.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) - ); - } - - private void CreateContent() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void UpdateContent() - { - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void ChangeStatus(Status status) - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteContent() - { - sut.Delete(CreateContentCommand(new DeleteContent())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateContentEvent(T @event) where T : ContentEvent - { - @event.ContentId = ContentId; - - return CreateEvent(@event); - } - - protected T CreateContentCommand(T command) where T : ContentCommand - { - command.ContentId = ContentId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs deleted file mode 100644 index dcdfe4fee..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// SchemaEventTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Events.Contents.Old; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentEventTests - { - private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); - private readonly Guid contentId = Guid.NewGuid(); - - [Fact] - public void Should_migrate_content_published_to_content_status_changed() - { - var source = CreateEvent(new ContentPublished()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); - } - - [Fact] - public void Should_migrate_content_unpublished_to_content_status_changed() - { - var source = CreateEvent(new ContentUnpublished()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); - } - - [Fact] - public void Should_migrate_content_restored_to_content_status_changed() - { - var source = CreateEvent(new ContentRestored()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); - } - - [Fact] - public void Should_migrate_content_archived_to_content_status_changed() - { - var source = CreateEvent(new ContentArchived()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); - } - - private T CreateEvent(T contentEvent) where T : ContentEvent - { - contentEvent.Actor = actor; - contentEvent.AppId = appId; - contentEvent.SchemaId = schemaId; - contentEvent.ContentId = contentId; - - return contentEvent; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs deleted file mode 100644 index 30bc8593d..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// ContentVersionLoaderTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentVersionLoaderTests - { - private readonly IEventStore eventStore = A.Fake(); - private readonly IEventDataFormatter formatter = A.Fake(); - private readonly IStreamNameResolver nameResolver = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly Guid appId = Guid.NewGuid(); - private readonly string streamName = Guid.NewGuid().ToString(); - private readonly ContentVersionLoader sut; - - public ContentVersionLoaderTests() - { - A.CallTo(() => nameResolver.GetStreamName(typeof(ContentDomainObject), id.ToString())) - .Returns(streamName); - - sut = new ContentVersionLoader(eventStore, nameResolver, formatter); - } - - [Fact] - public async Task Should_throw_exception_when_event_store_returns_no_events() - { - A.CallTo(() => eventStore.GetEventsAsync(streamName, 0)) - .Returns(new List()); - - await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, -1)); - } - - [Fact] - public async Task Should_throw_exception_when_version_not_found() - { - A.CallTo(() => eventStore.GetEventsAsync(streamName, 0)) - .Returns(new List()); - - await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 3)); - } - - [Fact] - public async Task Should_throw_exception_when_content_is_from_another_event() - { - var eventData1 = new EventData(); - - var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(Guid.NewGuid(), "my-app") }; - - var events = new List - { - new StoredEvent("0", 0, eventData1) - }; - - A.CallTo(() => eventStore.GetEventsAsync(streamName, 0)) - .Returns(events); - - A.CallTo(() => formatter.Parse(eventData1, true)) - .Returns(new Envelope(event1)); - - await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 0)); - } - - [Fact] - public async Task Should_load_content_from_created_event() - { - var eventData1 = new EventData(); - var eventData2 = new EventData(); - - var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; - var event2 = new ContentStatusChanged(); - - var events = new List - { - new StoredEvent("0", 0, eventData1), - new StoredEvent("1", 1, eventData2) - }; - - A.CallTo(() => eventStore.GetEventsAsync(streamName, 0)) - .Returns(events); - - A.CallTo(() => formatter.Parse(eventData1, true)) - .Returns(new Envelope(event1)); - A.CallTo(() => formatter.Parse(eventData2, true)) - .Returns(new Envelope(event2)); - - var data = await sut.LoadAsync(appId, id, 3); - - Assert.Same(event1.Data, data); - } - - [Fact] - public async Task Should_load_content_from_correct_version() - { - var eventData1 = new EventData(); - var eventData2 = new EventData(); - var eventData3 = new EventData(); - - var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; - var event2 = new ContentUpdated { Data = new NamedContentData() }; - var event3 = new ContentUpdated { Data = new NamedContentData() }; - - var events = new List - { - new StoredEvent("0", 0, eventData1), - new StoredEvent("1", 1, eventData2), - new StoredEvent("2", 2, eventData3) - }; - - A.CallTo(() => eventStore.GetEventsAsync(streamName, 0)) - .Returns(events); - - A.CallTo(() => formatter.Parse(eventData1, true)) - .Returns(new Envelope(event1)); - A.CallTo(() => formatter.Parse(eventData2, true)) - .Returns(new Envelope(event2)); - A.CallTo(() => formatter.Parse(eventData3, true)) - .Returns(new Envelope(event3)); - - var data = await sut.LoadAsync(appId, id, 1); - - Assert.Equal(event2.Data, data); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs deleted file mode 100644 index b0f78570d..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// GuardContentTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Domain.Apps.Write.Contents.Guards; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents.Guard -{ - public class GuardContentTests - { - [Fact] - public void CanCreate_should_throw_exception_if_data_is_null() - { - var command = new CreateContent(); - - Assert.Throws(() => GuardContent.CanCreate(command)); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_data_is_not_null() - { - var command = new CreateContent { Data = new NamedContentData() }; - - GuardContent.CanCreate(command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_data_is_null() - { - var command = new UpdateContent(); - - Assert.Throws(() => GuardContent.CanUpdate(command)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_data_is_not_null() - { - var command = new UpdateContent { Data = new NamedContentData() }; - - GuardContent.CanUpdate(command); - } - - [Fact] - public void CanPatch_should_throw_exception_if_data_is_null() - { - var command = new PatchContent(); - - Assert.Throws(() => GuardContent.CanPatch(command)); - } - - [Fact] - public void CanPatch_should_not_throw_exception_if_data_is_not_null() - { - var command = new PatchContent { Data = new NamedContentData() }; - - GuardContent.CanPatch(command); - } - - [Fact] - public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() - { - var command = new ChangeContentStatus { Status = (Status)10 }; - - Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); - } - - [Fact] - public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() - { - var command = new ChangeContentStatus { Status = Status.Published }; - - Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); - } - - [Fact] - public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid() - { - var command = new ChangeContentStatus { Status = Status.Published }; - - GuardContent.CanChangeContentStatus(Status.Draft, command); - } - - [Fact] - public void CanPatch_should_not_throw_exception() - { - var command = new DeleteContent(); - - GuardContent.CanDelete(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs deleted file mode 100644 index fe3487cce..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// WebhookActionTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Rules.Guards.Actions -{ - public sealed class WebhookActionTests - { - [Fact] - public async Task Should_add_error_if_url_is_null() - { - var action = new WebhookAction { Url = null }; - - var errors = await RuleActionValidator.ValidateAsync(action); - - Assert.NotEmpty(errors); - } - - [Fact] - public async Task Should_add_error_if_url_is_relative() - { - var action = new WebhookAction { Url = new Uri("/invalid", UriKind.Relative) }; - - var errors = await RuleActionValidator.ValidateAsync(action); - - Assert.NotEmpty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_url_is_absolute() - { - var action = new WebhookAction { Url = new Uri("https://squidex.io", UriKind.Absolute) }; - - var errors = await RuleActionValidator.ValidateAsync(action); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs deleted file mode 100644 index 76975b496..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -// ========================================================================== -// GuardRuleTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Rules.Commands; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Write.Rules.Guards -{ - public class GuardRuleTests - { - private readonly Uri validUrl = new Uri("https://squidex.io"); - private readonly Rule rule_0 = new Rule(new ContentChangedTrigger(), new WebhookAction()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly IAppProvider appProvider = A.Fake(); - - public GuardRuleTests() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Name, A.Ignored, false)) - .Returns(A.Fake()); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_trigger_null() - { - var command = CreateCommand(new CreateRule - { - Trigger = null, - Action = new WebhookAction - { - Url = validUrl - } - }); - - await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_action_null() - { - var command = CreateCommand(new CreateRule - { - Trigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Empty - }, - Action = null - }); - - await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider)); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() - { - var command = CreateCommand(new CreateRule - { - Trigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Empty - }, - Action = new WebhookAction - { - Url = validUrl - } - }); - - await GuardRule.CanCreate(command, appProvider); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() - { - var command = new UpdateRule(); - - await Assert.ThrowsAsync(() => GuardRule.CanUpdate(command, appProvider)); - } - - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid() - { - var command = CreateCommand(new UpdateRule - { - Trigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Empty - }, - Action = new WebhookAction - { - Url = validUrl - } - }); - - await GuardRule.CanUpdate(command, appProvider); - } - - [Fact] - public void CanEnable_should_throw_exception_if_rule_enabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Enable(); - - Assert.Throws(() => GuardRule.CanEnable(command, rule_1)); - } - - [Fact] - public void CanEnable_should_not_throw_exception_if_rule_disabled() - { - var command = new EnableRule(); - - var rule_1 = rule_0.Disable(); - - GuardRule.CanEnable(command, rule_1); - } - - [Fact] - public void CanDisable_should_throw_exception_if_rule_disabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Disable(); - - Assert.Throws(() => GuardRule.CanDisable(command, rule_1)); - } - - [Fact] - public void CanDisable_should_not_throw_exception_if_rule_enabled() - { - var command = new DisableRule(); - - var rule_1 = rule_0.Enable(); - - GuardRule.CanDisable(command, rule_1); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteRule(); - - GuardRule.CanDelete(command); - } - - private T CreateCommand(T command) where T : AppCommand - { - command.AppId = appId; - - return command; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs deleted file mode 100644 index 7e5546070..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// ContentChangedTriggerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Rules.Guards.Triggers -{ - public class ContentChangedTriggerTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly string appName = "my-app"; - - [Fact] - public async Task Should_add_error_if_schemas_ids_are_not_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) - .Returns(Task.FromResult(null)); - - var trigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Create( - new ContentChangedTriggerSchema() - ) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); - - Assert.NotEmpty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_is_null() - { - var trigger = new ContentChangedTrigger(); - - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_is_empty() - { - var trigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Empty - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_schemas_ids_are_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) - .Returns(A.Fake()); - - var trigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Create( - new ContentChangedTriggerSchema() - ) - }; - - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs deleted file mode 100644 index c8055837d..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// RuleCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Rules.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Rules -{ - public class RuleCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly RuleCommandMiddleware sut; - private readonly RuleDomainObject rule; - private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); - private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; - private readonly Guid ruleId = Guid.NewGuid(); - - public RuleCommandMiddlewareTests() - { - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) - .Returns(A.Fake()); - - rule = new RuleDomainObject(ruleId, -1); - - sut = new RuleCommandMiddleware(Handler, appProvider); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); - - await TestCreate(rule, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }); - - CreateRule(); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Enable_should_update_domain_object() - { - CreateRule(); - DisableRule(); - - var command = CreateContextForCommand(new EnableRule { RuleId = ruleId }); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(command); - }); - } - - [Fact] - public async Task Disable_should_update_domain_object() - { - CreateRule(); - - var command = CreateContextForCommand(new DisableRule { RuleId = ruleId }); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(command); - }); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateRule(); - - var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId }); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void DisableRule() - { - rule.Disable(new DisableRule()); - } - - private void CreateRule() - { - rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs deleted file mode 100644 index b6029798c..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs +++ /dev/null @@ -1,240 +0,0 @@ -// ========================================================================== -// RuleDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Domain.Apps.Write.Rules.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Rules -{ - public class RuleDomainObjectTests : HandlerTestBase - { - private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); - private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; - private readonly RuleDomainObject sut; - - public Guid RuleId { get; } = Guid.NewGuid(); - - public RuleDomainObjectTests() - { - sut = new RuleDomainObject(RuleId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); - - Assert.Throws(() => - { - sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); - }); - } - - [Fact] - public void Create_should_create_events() - { - var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction }; - - sut.Create(CreateRuleCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_rule_is_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); - }); - } - - [Fact] - public void Update_should_create_events() - { - var newTrigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Empty - }; - - var newAction = new WebhookAction - { - Url = new Uri("https://squidex.io/v2") - }; - - CreateRule(); - - var command = new UpdateRule { Trigger = newTrigger, Action = newAction }; - - sut.Update(CreateRuleCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction }) - ); - } - - [Fact] - public void Enable_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Enable(CreateRuleCommand(new EnableRule())); - }); - } - - [Fact] - public void Enable_should_throw_exception_if_rule_is_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Enable(CreateRuleCommand(new EnableRule())); - }); - } - - [Fact] - public void Enable_should_create_events() - { - CreateRule(); - - var command = new EnableRule(); - - sut.Enable(CreateRuleCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleEnabled()) - ); - } - - [Fact] - public void Disable_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Disable(CreateRuleCommand(new DisableRule())); - }); - } - - [Fact] - public void Disable_should_throw_exception_if_rule_is_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Disable(CreateRuleCommand(new DisableRule())); - }); - } - - [Fact] - public void Disable_should_create_events() - { - CreateRule(); - - var command = new DisableRule(); - - sut.Disable(CreateRuleCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleDisabled()) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateRuleCommand(new DeleteRule())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Delete(CreateRuleCommand(new DeleteRule())); - }); - } - - [Fact] - public void Delete_should_update_create_events() - { - CreateRule(); - - sut.Delete(CreateRuleCommand(new DeleteRule())); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleDeleted()) - ); - } - - private void CreateRule() - { - sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteRule() - { - sut.Delete(CreateRuleCommand(new DeleteRule())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateRuleEvent(T @event) where T : RuleEvent - { - @event.RuleId = RuleId; - - return CreateEvent(@event); - } - - protected T CreateRuleCommand(T command) where T : RuleAggregateCommand - { - command.RuleId = RuleId; - - return CreateCommand(command); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs deleted file mode 100644 index c8c1a46c1..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/AssetsFieldPropertiesTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// AssetsFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class AssetsFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_min_items_greater_than_max_items() - { - var sut = new AssetsFieldProperties { MinItems = 10, MaxItems = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems") - }); - } - - [Fact] - public void Should_add_error_if_min_width_greater_than_max_width() - { - var sut = new AssetsFieldProperties { MinWidth = 10, MaxWidth = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max width must be greater than min width.", "MinWidth", "MaxWidth") - }); - } - - [Fact] - public void Should_add_error_if_min_height_greater_than_max_height() - { - var sut = new AssetsFieldProperties { MinHeight = 10, MaxHeight = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max height must be greater than min height.", "MinHeight", "MaxHeight") - }); - } - - [Fact] - public void Should_add_error_if_min_size_greater_than_max_size() - { - var sut = new AssetsFieldProperties { MinSize = 10, MaxSize = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max size must be greater than min size.", "MinSize", "MaxSize") - }); - } - - [Fact] - public void Should_add_error_if_only_aspect_width_is_defined() - { - var sut = new AssetsFieldProperties { AspectWidth = 10 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Aspect width and height must be defined.", "AspectWidth", "AspectHeight") - }); - } - - [Fact] - public void Should_add_error_if_only_aspect_height_is_defined() - { - var sut = new AssetsFieldProperties { AspectHeight = 10 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Aspect width and height must be defined.", "AspectWidth", "AspectHeight") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs deleted file mode 100644 index 4469a2dcf..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/BooleanFieldPropertiesTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// BooleanFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class BooleanFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new BooleanFieldProperties { Editor = (BooleanFieldEditor)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs deleted file mode 100644 index 14bd29f42..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ========================================================================== -// DateTimeFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using NodaTime; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class DateTimeFieldPropertiesTests - { - [Fact] - public void Should_not_add_error_if_sut_is_valid() - { - var sut = new DateTimeFieldProperties - { - MinValue = FutureDays(10), - MaxValue = FutureDays(20), - DefaultValue = FutureDays(15) - }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_default_value_is_less_than_min() - { - var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), DefaultValue = FutureDays(5) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Default value must be greater than min value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_default_value_is_greater_than_min() - { - var sut = new DateTimeFieldProperties { MaxValue = FutureDays(10), DefaultValue = FutureDays(15) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Default value must be less than max value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_min_greater_than_max() - { - var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") - }); - } - - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new DateTimeFieldProperties { Editor = (DateTimeFieldEditor)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } - - [Fact] - public void Should_add_error_if_calculated_default_value_is_not_valid() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = (DateTimeCalculatedDefaultValue)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Calculated default value is not valid.", "CalculatedDefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_calculated_default_value_default_value_is_defined() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now, DefaultValue = FutureDays(10) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Calculated default value and default value cannot be used together.", "CalculatedDefaultValue", "DefaultValue") - }); - } - - private static Instant FutureDays(int days) - { - return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs deleted file mode 100644 index 8f3967ee3..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// GeolocationPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class GeolocationFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs deleted file mode 100644 index c4415bf1d..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// JsonFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class JsonFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new JsonFieldProperties(); - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs deleted file mode 100644 index 199261ba4..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ========================================================================== -// NumberFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class NumberFieldPropertiesTests - { - [Fact] - public void Should_not_add_error_if_sut_is_valid() - { - var sut = new NumberFieldProperties - { - MinValue = 0, - MaxValue = 100, - DefaultValue = 5 - }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_default_value_is_less_than_min() - { - var sut = new NumberFieldProperties { MinValue = 10, DefaultValue = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Default value must be greater than min value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_default_value_is_greater_than_min() - { - var sut = new NumberFieldProperties { MaxValue = 0, DefaultValue = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Default value must be less than max value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_min_greater_than_max() - { - var sut = new NumberFieldProperties { MinValue = 10, MaxValue = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") - }); - } - - [Fact] - public void Should_add_error_if_allowed_values_and_max_value_is_specified() - { - var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = ImmutableList.Create(4d) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue") - }); - } - - [Fact] - public void Should_add_error_if_allowed_values_and_min_value_is_specified() - { - var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = ImmutableList.Create(4d) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue") - }); - } - - [Fact] - public void Should_add_error_if_radio_button_has_no_allowed_values() - { - var sut = new NumberFieldProperties { Editor = NumberFieldEditor.Radio }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") - }); - } - - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new NumberFieldProperties { Editor = (NumberFieldEditor)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs deleted file mode 100644 index 2f721bf05..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// ReferencesFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class ReferencesFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_min_greater_than_max() - { - var sut = new ReferencesFieldProperties { MinItems = 10, MaxItems = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs deleted file mode 100644 index 19c306680..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// StringFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class StringFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_min_greater_than_max() - { - var sut = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max length must be greater than min length.", "MinLength", "MaxLength") - }); - } - - [Fact] - public void Should_add_error_if_allowed_values_and_max_value_is_specified() - { - var sut = new StringFieldProperties { MinLength = 10, AllowedValues = ImmutableList.Create("4") }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Either allowed values or min and max length can be defined.", "AllowedValues", "MinLength", "MaxLength") - }); - } - - [Fact] - public void Should_add_error_if_allowed_values_and_min_value_is_specified() - { - var sut = new StringFieldProperties { MaxLength = 10, AllowedValues = ImmutableList.Create("4") }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Either allowed values or min and max length can be defined.", "AllowedValues", "MinLength", "MaxLength") - }); - } - - [Fact] - public void Should_add_error_if_radio_button_has_no_allowed_values() - { - var sut = new StringFieldProperties { Editor = StringFieldEditor.Radio }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") - }); - } - - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new StringFieldProperties { Editor = (StringFieldEditor)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } - - [Fact] - public void Should_add_error_if_pattern_is_not_valid_regex() - { - var sut = new StringFieldProperties { Pattern = "[0-9{1}" }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Pattern is not a valid expression.", "Pattern") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs deleted file mode 100644 index 77dec4d89..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/TagsFieldPropertiesTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// TagsFieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties -{ - public class TagsFieldPropertiesTests - { - [Fact] - public void Should_add_error_if_min_greater_than_max() - { - var sut = new TagsFieldProperties { MinItems = 10, MaxItems = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.ShouldBeEquivalentTo( - new List - { - new ValidationError("Max items must be greater than min items.", "MinItems", "MaxItems") - }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs deleted file mode 100644 index 19162a358..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs +++ /dev/null @@ -1,247 +0,0 @@ -// ========================================================================== -// GuardSchemaFieldTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public class GuardSchemaFieldTests - { - private readonly Schema schema_0; - private readonly StringFieldProperties validProperties = new StringFieldProperties(); - private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - - public GuardSchemaFieldTests() - { - schema_0 = - new Schema("my-schema") - .AddField(new StringField(1, "field1", Partitioning.Invariant)) - .AddField(new StringField(2, "field2", Partitioning.Invariant)); - } - - [Fact] - public void CanHide_should_throw_exception_if_already_hidden() - { - var command = new HideField { FieldId = 1 }; - - var schema_1 = schema_0.HideField(1); - - Assert.Throws(() => GuardSchemaField.CanHide(schema_1, command)); - } - - [Fact] - public void CanHide_should_throw_exception_if_not_found() - { - var command = new HideField { FieldId = 3 }; - - Assert.Throws(() => GuardSchemaField.CanHide(schema_0, command)); - } - - [Fact] - public void CanHide_hould_not_throw_exception_if_visible() - { - var command = new HideField { FieldId = 1 }; - - GuardSchemaField.CanHide(schema_0, command); - } - - [Fact] - public void CanDisable_should_throw_exception_if_already_disabled() - { - var command = new DisableField { FieldId = 1 }; - - var schema_1 = schema_0.DisableField(1); - - Assert.Throws(() => GuardSchemaField.CanDisable(schema_1, command)); - } - - [Fact] - public void CanDisable_should_throw_exception_if_not_found() - { - var command = new DisableField { FieldId = 3 }; - - Assert.Throws(() => GuardSchemaField.CanDisable(schema_0, command)); - } - - [Fact] - public void CanDisable_Should_not_throw_exception_if_enabled() - { - var command = new DisableField { FieldId = 1 }; - - GuardSchemaField.CanDisable(schema_0, command); - } - - [Fact] - public void CanShow_should_throw_exception_if_already_shown() - { - var command = new ShowField { FieldId = 1 }; - - Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); - } - - [Fact] - public void CanShow_should_throw_exception_if_not_found() - { - var command = new ShowField { FieldId = 3 }; - - Assert.Throws(() => GuardSchemaField.CanShow(schema_0, command)); - } - - [Fact] - public void CanShow_should_not_throw_exception_if_hidden() - { - var command = new ShowField { FieldId = 1 }; - - var schema_1 = schema_0.HideField(1); - - GuardSchemaField.CanShow(schema_1, command); - } - - [Fact] - public void CanEnable_should_throw_exception_if_already_enabled() - { - var command = new EnableField { FieldId = 1 }; - - Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); - } - - [Fact] - public void CanEnable_should_throw_exception_if_not_found() - { - var command = new EnableField { FieldId = 3 }; - - Assert.Throws(() => GuardSchemaField.CanEnable(schema_0, command)); - } - - [Fact] - public void CanEnable_should_not_throw_exception_if_disabled() - { - var command = new EnableField { FieldId = 1 }; - - var schema_1 = schema_0.DisableField(1); - - GuardSchemaField.CanEnable(schema_1, command); - } - - [Fact] - public void CanLock_should_throw_exception_if_already_locked() - { - var command = new LockField { FieldId = 1 }; - - var schema_1 = schema_0.LockField(1); - - Assert.Throws(() => GuardSchemaField.CanLock(schema_1, command)); - } - - [Fact] - public void LockField_should_throw_exception_if_not_found() - { - var command = new LockField { FieldId = 3 }; - - Assert.Throws(() => GuardSchemaField.CanLock(schema_0, command)); - } - - [Fact] - public void CanLock_should_not_throw_exception_if_not_locked() - { - var command = new LockField { FieldId = 1 }; - - GuardSchemaField.CanLock(schema_0, command); - } - - [Fact] - public void CanDelete_should_throw_exception_if_not_found() - { - var command = new DeleteField { FieldId = 3 }; - - Assert.Throws(() => GuardSchemaField.CanDelete(schema_0, command)); - } - - [Fact] - public void CanDelete_should_throw_exception_if_locked() - { - var command = new DeleteField { FieldId = 1 }; - - var schema_1 = schema_0.LockField(1); - - Assert.Throws(() => GuardSchemaField.CanDelete(schema_1, command)); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_not_locked() - { - var command = new DeleteField { FieldId = 1 }; - - GuardSchemaField.CanDelete(schema_0, command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_locked() - { - var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; - - var schema_1 = schema_0.LockField(1); - - Assert.Throws(() => GuardSchemaField.CanUpdate(schema_1, command)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_not_locked() - { - var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; - - GuardSchemaField.CanUpdate(schema_0, command); - } - - [Fact] - public void CanAdd_should_throw_exception_if_field_already_exists() - { - var command = new AddField { Name = "field1", Properties = new StringFieldProperties() }; - - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); - } - - [Fact] - public void CanAdd_should_throw_exception_if_name_not_valid() - { - var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; - - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); - } - - [Fact] - public void CanAdd_should_throw_exception_if_properties_not_valid() - { - var command = new AddField { Name = "field3", Properties = invalidProperties }; - - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); - } - - [Fact] - public void CanAdd_should_throw_exception_if_partitioning_not_valid() - { - var command = new AddField { Name = "field3", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; - - Assert.Throws(() => GuardSchemaField.CanAdd(schema_0, command)); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_not_exists() - { - var command = new AddField { Name = "field3", Properties = new StringFieldProperties() }; - - GuardSchemaField.CanAdd(schema_0, command); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs deleted file mode 100644 index 3b9ac5b4b..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs +++ /dev/null @@ -1,201 +0,0 @@ -// ========================================================================== -// GuardSchemaTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -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.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable SA1310 // Field names must not contain underscore - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public class GuardSchemaTests - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly Schema schema_0; - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - - public GuardSchemaTests() - { - schema_0 = - new Schema("my-schema") - .AddField(new StringField(1, "field1", Partitioning.Invariant)) - .AddField(new StringField(2, "field2", Partitioning.Invariant)); - - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, "new-schema", false)) - .Returns(Task.FromResult(null)); - } - - [Fact] - public Task CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; - - return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); - } - - [Fact] - public Task CanCreate_should_throw_exception_if_name_already_in_use() - { - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, "new-schema", false)) - .Returns(Task.FromResult(A.Fake())); - - var command = new CreateSchema { AppId = appId, Name = "new-schema" }; - - return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); - } - - [Fact] - public Task CanCreate_should_throw_exception_if_fields_not_valid() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new CreateSchemaField - { - Name = null, - Properties = null, - Partitioning = "invalid" - }, - new CreateSchemaField - { - Name = null, - Properties = InvalidProperties(), - Partitioning = "invalid" - } - }, - Name = "new-schema" - }; - - return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); - } - - [Fact] - public Task CanCreate_should_throw_exception_if_fields_contain_duplicate_names() - { - var command = new CreateSchema - { - AppId = appId, - Fields = new List - { - new CreateSchemaField - { - Name = "field1", - Properties = ValidProperties(), - Partitioning = "invariant" - }, - new CreateSchemaField - { - Name = "field1", - Properties = ValidProperties(), - Partitioning = "invariant" - } - }, - Name = "new-schema" - }; - - return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, appProvider)); - } - - [Fact] - public Task CanCreate_should_not_throw_exception_if_command_is_valid() - { - var command = new CreateSchema { AppId = appId, Name = "new-schema" }; - - return GuardSchema.CanCreate(command, appProvider); - } - - [Fact] - public void CanPublish_should_throw_exception_if_already_published() - { - var command = new PublishSchema(); - - var schema_1 = schema_0.Publish(); - - Assert.Throws(() => GuardSchema.CanPublish(schema_1, command)); - } - - [Fact] - public void CanPublish_should_not_throw_exception_if_not_published() - { - var command = new PublishSchema(); - - GuardSchema.CanPublish(schema_0, command); - } - - [Fact] - public void CanUnpublish_should_throw_exception_if_already_unpublished() - { - var command = new UnpublishSchema(); - - Assert.Throws(() => GuardSchema.CanUnpublish(schema_0, command)); - } - - [Fact] - public void CanUnpublish_should_not_throw_exception_if_already_published() - { - var command = new UnpublishSchema(); - - var schema_1 = schema_0.Publish(); - - GuardSchema.CanUnpublish(schema_1, command); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() - { - var command = new ReorderFields { FieldIds = new List { 1, 3 } }; - - Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); - } - - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() - { - var command = new ReorderFields { FieldIds = new List { 1 } }; - - Assert.Throws(() => GuardSchema.CanReorder(schema_0, command)); - } - - [Fact] - public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() - { - var command = new ReorderFields { FieldIds = new List { 1, 2 } }; - - GuardSchema.CanReorder(schema_0, command); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteSchema(); - - GuardSchema.CanDelete(schema_0, command); - } - - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } - - private static StringFieldProperties InvalidProperties() - { - return new StringFieldProperties { MinLength = 20, MaxLength = 10 }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs deleted file mode 100644 index 9ad2b24fc..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -// ========================================================================== -// SchemaCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas -{ - public class SchemaCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly SchemaCommandMiddleware sut; - private readonly SchemaDomainObject schema; - private readonly FieldRegistry registry = new FieldRegistry(new TypeNameRegistry()); - private readonly string fieldName = "age"; - - public SchemaCommandMiddlewareTests() - { - schema = new SchemaDomainObject(SchemaId, -1, registry); - - sut = new SchemaCommandMiddleware(Handler, appProvider); - - A.CallTo(() => appProvider.GetSchemaAsync(AppName, SchemaName, false)) - .Returns((ISchemaEntity)null); - } - - [Fact] - public async Task Create_should_create_schema_domain_object() - { - var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - - await TestCreate(schema, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(SchemaId, context.Result>().IdOrValue); - - A.CallTo(() => appProvider.GetSchemaAsync(AppName, SchemaName, false)).MustHaveHappened(); - } - - [Fact] - public async Task UpdateSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new UpdateSchema { Properties = new SchemaProperties() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ReorderSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new ReorderFields { FieldIds = new List() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task PublishSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new PublishSchema()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task UnpublishSchema_should_update_domain_object() - { - CreateSchema(); - PublishSchema(); - - var context = CreateContextForCommand(new UnpublishSchema()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ConfigureScripts_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new ConfigureScripts()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DeleteSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new DeleteSchema()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Add_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(1, context.Result>().IdOrValue); - } - - [Fact] - public async Task UpdateField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task LockField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new LockField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task HideField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new HideField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ShowField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - HideField(); - - var context = CreateContextForCommand(new ShowField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DisableField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new DisableField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task EnableField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - DisableField(); - - var context = CreateContextForCommand(new EnableField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DeleteField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new DeleteField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - private void CreateSchema() - { - schema.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - } - - private void PublishSchema() - { - schema.Publish(CreateCommand(new PublishSchema())); - } - - private void CreateField() - { - schema.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); - } - - private void HideField() - { - schema.HideField(CreateCommand(new HideField { FieldId = 1 })); - } - - private void DisableField() - { - schema.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs deleted file mode 100644 index 69fd68ebd..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs +++ /dev/null @@ -1,656 +0,0 @@ -// ========================================================================== -// SchemaDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas -{ - public class SchemaDomainObjectTests : HandlerTestBase - { - private readonly string fieldName = "age"; - private readonly NamedId fieldId; - private readonly SchemaDomainObject sut; - - public SchemaDomainObjectTests() - { - fieldId = new NamedId(1, fieldName); - - var fieldRegistry = new FieldRegistry(new TypeNameRegistry()); - - sut = new SchemaDomainObject(SchemaId, 0, fieldRegistry); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateSchema { Name = SchemaName }); - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - }); - } - - [Fact] - public void Create_should_create_schema_and_create_events() - { - var properties = new SchemaProperties(); - - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties })); - - Assert.Equal(SchemaName, sut.Schema.Name); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties }) - ); - } - - [Fact] - public void Create_should_create_schema_with_initial_fields() - { - var properties = new SchemaProperties(); - - var fields = new List - { - new CreateSchemaField { Name = "field1", Properties = ValidProperties() }, - new CreateSchemaField { Name = "field2", Properties = ValidProperties() } - }; - - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties, Fields = fields })); - - var @event = (SchemaCreated)sut.GetUncomittedEvents().Single().Payload; - - Assert.Equal(SchemaName, sut.Schema.Name); - Assert.Equal(2, @event.Fields.Count); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); - }); - } - - [Fact] - public void Update_should_refresh_properties_and_create_events() - { - var properties = new SchemaProperties(); - - CreateSchema(); - - sut.Update(CreateCommand(new UpdateSchema { Properties = properties })); - - Assert.Equal(properties, sut.Schema.Properties); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaUpdated { Properties = properties }) - ); - } - - [Fact] - public void ConfigureScripts_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ConfigureScripts(CreateCommand(new ConfigureScripts())); - }); - } - - [Fact] - public void ConfigureScripts_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.ConfigureScripts(CreateCommand(new ConfigureScripts())); - }); - } - - [Fact] - public void ConfigureScripts_should_create_events() - { - CreateSchema(); - - sut.ConfigureScripts(CreateCommand(new ConfigureScripts - { - ScriptQuery = "", - ScriptCreate = "", - ScriptUpdate = "", - ScriptDelete = "", - ScriptChange = "" - })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new ScriptsConfigured - { - ScriptQuery = "", - ScriptCreate = "", - ScriptUpdate = "", - ScriptDelete = "", - ScriptChange = "" - }) - ); - } - - [Fact] - public void Reorder_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); - }); - } - - [Fact] - public void Reorder_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); - }); - } - - [Fact] - public void Reorder_should_refresh_properties_and_create_events() - { - var fieldIds = new List { 1, 2 }; - - CreateSchema(); - - sut.Add(new AddField { Name = "field1", Properties = ValidProperties() }); - sut.Add(new AddField { Name = "field2", Properties = ValidProperties() }); - - ((IAggregate)sut).ClearUncommittedEvents(); - - sut.Reorder(CreateCommand(new ReorderFields { FieldIds = fieldIds })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaFieldsReordered { FieldIds = fieldIds }) - ); - } - - [Fact] - public void Publish_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Publish(CreateCommand(new PublishSchema())); - }); - } - - [Fact] - public void Publish_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Publish(CreateCommand(new PublishSchema())); - }); - } - - [Fact] - public void Publish_should_refresh_properties_and_create_events() - { - CreateSchema(); - - sut.Publish(CreateCommand(new PublishSchema())); - - Assert.True(sut.Schema.IsPublished); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaPublished()) - ); - } - - [Fact] - public void Unpublish_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Unpublish(CreateCommand(new UnpublishSchema())); - }); - } - - [Fact] - public void Unpublish_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Unpublish(CreateCommand(new UnpublishSchema())); - }); - } - - [Fact] - public void Unpublish_should_refresh_properties_and_create_events() - { - CreateSchema(); - PublishSchema(); - - sut.Unpublish(CreateCommand(new UnpublishSchema())); - - Assert.False(sut.Schema.IsPublished); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaUnpublished()) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateCommand(new DeleteSchema())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Delete(CreateCommand(new DeleteSchema())); - }); - } - - [Fact] - public void Delete_should_refresh_properties_and_create_events() - { - CreateSchema(); - - sut.Delete(CreateCommand(new DeleteSchema())); - - Assert.True(sut.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaDeleted()) - ); - } - - [Fact] - public void AddField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = ValidProperties() })); - }); - } - - [Fact] - public void AddField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); - }); - } - - [Fact] - public void Add_should_update_schema_and_create_events() - { - var properties = new NumberFieldProperties(); - - CreateSchema(); - - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = properties })); - - Assert.Equal(properties, sut.Schema.FieldsById[1].RawProperties); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = properties }) - ); - } - - [Fact] - public void UpdateField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); - }); - } - - [Fact] - public void UpdateField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); - }); - } - - [Fact] - public void UpdateField_should_update_schema_and_create_events() - { - var properties = new NumberFieldProperties(); - - CreateSchema(); - CreateField(); - - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = properties })); - - Assert.Equal(properties, sut.Schema.FieldsById[1].RawProperties); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = properties }) - ); - } - - [Fact] - public void LockField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.LockField(CreateCommand(new LockField { FieldId = 1 })); - }); - } - - [Fact] - public void LockField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.LockField(CreateCommand(new LockField { FieldId = 1 })); - }); - } - - [Fact] - public void LockField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.LockField(CreateCommand(new LockField { FieldId = 1 })); - - Assert.False(sut.Schema.FieldsById[1].IsDisabled); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldLocked { FieldId = fieldId }) - ); - } - - [Fact] - public void HideField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - }); - } - - [Fact] - public void HideField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - }); - } - - [Fact] - public void HideField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - - Assert.True(sut.Schema.FieldsById[1].IsHidden); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldHidden { FieldId = fieldId }) - ); - } - - [Fact] - public void ShowField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - }); - } - - [Fact] - public void ShowField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - }); - } - - [Fact] - public void ShowField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - - Assert.False(sut.Schema.FieldsById[1].IsHidden); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new FieldShown { FieldId = fieldId }) - ); - } - - [Fact] - public void DisableField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - }); - } - - [Fact] - public void DisableField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - }); - } - - [Fact] - public void DisableField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - - Assert.True(sut.Schema.FieldsById[1].IsDisabled); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldDisabled { FieldId = fieldId }) - ); - } - - [Fact] - public void EnableField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - }); - } - - [Fact] - public void EnableField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - }); - } - - [Fact] - public void EnableField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - - Assert.False(sut.Schema.FieldsById[1].IsDisabled); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new FieldEnabled { FieldId = fieldId }) - ); - } - - [Fact] - public void DeleteField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - }); - } - - [Fact] - public void DeleteField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - }); - } - - [Fact] - public void DeleteField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - - Assert.False(sut.Schema.FieldsById.ContainsKey(1)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldDeleted { FieldId = fieldId }) - ); - } - - private void CreateField() - { - sut.Add(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void CreateSchema() - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void PublishSchema() - { - sut.Publish(CreateCommand(new PublishSchema())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteSchema() - { - sut.Delete(CreateCommand(new DeleteSchema())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } - - private static StringFieldProperties InvalidProperties() - { - return new StringFieldProperties { MinLength = 20, MaxLength = 10 }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj deleted file mode 100644 index eb908d328..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - Exe - netcoreapp2.0 - Squidex.Domain.Apps.Write - - - - - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - diff --git a/tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/AssertHelper.cs b/tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/AssertHelper.cs deleted file mode 100644 index f9e889f95..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/AssertHelper.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// AssertHelper.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Write.TestHelpers -{ - public static class AssertHelper - { - public static void ShouldHaveSameEvents(this IEnumerable> events, params IEvent[] others) - { - var source = events.Select(x => x.Payload).ToArray(); - - source.Should().HaveSameCount(others); - - for (var i = 0; i < source.Length; i++) - { - var lhs = source[i]; - var rhs = others[i]; - - lhs.ShouldBeSameEvent(rhs); - } - } - - public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) - { - lhs.Should().BeOfType(rhs.GetType()); - - ((object)lhs).ShouldBeEquivalentTo(rhs, o => o.IncludingAllDeclaredProperties()); - } - - public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs) - { - lhs.Should().BeOfType(rhs.GetType()); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/HandlerTestBase.cs deleted file mode 100644 index 2425a7e86..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/TestHelpers/HandlerTestBase.cs +++ /dev/null @@ -1,162 +0,0 @@ -// ========================================================================== -// HandlerTestBase.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -#pragma warning disable IDE0019 // Use pattern matching - -namespace Squidex.Domain.Apps.Write.TestHelpers -{ - public abstract class HandlerTestBase where T : DomainObjectBase - { - private sealed class MockupHandler : IAggregateHandler - { - private T domainObject; - - public bool IsCreated { get; private set; } - public bool IsUpdated { get; private set; } - - public void Init(T newDomainObject) - { - domainObject = newDomainObject; - - IsCreated = false; - IsUpdated = false; - } - - public async Task CreateAsync(CommandContext context, Func creator) where V : class, IAggregate - { - IsCreated = true; - - var @do = domainObject as V; - - await creator(domainObject as V); - - return @do; - } - - public async Task UpdateAsync(CommandContext context, Func updater) where V : class, IAggregate - { - IsUpdated = true; - - var @do = domainObject as V; - - await updater(domainObject as V); - - return @do; - } - } - - private readonly MockupHandler handler = new MockupHandler(); - - protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString()); - - protected Guid AppId { get; } = Guid.NewGuid(); - - protected Guid SchemaId { get; } = Guid.NewGuid(); - - protected string AppName { get; } = "my-app"; - - protected string SchemaName { get; } = "my-schema"; - - protected NamedId AppNamedId - { - get { return new NamedId(AppId, AppName); } - } - - protected NamedId SchemaNamedId - { - get { return new NamedId(SchemaId, SchemaName); } - } - - protected IAggregateHandler Handler - { - get { return handler; } - } - - protected CommandContext CreateContextForCommand(TCommand command) where TCommand : SquidexCommand - { - return new CommandContext(CreateCommand(command)); - } - - protected async Task TestCreate(T domainObject, Func action, bool shouldCreate = true) - { - handler.Init(domainObject); - - await action(domainObject); - - if (!handler.IsCreated && shouldCreate) - { - throw new InvalidOperationException("Create not called."); - } - } - - protected async Task TestUpdate(T domainObject, Func action, bool shouldUpdate = true) - { - handler.Init(domainObject); - - await action(domainObject); - - if (!handler.IsUpdated && shouldUpdate) - { - throw new InvalidOperationException("Update not called."); - } - } - - protected TCommand CreateCommand(TCommand command) where TCommand : SquidexCommand - { - if (command.Actor == null) - { - command.Actor = User; - } - - var appCommand = command as AppCommand; - - if (appCommand != null && appCommand.AppId == null) - { - appCommand.AppId = AppNamedId; - } - - var schemaCommand = command as SchemaCommand; - - if (schemaCommand != null && schemaCommand.SchemaId == null) - { - schemaCommand.SchemaId = SchemaNamedId; - } - - return command; - } - - protected TEvent CreateEvent(TEvent @event) where TEvent : SquidexEvent - { - @event.Actor = User; - - var appEvent = @event as AppEvent; - - if (appEvent != null) - { - appEvent.AppId = AppNamedId; - } - - var schemaEvent = @event as SchemaEvent; - - if (schemaEvent != null) - { - schemaEvent.SchemaId = SchemaNamedId; - } - - return @event; - } - } -} - -#pragma warning restore IDE0019 // Use pattern matching \ No newline at end of file From 9a2e531c729eeaf3fba5ea8463afd09e1bcaff55 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 11 Dec 2017 21:54:51 +0100 Subject: [PATCH 22/30] Logging cleaned. --- Squidex.sln | 86 +---- .../Apps/MongoAppRepository.cs | 2 +- .../Assets/MongoAssetRepository.cs | 2 +- .../Contents/MongoContentRepository.cs | 3 +- .../Rules/MongoRuleRepository.cs | 2 +- .../Schemas/MongoSchemaRepository.cs | 2 +- .../Apps/Utils/AppEventDispatcher.cs | 77 ----- .../Rules/Utils/RuleEventDispatcher.cs | 45 --- .../Schemas/Utils/SchemaEventDispatcher.cs | 133 -------- .../Apps/AppEntityExtensions.cs | 20 -- .../Apps/AppHistoryEventsCreator.cs | 145 --------- .../Apps/IAppEntity.cs | 29 -- .../Apps/Services/IAppLimitsPlan.cs | 25 -- .../Apps/Services/IAppPlanBillingManager.cs | 22 -- .../Apps/Services/IAppPlansProvider.cs | 27 -- .../Apps/Services/IChangePlanResult.cs | 14 - .../Implementations/ConfigAppLimitsPlan.cs | 30 -- .../Implementations/ConfigAppPlansProvider.cs | 86 ----- .../NoopAppPlanBillingManager.cs | 31 -- .../Apps/Services/PlanChangeAsyncResult.cs | 19 -- .../Apps/Services/PlanChangedResult.cs | 19 -- .../Apps/Services/RedirectToCheckoutResult.cs | 25 -- .../Assets/IAssetEntity.cs | 25 -- .../Assets/IAssetEventConsumer.cs | 16 - .../Assets/IAssetStatsEntity.cs | 21 -- .../Assets/Repositories/IAssetRepository.cs | 23 -- .../Repositories/IAssetStatsRepository.cs | 21 -- .../CachingProviderBase.cs | 30 -- .../Contents/ContentHistoryEventsCreator.cs | 49 --- .../Contents/ContentQueryService.cs | 216 ------------- .../Contents/Edm/EdmModelBuilder.cs | 74 ----- .../Contents/Edm/EdmModelExtensions.cs | 39 --- .../Contents/GraphQL/CachingGraphQLService.cs | 85 ----- .../Contents/GraphQL/GraphQLModel.cs | 229 ------------- .../Contents/GraphQL/GraphQLQuery.cs | 23 -- .../Contents/GraphQL/GraphQLQueryContext.cs | 67 ---- .../Contents/GraphQL/IGraphQLContext.cs | 38 --- .../Contents/GraphQL/IGraphQLService.cs | 19 -- .../Contents/GraphQL/IGraphQLUrlGenerator.cs | 27 -- .../Contents/GraphQL/Types/AssetGraphType.cs | 170 ---------- .../GraphQL/Types/ContentDataGraphType.cs | 68 ---- .../GraphQL/Types/ContentGraphType.cs | 112 ------- .../GraphQL/Types/ContentQueryGraphType.cs | 192 ----------- .../Contents/GraphQL/Types/NoopGraphType.cs | 37 --- .../Contents/IContentEntity.cs | 25 -- .../Contents/IContentQueryService.cs | 28 -- .../Contents/QueryContext.cs | 146 --------- .../Repositories/IContentRepository.cs | 33 -- src/Squidex.Domain.Apps.Read/EntityMapper.cs | 90 ------ .../History/HistoryEventToStore.cs | 44 --- .../History/HistoryEventsCreatorBase.cs | 66 ---- .../History/IHistoryEventEntity.cs | 24 -- .../History/IHistoryEventsCreator.cs | 21 -- .../Repositories/IHistoryEventRepository.cs | 19 -- src/Squidex.Domain.Apps.Read/IAppProvider.cs | 34 -- src/Squidex.Domain.Apps.Read/IEntity.cs | 22 -- .../IEntityWithAppRef.cs | 17 - .../IEntityWithCreatedBy.cs | 17 - .../IEntityWithLastModifiedBy.cs | 17 - .../IEntityWithVersion.cs | 15 - .../IUpdateableEntityWithAppRef.cs | 17 - .../IUpdateableEntityWithCreatedBy.cs | 17 - .../IUpdateableEntityWithLastModifiedBy.cs | 17 - .../IUpdateableEntityWithVersion.cs | 15 - .../Rules/IRuleEntity.cs | 22 -- .../Rules/IRuleEventEntity.cs | 29 -- .../Repositories/IRuleEventRepository.cs | 35 -- .../Rules/RuleDequeuer.cs | 157 --------- .../Rules/RuleEnqueuer.cs | 73 ----- .../Rules/RuleJobResult.cs | 18 -- .../Schemas/ISchemaEntity.cs | 38 --- .../Schemas/SchemaHistoryEventsCreator.cs | 88 ----- .../Squidex.Domain.Apps.Read.csproj | 27 -- .../State/AppProvider.cs | 87 ----- .../State/AppStateEventConsumer.cs | 70 ---- .../State/Grains/AppStateGrain.cs | 134 -------- .../State/Grains/AppStateGrainState.cs | 75 ----- .../State/Grains/AppStateGrainState_Apps.cs | 117 ------- .../State/Grains/AppStateGrainState_Rules.cs | 65 ---- .../Grains/AppStateGrainState_Schemas.cs | 162 ---------- .../State/Grains/AppUserGrain.cs | 48 --- .../State/Grains/AppUserGrainState.cs | 30 -- .../State/Grains/JsonAppEntity.cs | 38 --- .../State/Grains/JsonEntity.cs | 35 -- .../State/Grains/JsonRuleEntity.cs | 36 --- .../State/Grains/JsonSchemaEntity.cs | 63 ---- .../AppAggregateCommand.cs | 21 -- src/Squidex.Domain.Apps.Write/AppCommand.cs | 18 -- .../Apps/AppCommandMiddleware.cs | 174 ---------- .../Apps/AppDomainObject.cs | 260 --------------- .../Apps/Commands/AddLanguage.cs | 17 - .../Apps/Commands/AssignContributor.cs | 19 -- .../Apps/Commands/AttachClient.cs | 19 -- .../Apps/Commands/ChangePlan.cs | 17 - .../Apps/Commands/CreateApp.cs | 30 -- .../Apps/Commands/RemoveContributor.cs | 15 - .../Apps/Commands/RemoveLanguage.cs | 17 - .../Apps/Commands/RevokeClient.cs | 15 - .../Apps/Commands/UpdateClient.cs | 21 -- .../Apps/Commands/UpdateLanguage.cs | 24 -- .../Apps/Guards/GuardApp.cs | 66 ---- .../Apps/Guards/GuardAppClients.cs | 102 ------ .../Apps/Guards/GuardAppContributors.cs | 82 ----- .../Apps/Guards/GuardAppLanguages.cs | 100 ------ .../Assets/AssetCommandMiddleware.cs | 117 ------- .../Assets/AssetDomainObject.cs | 150 --------- .../Assets/AssetSavedResult.cs | 23 -- .../Assets/Commands/AssetAggregateCommand.cs | 23 -- .../Assets/Commands/CreateAsset.cs | 25 -- .../Assets/Commands/DeleteAsset.cs | 14 - .../Assets/Commands/RenameAsset.cs | 15 - .../Assets/Commands/UpdateAsset.cs | 19 -- .../Assets/Guards/GuardAsset.cs | 49 --- .../Contents/Commands/ChangeContentStatus.cs | 17 - .../Contents/Commands/ContentCommand.cs | 26 -- .../Contents/Commands/ContentDataCommand.cs | 17 - .../Contents/Commands/CreateContent.cs | 15 - .../Contents/Commands/DeleteContent.cs | 14 - .../Contents/Commands/PatchContent.cs | 14 - .../Contents/Commands/UpdateContent.cs | 14 - .../Contents/ContentCommandMiddleware.cs | 159 --------- .../Contents/ContentDataChangedResult.cs | 24 -- .../Contents/ContentDomainObject.cs | 149 --------- .../Contents/ContentOperationContext.cs | 141 -------- .../Contents/ContentVersionLoader.cs | 87 ----- .../Contents/Guards/GuardContent.cs | 74 ----- .../Contents/IContentVersionLoader.cs | 19 -- .../Rules/Commands/CreateRule.cs | 20 -- .../Rules/Commands/DeleteRule.cs | 14 - .../Rules/Commands/DisableRule.cs | 14 - .../Rules/Commands/EnableRule.cs | 14 - .../Rules/Commands/RuleAggregateCommand.cs | 23 -- .../Rules/Commands/RuleEditCommand.cs | 19 -- .../Rules/Commands/UpdateRule.cs | 14 - .../Rules/Guards/GuardRule.cs | 107 ------ .../Rules/Guards/RuleActionValidator.cs | 40 --- .../Rules/Guards/RuleTriggerValidator.cs | 56 ---- .../Rules/RuleCommandMiddleware.cs | 93 ------ .../Rules/RuleDomainObject.cs | 118 ------- .../SchemaAggregateCommand.cs | 21 -- .../SchemaCommand.cs | 18 -- .../Schemas/Commands/AddField.cs | 21 -- .../Schemas/Commands/ConfigureScripts.cs | 23 -- .../Schemas/Commands/CreateSchema.cs | 36 --- .../Schemas/Commands/CreateSchemaField.cs | 27 -- .../Schemas/Commands/DeleteField.cs | 14 - .../Schemas/Commands/DeleteSchema.cs | 14 - .../Schemas/Commands/DisableField.cs | 14 - .../Schemas/Commands/EnableField.cs | 14 - .../Schemas/Commands/FieldCommand.cs | 15 - .../Schemas/Commands/HideField.cs | 14 - .../Schemas/Commands/LockField.cs | 14 - .../Schemas/Commands/PublishSchema.cs | 14 - .../Schemas/Commands/ReorderFields.cs | 17 - .../Schemas/Commands/ShowField.cs | 14 - .../Schemas/Commands/UnpublishSchema.cs | 14 - .../Schemas/Commands/UpdateField.cs | 17 - .../Schemas/Commands/UpdateSchema.cs | 17 - .../Guards/FieldPropertiesValidator.cs | 232 ------------- .../Schemas/Guards/GuardSchema.cs | 129 -------- .../Schemas/Guards/GuardSchemaField.cs | 160 --------- .../Schemas/SchemaCommandMiddleware.cs | 198 ------------ .../Schemas/SchemaDomainObject.cs | 304 ------------------ .../Squidex.Domain.Apps.Write.csproj | 26 -- .../SquidexCommand.cs | 20 -- .../Migrations/MongoMigrationStatus.cs | 4 +- .../MongoDb/MongoRepositoryBase.cs | 2 +- .../RedisSubscription.cs | 6 +- .../Commands/DomainObjectBase.cs | 7 + .../Commands/LogCommandMiddleware.cs | 10 +- .../Grains/EventConsumerGrain.cs | 10 +- .../Migrations/Migrator.cs | 10 +- .../Config/Domain/InfrastructureServices.cs | 4 + src/Squidex/Config/Domain/ReadServices.cs | 7 +- .../Config/Domain/SerializationServices.cs | 12 +- src/Squidex/Config/Domain/StoreServices.cs | 2 +- src/Squidex/Config/Domain/WriteServices.cs | 24 +- src/Squidex/Config/ServiceExtensions.cs | 19 ++ src/Squidex/Squidex.csproj | 1 + src/Squidex/WebStartup.cs | 2 +- tools/Migrate_01/Migrate_01.csproj | 14 + tools/Migrate_01/Migration01.cs | 129 ++++++++ .../Migrate_01/OldEvents}/AppClientChanged.cs | 6 +- .../Migrate_01/OldEvents}/ContentArchived.cs | 3 +- .../Migrate_01/OldEvents}/ContentPublished.cs | 3 +- .../Migrate_01/OldEvents}/ContentRestored.cs | 3 +- .../OldEvents}/ContentUnpublished.cs | 3 +- .../Migrate_01/OldEvents}/WebhookAdded.cs | 3 +- .../Migrate_01/OldEvents}/WebhookDeleted.cs | 3 +- 189 files changed, 270 insertions(+), 8734 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/AppHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/IAppLimitsPlan.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppLimitsPlan.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Assets/IAssetStatsEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetStatsRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/CachingProviderBase.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQueryContext.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLService.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/QueryContext.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/EntityMapper.cs delete mode 100644 src/Squidex.Domain.Apps.Read/History/HistoryEventToStore.cs delete mode 100644 src/Squidex.Domain.Apps.Read/History/HistoryEventsCreatorBase.cs delete mode 100644 src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/History/IHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Read/History/Repositories/IHistoryEventRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IAppProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IEntityWithLastModifiedBy.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs delete mode 100644 src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Schemas/SchemaHistoryEventsCreator.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj delete mode 100644 src/Squidex.Domain.Apps.Read/State/AppProvider.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Write/AppAggregateCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/AppCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/AssetSavedResult.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/Commands/AssetAggregateCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/Commands/CreateAsset.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/Commands/DeleteAsset.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/Commands/UpdateAsset.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/DeleteContent.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentDataChangedResult.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs delete mode 100644 src/Squidex.Domain.Apps.Write/SchemaAggregateCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/SchemaCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/DisableField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/EnableField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/FieldCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/HideField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/PublishSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/ShowField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/UnpublishSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj delete mode 100644 src/Squidex.Domain.Apps.Write/SquidexCommand.cs create mode 100644 tools/Migrate_01/Migrate_01.csproj create mode 100644 tools/Migrate_01/Migration01.cs rename {src/Squidex.Domain.Apps.Events/Apps/Old => tools/Migrate_01/OldEvents}/AppClientChanged.cs (87%) rename {src/Squidex.Domain.Apps.Events/Contents/Old => tools/Migrate_01/OldEvents}/ContentArchived.cs (91%) rename {src/Squidex.Domain.Apps.Events/Contents/Old => tools/Migrate_01/OldEvents}/ContentPublished.cs (91%) rename {src/Squidex.Domain.Apps.Events/Contents/Old => tools/Migrate_01/OldEvents}/ContentRestored.cs (91%) rename {src/Squidex.Domain.Apps.Events/Contents/Old => tools/Migrate_01/OldEvents}/ContentUnpublished.cs (91%) rename {src/Squidex.Domain.Apps.Events/Schemas/Old => tools/Migrate_01/OldEvents}/WebhookAdded.cs (90%) rename {src/Squidex.Domain.Apps.Events/Schemas/Old => tools/Migrate_01/OldEvents}/WebhookDeleted.cs (89%) diff --git a/Squidex.sln b/Squidex.sln index cb23f22ff..72eff25c0 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2009 +VisualStudioVersion = 15.0.27130.2003 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}" EndProject @@ -12,22 +12,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Events", "src\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj", "{25F66C64-058A-4D44-BC0C-F12A054F9A91}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Write", "src\Squidex.Domain.Apps.Write\Squidex.Domain.Apps.Write.csproj", "{A85201C6-6AF8-4B63-8365-08F741050438}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Read", "src\Squidex.Domain.Apps.Read\Squidex.Domain.Apps.Read.csproj", "{A92B4734-2587-4F6F-97A3-741BE48709A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Read.MongoDb", "src\Squidex.Domain.Apps.Read.MongoDb\Squidex.Domain.Apps.Read.MongoDb.csproj", "{28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Write.Tests", "tests\Squidex.Domain.Apps.Write.Tests\Squidex.Domain.Apps.Write.Tests.csproj", "{9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.Tests", "tests\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj", "{7FD0A92B-7862-4BB1-932B-B52A9CACB56B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Tests", "tests\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj", "{FD0AFD44-7A93-4F9E-B5ED-72582392E435}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.MongoDb", "src\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj", "{6A811927-3C37-430A-90F4-503E37123956}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Read.Tests", "tests\Squidex.Domain.Apps.Read.Tests\Squidex.Domain.Apps.Read.Tests.csproj", "{8B074219-F69A-4E41-83C6-12EE1E647779}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.Redis", "src\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj", "{D7166C56-178A-4457-B56A-C615C7450DEE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.RabbitMq", "src\Squidex.Infrastructure.RabbitMq\Squidex.Infrastructure.RabbitMq.csproj", "{C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}" @@ -63,14 +53,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Mo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities", "src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj", "{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.Tests", "tests\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj", "{AA003372-CD8D-4DBC-962C-F61E0C93CF05}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.MongoDb", "src\Squidex.Domain.Apps.Entities.MongoDb\Squidex.Domain.Apps.Entities.MongoDb.csproj", "{7DA5B308-D950-4496-93D5-21D6C4D91644}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{A4823E14-C0E5-4A4D-B28F-27424C25C3C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -105,38 +95,6 @@ Global {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Release|Any CPU.Build.0 = Release|Any CPU {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Release|x64.ActiveCfg = Release|Any CPU {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Release|x86.ActiveCfg = Release|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Debug|x64.ActiveCfg = Debug|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Debug|x86.ActiveCfg = Debug|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Release|Any CPU.Build.0 = Release|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Release|x64.ActiveCfg = Release|Any CPU - {A85201C6-6AF8-4B63-8365-08F741050438}.Release|x86.ActiveCfg = Release|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Debug|x64.ActiveCfg = Debug|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Debug|x86.ActiveCfg = Debug|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Release|Any CPU.Build.0 = Release|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Release|x64.ActiveCfg = Release|Any CPU - {A92B4734-2587-4F6F-97A3-741BE48709A5}.Release|x86.ActiveCfg = Release|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Debug|x64.ActiveCfg = Debug|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Debug|x86.ActiveCfg = Debug|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Release|Any CPU.Build.0 = Release|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Release|x64.ActiveCfg = Release|Any CPU - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD}.Release|x86.ActiveCfg = Release|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Debug|x64.ActiveCfg = Debug|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Debug|x86.ActiveCfg = Debug|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Release|Any CPU.Build.0 = Release|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Release|x64.ActiveCfg = Release|Any CPU - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203}.Release|x86.ActiveCfg = Release|Any CPU {7FD0A92B-7862-4BB1-932B-B52A9CACB56B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7FD0A92B-7862-4BB1-932B-B52A9CACB56B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7FD0A92B-7862-4BB1-932B-B52A9CACB56B}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -161,14 +119,6 @@ Global {6A811927-3C37-430A-90F4-503E37123956}.Release|Any CPU.Build.0 = Release|Any CPU {6A811927-3C37-430A-90F4-503E37123956}.Release|x64.ActiveCfg = Release|Any CPU {6A811927-3C37-430A-90F4-503E37123956}.Release|x86.ActiveCfg = Release|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Debug|x64.ActiveCfg = Debug|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Debug|x86.ActiveCfg = Debug|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Release|Any CPU.Build.0 = Release|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Release|x64.ActiveCfg = Release|Any CPU - {8B074219-F69A-4E41-83C6-12EE1E647779}.Release|x86.ActiveCfg = Release|Any CPU {D7166C56-178A-4457-B56A-C615C7450DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D7166C56-178A-4457-B56A-C615C7450DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7166C56-178A-4457-B56A-C615C7450DEE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -309,18 +259,6 @@ Global {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.Build.0 = Release|Any CPU {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.ActiveCfg = Release|Any CPU {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.Build.0 = Release|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x64.ActiveCfg = Debug|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x64.Build.0 = Debug|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x86.ActiveCfg = Debug|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x86.Build.0 = Debug|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|Any CPU.Build.0 = Release|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.ActiveCfg = Release|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.Build.0 = Release|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.ActiveCfg = Release|Any CPU - {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.Build.0 = Release|Any CPU {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -357,6 +295,18 @@ Global {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x64.Build.0 = Release|Any CPU {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x86.ActiveCfg = Release|Any CPU {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x86.Build.0 = Release|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Debug|x64.Build.0 = Debug|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Debug|x86.Build.0 = Debug|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|Any CPU.Build.0 = Release|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x64.ActiveCfg = Release|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x64.Build.0 = Release|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x86.ActiveCfg = Release|Any CPU + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -364,14 +314,9 @@ Global GlobalSection(NestedProjects) = preSolution {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {C9809D59-6665-471E-AD87-5AC624C65892} - {A85201C6-6AF8-4B63-8365-08F741050438} = {C9809D59-6665-471E-AD87-5AC624C65892} - {A92B4734-2587-4F6F-97A3-741BE48709A5} = {C9809D59-6665-471E-AD87-5AC624C65892} - {28F8E9E2-FE24-41F7-A888-9FC244A9E2DD} = {C9809D59-6665-471E-AD87-5AC624C65892} - {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203} = {C9809D59-6665-471E-AD87-5AC624C65892} {7FD0A92B-7862-4BB1-932B-B52A9CACB56B} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {C9809D59-6665-471E-AD87-5AC624C65892} {6A811927-3C37-430A-90F4-503E37123956} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} - {8B074219-F69A-4E41-83C6-12EE1E647779} = {C9809D59-6665-471E-AD87-5AC624C65892} {D7166C56-178A-4457-B56A-C615C7450DEE} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {945871B1-77B8-43FB-B53C-27CF385AB756} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} @@ -389,6 +334,7 @@ Global {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892} {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892} {7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892} + {A4823E14-C0E5-4A4D-B28F-27424C25C3C7} = {94207AA6-4923-4183-A558-E0F8196B8CA3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 77fec730e..1adb16a06 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps protected override string CollectionName() { - return "Snapshots_Apps"; + return "States_Apps"; } protected override async Task SetupCollectionAsync(IMongoCollection collection) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 440d82e3f..f632f8d81 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets protected override string CollectionName() { - return "Snapshots_Assets"; + return "States_Assets"; } protected override Task SetupCollectionAsync(IMongoCollection collection) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index ca185a3c0..05ea8586e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents protected override string CollectionName() { - return "Snapshots_Contents"; + return "States_Contents"; } protected override async Task SetupCollectionAsync(IMongoCollection collection) @@ -96,7 +96,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents try { await Collection.InsertOneAsync(document); - await Collection.UpdateManyAsync(x => x.Id == value.Id && x.Version < value.Version, Update.Set(x => x.IsLatest, false)); } catch (MongoWriteException ex) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 4ad5b8bd8..ff579d0f3 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules protected override string CollectionName() { - return "Snapshots_Rules"; + return "States_Rules"; } protected override async Task SetupCollectionAsync(IMongoCollection collection) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 5f373c18c..9da25e5e7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas protected override string CollectionName() { - return "Snapshots_Schemas"; + return "States_Schemas"; } protected override async Task SetupCollectionAsync(IMongoCollection collection) diff --git a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs deleted file mode 100644 index 5da60f86e..000000000 --- a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// AppEventDispatcher.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; - -namespace Squidex.Domain.Apps.Events.Apps.Utils -{ - public static class AppEventDispatcher - { - public static AppContributors Apply(this AppContributors contributors, AppContributorRemoved @event) - { - return contributors.Remove(@event.ContributorId); - } - - public static AppContributors Apply(this AppContributors contributors, AppContributorAssigned @event) - { - return contributors.Assign(@event.ContributorId, @event.Permission); - } - - public static LanguagesConfig Apply(this LanguagesConfig languagesConfig, AppLanguageAdded @event) - { - return languagesConfig.Set(new LanguageConfig(@event.Language)); - } - - public static LanguagesConfig Apply(this LanguagesConfig languagesConfig, AppLanguageRemoved @event) - { - return languagesConfig.Remove(@event.Language); - } - - public static AppClients Apply(this AppClients clients, AppClientAttached @event) - { - return clients.Add(@event.Id, @event.Secret); - } - - public static AppClients Apply(this AppClients clients, AppClientRevoked @event) - { - return clients.Revoke(@event.Id); - } - - public static AppClients Apply(this AppClients clients, AppClientRenamed @event) - { - return clients.Rename(@event.Id, @event.Name); - } - - public static AppClients Apply(this AppClients clients, AppClientUpdated @event) - { - return clients.Update(@event.Id, @event.Permission); - } - - public static LanguagesConfig Apply(this LanguagesConfig languagesConfig, AppLanguageUpdated @event) - { - var fallback = @event.Fallback; - - if (fallback != null && fallback.Count > 0) - { - var existingLangauges = languagesConfig.OfType().Select(x => x.Language); - - fallback = fallback.Intersect(existingLangauges).ToList(); - } - - languagesConfig = languagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, fallback)); - - if (@event.IsMaster) - { - languagesConfig = languagesConfig.MakeMaster(@event.Language); - } - - return languagesConfig; - } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs deleted file mode 100644 index e85f4eecf..000000000 --- a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// RuleEventDispatcher.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Events.Rules.Utils -{ - public static class RuleEventDispatcher - { - public static Rule Create(RuleCreated @event) - { - return new Rule(@event.Trigger, @event.Action); - } - - public static Rule Apply(this Rule rule, RuleUpdated @event) - { - if (@event.Trigger != null) - { - return rule.Update(@event.Trigger); - } - - if (@event.Action != null) - { - return rule.Update(@event.Action); - } - - return rule; - } - - public static Rule Apply(this Rule rule, RuleEnabled @event) - { - return rule.Enable(); - } - - public static Rule Apply(this Rule rule, RuleDisabled @event) - { - return rule.Disable(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs deleted file mode 100644 index 7deea8e63..000000000 --- a/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ========================================================================== -// SchemaEventDispatcher.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Events.Schemas.Utils -{ - public static class SchemaEventDispatcher - { - public static Schema Create(SchemaCreated @event, FieldRegistry registry) - { - var schema = new Schema(@event.Name); - - if (@event.Properties != null) - { - schema = schema.Update(@event.Properties); - } - - if (@event.Fields != null) - { - var fieldId = 1; - - foreach (var eventField in @event.Fields) - { - var partitioning = - string.Equals(eventField.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? - Partitioning.Language : - Partitioning.Invariant; - - var field = registry.CreateField(fieldId, eventField.Name, partitioning, eventField.Properties); - - if (eventField.IsHidden) - { - field = field.Hide(); - } - - if (eventField.IsDisabled) - { - field = field.Disable(); - } - - if (eventField.IsLocked) - { - field = field.Lock(); - } - - schema = schema.AddField(field); - - fieldId++; - } - } - - return schema; - } - - public static Schema Apply(this Schema schema, FieldAdded @event, FieldRegistry registry) - { - var partitioning = - string.Equals(@event.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? - Partitioning.Language : - Partitioning.Invariant; - - var field = registry.CreateField(@event.FieldId.Id, @event.Name, partitioning, @event.Properties); - - schema = schema.DeleteField(@event.FieldId.Id); - schema = schema.AddField(field); - - return schema; - } - - public static Schema Apply(this Schema schema, FieldUpdated @event) - { - return schema.UpdateField(@event.FieldId.Id, @event.Properties); - } - - public static Schema Apply(this Schema schema, FieldLocked @event) - { - return schema.LockField(@event.FieldId.Id); - } - - public static Schema Apply(this Schema schema, FieldHidden @event) - { - return schema.HideField(@event.FieldId.Id); - } - - public static Schema Apply(this Schema schema, FieldShown @event) - { - return schema.ShowField(@event.FieldId.Id); - } - - public static Schema Apply(this Schema schema, FieldDisabled @event) - { - return schema.DisableField(@event.FieldId.Id); - } - - public static Schema Apply(this Schema schema, FieldEnabled @event) - { - return schema.EnableField(@event.FieldId.Id); - } - - public static Schema Apply(this Schema schema, SchemaUpdated @event) - { - return schema.Update(@event.Properties); - } - - public static Schema Apply(this Schema schema, SchemaFieldsReordered @event) - { - return schema.ReorderFields(@event.FieldIds); - } - - public static Schema Apply(this Schema schema, FieldDeleted @event) - { - return schema.DeleteField(@event.FieldId.Id); - } - - public static Schema Apply(this Schema schema, SchemaPublished @event) - { - return schema.Publish(); - } - - public static Schema Apply(this Schema schema, SchemaUnpublished @event) - { - return schema.Unpublish(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs b/src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs deleted file mode 100644 index 39fe7824b..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// AppEntityExtensions.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core; - -namespace Squidex.Domain.Apps.Read.Apps -{ - public static class AppEntityExtensions - { - public static PartitionResolver PartitionResolver(this IAppEntity entity) - { - return entity.LanguagesConfig.ToResolver(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Read/Apps/AppHistoryEventsCreator.cs deleted file mode 100644 index 2e677ba59..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/AppHistoryEventsCreator.cs +++ /dev/null @@ -1,145 +0,0 @@ -// ========================================================================== -// AppHistoryEventsCreator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Read.History; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.Apps -{ - public class AppHistoryEventsCreator : HistoryEventsCreatorBase - { - public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "assigned {user:[Contributor]} as [Permission]"); - - AddEventMessage( - "removed {user:[Contributor]} from app"); - - AddEventMessage( - "added client {[Id]} to app"); - - AddEventMessage( - "revoked client {[Id]}"); - - AddEventMessage( - "updated client {[Id]}"); - - AddEventMessage( - "renamed client {[Id]} to {[Name]}"); - - AddEventMessage( - "added language {[Language]}"); - - AddEventMessage( - "removed language {[Language]}"); - - AddEventMessage( - "updated language {[Language]}"); - - AddEventMessage( - "changed master language to {[Language]}"); - } - - protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) - { - const string channel = "settings.contributors"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Contributor", @event.ContributorId)); - } - - protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers) - { - const string channel = "settings.contributors"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Contributor", @event.ContributorId).AddParameter("Permission", @event.Permission)); - } - - protected Task On(AppClientAttached @event, EnvelopeHeaders headers) - { - const string channel = "settings.clients"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Id", @event.Id)); - } - - protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) - { - const string channel = "settings.clients"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Id", @event.Id)); - } - - protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) - { - const string channel = "settings.clients"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Id", @event.Id).AddParameter("Name", ClientName(@event))); - } - - protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) - { - const string channel = "settings.languages"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Language", @event.Language)); - } - - protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) - { - const string channel = "settings.languages"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Language", @event.Language)); - } - - protected Task On(AppLanguageUpdated @event, EnvelopeHeaders headers) - { - const string channel = "settings.languages"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Language", @event.Language)); - } - - protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers) - { - const string channel = "settings.languages"; - - return Task.FromResult( - ForEvent(@event, channel) - .AddParameter("Language", @event.Language)); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null); - } - - private static string ClientName(AppClientRenamed @event) - { - return !string.IsNullOrWhiteSpace(@event.Name) ? @event.Name : @event.Id; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs deleted file mode 100644 index df1743d69..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// IAppEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; - -namespace Squidex.Domain.Apps.Read.Apps -{ - public interface IAppEntity : IEntity, IEntityWithVersion - { - string Etag { get; } - - string Name { get; } - - string PlanId { get; } - - string PlanOwner { get; } - - AppClients Clients { get; } - - AppContributors Contributors { get; } - - LanguagesConfig LanguagesConfig { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppLimitsPlan.cs deleted file mode 100644 index 3bc0a0012..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppLimitsPlan.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// IAppLimitsPlan.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public interface IAppLimitsPlan - { - string Id { get; } - - string Name { get; } - - string Costs { get; } - - long MaxApiCalls { get; } - - long MaxAssetSize { get; } - - int MaxContributors { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs deleted file mode 100644 index ebb189b2e..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// IAppPlanBillingManager.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public interface IAppPlanBillingManager - { - bool HasPortal { get; } - - Task ChangePlanAsync(string userId, Guid appId, string appName, string planId); - - Task GetPortalLinkAsync(string userId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs deleted file mode 100644 index 5128fdfb2..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// IAppPlansProvider.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public interface IAppPlansProvider - { - IEnumerable GetAvailablePlans(); - - bool IsConfiguredPlan(string planId); - - IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app); - - IAppLimitsPlan GetPlanUpgrade(string planId); - - IAppLimitsPlan GetPlanForApp(IAppEntity app); - - IAppLimitsPlan GetPlan(string planId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs deleted file mode 100644 index 5086dff24..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// IChangePlanResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public interface IChangePlanResult - { - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppLimitsPlan.cs deleted file mode 100644 index 0ed64ddcb..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppLimitsPlan.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// ConfigAppLimitsPlan.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations -{ - public sealed class ConfigAppLimitsPlan : IAppLimitsPlan - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Costs { get; set; } - - public long MaxApiCalls { get; set; } - - public long MaxAssetSize { get; set; } - - public int MaxContributors { get; set; } - - public ConfigAppLimitsPlan Clone() - { - return (ConfigAppLimitsPlan)MemberwiseClone(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs deleted file mode 100644 index ed2e6181a..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// ConfigAppLimitsProvider.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations -{ - public sealed class ConfigAppPlansProvider : IAppPlansProvider - { - private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1 - }; - - private readonly Dictionary plansById; - private readonly List plansList; - - public ConfigAppPlansProvider(IEnumerable config) - { - Guard.NotNull(config, nameof(config)); - - plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList(); - plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); - } - - public IEnumerable GetAvailablePlans() - { - return plansList; - } - - public bool IsConfiguredPlan(string planId) - { - return planId != null && plansById.ContainsKey(planId); - } - - public IAppLimitsPlan GetPlanForApp(IAppEntity app) - { - Guard.NotNull(app, nameof(app)); - - return GetPlan(app.PlanId); - } - - public IAppLimitsPlan GetPlan(string planId) - { - return GetPlanCore(planId); - } - - public IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app) - { - Guard.NotNull(app, nameof(app)); - - return GetPlanUpgrade(app.PlanId); - } - - public IAppLimitsPlan GetPlanUpgrade(string planId) - { - var plan = GetPlanCore(planId); - - var nextPlanIndex = plansList.IndexOf(plan); - - if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) - { - return plansList[nextPlanIndex + 1]; - } - - return null; - } - - private ConfigAppLimitsPlan GetPlanCore(string planId) - { - return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs deleted file mode 100644 index 5f6bdec01..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// NoopAppPlanBillingManager.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations -{ - public sealed class NoopAppPlanBillingManager : IAppPlanBillingManager - { - public bool HasPortal - { - get { return false; } - } - - public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId) - { - return Task.FromResult(PlanChangedResult.Instance); - } - - public Task GetPortalLinkAsync(string userId) - { - return Task.FromResult(string.Empty); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs deleted file mode 100644 index 4f57733e8..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// PlanChangeAsyncResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public sealed class PlanChangeAsyncResult : IChangePlanResult - { - public static readonly PlanChangeAsyncResult Instance = new PlanChangeAsyncResult(); - - private PlanChangeAsyncResult() - { - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs deleted file mode 100644 index 1456823c5..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// PlanChangedResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public sealed class PlanChangedResult : IChangePlanResult - { - public static readonly PlanChangedResult Instance = new PlanChangedResult(); - - private PlanChangedResult() - { - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs deleted file mode 100644 index 21b3043d1..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// RedirectToCheckoutResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Apps.Services -{ - public sealed class RedirectToCheckoutResult : IChangePlanResult - { - public Uri Url { get; } - - public RedirectToCheckoutResult(Uri url) - { - Guard.NotNull(url, nameof(url)); - - Url = url; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs deleted file mode 100644 index 9a502ed7b..000000000 --- a/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// IAssetEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.ValidateContent; - -namespace Squidex.Domain.Apps.Read.Assets -{ - public interface IAssetEntity : - IEntity, - IEntityWithAppRef, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion, - IAssetInfo - { - string MimeType { get; } - - long FileVersion { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs b/src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs deleted file mode 100644 index bd0a6ddb9..000000000 --- a/src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// IAssetEventConsumer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.Assets -{ - public interface IAssetEventConsumer : IEventConsumer - { - } -} diff --git a/src/Squidex.Domain.Apps.Read/Assets/IAssetStatsEntity.cs b/src/Squidex.Domain.Apps.Read/Assets/IAssetStatsEntity.cs deleted file mode 100644 index 05f7b342d..000000000 --- a/src/Squidex.Domain.Apps.Read/Assets/IAssetStatsEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// IAssetStatsEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Read.Assets -{ - public interface IAssetStatsEntity - { - DateTime Date { get; } - - long TotalSize { get; } - - long TotalCount { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetRepository.cs deleted file mode 100644 index 48c2ce66e..000000000 --- a/src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// IAssetRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Assets.Repositories -{ - public interface IAssetRepository - { - Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0); - - Task FindAssetAsync(Guid id); - - Task CountAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetStatsRepository.cs deleted file mode 100644 index 09159069b..000000000 --- a/src/Squidex.Domain.Apps.Read/Assets/Repositories/IAssetStatsRepository.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// IAssetStatsRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Assets.Repositories -{ - public interface IAssetStatsRepository - { - Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate); - - Task GetTotalSizeAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs b/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs deleted file mode 100644 index 5883b29ab..000000000 --- a/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// CachingProviderBase.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Microsoft.Extensions.Caching.Memory; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read -{ - public abstract class CachingProviderBase - { - private readonly IMemoryCache cache; - - protected IMemoryCache Cache - { - get { return cache; } - } - - protected CachingProviderBase(IMemoryCache cache) - { - Guard.NotNull(cache, nameof(cache)); - - this.cache = cache; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs deleted file mode 100644 index ab734aab4..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// ContentHistoryEventsCreator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.History; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase - { - public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "created content item."); - - AddEventMessage( - "updated content item."); - - AddEventMessage( - "deleted content item."); - - AddEventMessage( - "changed status of content item to {[Status]}."); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - var channel = $"contents.{@event.Headers.AggregateId()}"; - - var result = ForEvent(@event.Payload, channel); - - if (@event.Payload is ContentStatusChanged contentStatusChanged) - { - result = result.AddParameter("Status", contentStatusChanged.Status); - } - - return Task.FromResult(result); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs deleted file mode 100644 index 46bb4848e..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs +++ /dev/null @@ -1,216 +0,0 @@ -// ========================================================================== -// ContentQueryService.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.OData; -using Microsoft.OData.UriParser; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Contents.Edm; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Security; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public sealed class ContentQueryService : IContentQueryService - { - private readonly IContentRepository contentRepository; - private readonly IAppProvider appProvider; - private readonly IScriptEngine scriptEngine; - private readonly EdmModelBuilder modelBuilder; - - public ContentQueryService( - IContentRepository contentRepository, - IAppProvider appProvider, - IScriptEngine scriptEngine, - EdmModelBuilder modelBuilder) - { - Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(modelBuilder, nameof(modelBuilder)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.contentRepository = contentRepository; - this.appProvider = appProvider; - this.scriptEngine = scriptEngine; - this.modelBuilder = modelBuilder; - } - - public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(user, nameof(user)); - Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); - - var isFrontendClient = user.IsInClient("squidex-frontend"); - - var schema = await FindSchemaAsync(app, schemaIdOrName); - - var content = await contentRepository.FindContentAsync(app, schema, id); - - if (content == null || (content.Status != Status.Published && !isFrontendClient)) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); - } - - content = TransformContent(user, schema, new List { content })[0]; - - return (schema, content); - } - - public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(user, nameof(user)); - Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); - - var schema = await FindSchemaAsync(app, schemaIdOrName); - - var parsedQuery = ParseQuery(app, query, schema); - - var status = ParseStatus(user, archived); - - var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), parsedQuery); - var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), parsedQuery); - - await Task.WhenAll(taskForItems, taskForCount); - - var list = TransformContent(user, schema, taskForItems.Result.ToList()); - - return (schema, taskForCount.Result, list); - } - - public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet ids) - { - Guard.NotNull(ids, nameof(ids)); - Guard.NotNull(app, nameof(app)); - Guard.NotNull(user, nameof(user)); - Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); - - var schema = await FindSchemaAsync(app, schemaIdOrName); - - var status = ParseStatus(user, archived); - - var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), ids); - var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), ids); - - await Task.WhenAll(taskForItems, taskForCount); - - var list = TransformContent(user, schema, taskForItems.Result.ToList()); - - return (schema, taskForCount.Result, list); - } - - private List TransformContent(ClaimsPrincipal user, ISchemaEntity schema, List contents) - { - var scriptText = schema.ScriptQuery; - - if (!string.IsNullOrWhiteSpace(scriptText)) - { - for (var i = 0; i < contents.Count; i++) - { - var content = contents[i]; - var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText); - - contents[i] = SimpleMapper.Map(content, new Content { Data = contentData }); - } - } - - return contents; - } - - private ODataUriParser ParseQuery(IAppEntity app, string query, ISchemaEntity schema) - { - try - { - var model = modelBuilder.BuildEdmModel(schema, app); - - return model.ParseQuery(query); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - } - - public async Task FindSchemaAsync(IAppEntity app, string schemaIdOrName) - { - Guard.NotNull(app, nameof(app)); - - ISchemaEntity schema = null; - - if (Guid.TryParse(schemaIdOrName, out var id)) - { - schema = await appProvider.GetSchemaAsync(app.Name, id); - } - - if (schema == null) - { - schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); - } - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); - } - - return schema; - } - - private static List ParseStatus(ClaimsPrincipal user, bool archived) - { - var status = new List(); - - if (user.IsInClient("squidex-frontend")) - { - if (archived) - { - status.Add(Status.Archived); - } - else - { - status.Add(Status.Draft); - status.Add(Status.Published); - } - } - else - { - status.Add(Status.Published); - } - - return status; - } - - private sealed class Content : IContentEntity - { - public Guid Id { get; set; } - public Guid AppId { get; set; } - - public long Version { get; set; } - - public Instant Created { get; set; } - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - public RefToken LastModifiedBy { get; set; } - - public NamedContentData Data { get; set; } - - public Status Status { get; set; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs deleted file mode 100644 index 696d5282d..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// EdmModelBuilder.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.OData.Edm; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.GenerateEdmSchema; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.Edm -{ - public class EdmModelBuilder : CachingProviderBase - { - public EdmModelBuilder(IMemoryCache cache) - : base(cache) - { - } - - public virtual IEdmModel BuildEdmModel(ISchemaEntity schema, IAppEntity app) - { - Guard.NotNull(schema, nameof(schema)); - - var cacheKey = $"{schema.Id}_{schema.Version}_{app.Id}_{app.Version}"; - - var result = Cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); - - return BuildEdmModel(schema.SchemaDef, app.PartitionResolver()); - }); - - return result; - } - - private static EdmModel BuildEdmModel(Schema schema, PartitionResolver partitionResolver) - { - var model = new EdmModel(); - - var container = new EdmEntityContainer("Squidex", "Container"); - - var schemaType = schema.BuildEdmType(partitionResolver, x => - { - model.AddElement(x); - - return x; - }); - - var entityType = new EdmEntityType("Squidex", schema.Name); - entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); - entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32); - entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String); - - model.AddElement(container); - model.AddElement(schemaType); - model.AddElement(entityType); - - container.AddEntitySet("ContentSet", entityType); - - return model; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs deleted file mode 100644 index 7da78c5be..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// EdmModelExtensions.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; - -namespace Squidex.Domain.Apps.Read.Contents.Edm -{ - public static class EdmModelExtensions - { - public static ODataUriParser ParseQuery(this IEdmModel model, string query) - { - if (!model.EntityContainer.EntitySets().Any()) - { - return null; - } - - query = query ?? string.Empty; - - var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); - - if (query.StartsWith("?", StringComparison.Ordinal)) - { - query = query.Substring(1); - } - - var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); - - return parser; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs deleted file mode 100644 index 17d31bb7d..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// CachingGraphQLService.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(60); - private readonly IContentQueryService contentQuery; - private readonly IGraphQLUrlGenerator urlGenerator; - private readonly IAssetRepository assetRepository; - private readonly IAppProvider appProvider; - - public CachingGraphQLService(IMemoryCache cache, - IAppProvider appProvider, - IAssetRepository assetRepository, - IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator) - : base(cache) - { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(contentQuery, nameof(urlGenerator)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - - this.appProvider = appProvider; - this.assetRepository = assetRepository; - this.contentQuery = contentQuery; - this.urlGenerator = urlGenerator; - } - - public async Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query) - { - Guard.NotNull(app, nameof(app)); - Guard.NotNull(query, nameof(query)); - - if (string.IsNullOrWhiteSpace(query.Query)) - { - return (new object(), new object[0]); - } - - var modelContext = await GetModelAsync(app); - var queryContext = new GraphQLQueryContext(app, assetRepository, contentQuery, user, urlGenerator); - - return await modelContext.ExecuteAsync(queryContext, query); - } - - private async Task GetModelAsync(IAppEntity app) - { - var cacheKey = CreateCacheKey(app.Id, app.Etag); - - var modelContext = Cache.Get(cacheKey); - - if (modelContext == null) - { - var allSchemas = await appProvider.GetSchemasAsync(app.Name); - - modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); - - Cache.Set(cacheKey, modelContext, CacheDuration); - } - - return modelContext; - } - - private static object CreateCacheKey(Guid appId, string etag) - { - return $"GraphQLModel_{appId}_{etag}"; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs deleted file mode 100644 index 292cc138b..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs +++ /dev/null @@ -1,229 +0,0 @@ -// ========================================================================== -// GraphQLContext.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using GraphQL; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using GraphQLSchema = GraphQL.Types.Schema; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public sealed class GraphQLModel : IGraphQLContext - { - private readonly Dictionary> fieldInfos; - private readonly Dictionary schemaTypes = new Dictionary(); - private readonly Dictionary schemas; - private readonly PartitionResolver partitionResolver; - private readonly IAppEntity app; - private readonly IGraphType assetType; - private readonly IGraphType assetListType; - private readonly GraphQLSchema graphQLSchema; - - public bool CanGenerateAssetSourceUrl { get; } - - public GraphQLModel(IAppEntity app, IEnumerable schemas, IGraphQLUrlGenerator urlGenerator) - { - this.app = app; - - CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; - - partitionResolver = app.PartitionResolver(); - - assetType = new AssetGraphType(this); - assetListType = new ListGraphType(new NonNullGraphType(assetType)); - - fieldInfos = new Dictionary> - { - { - typeof(StringField), - field => ResolveDefault("String") - }, - { - typeof(BooleanField), - field => ResolveDefault("Boolean") - }, - { - typeof(NumberField), - field => ResolveDefault("Float") - }, - { - typeof(DateTimeField), - field => ResolveDefault("Date") - }, - { - typeof(JsonField), - field => ResolveDefault("Json") - }, - { - typeof(TagsField), - field => ResolveDefault("String") - }, - { - typeof(GeolocationField), - field => ResolveDefault("Geolocation") - }, - { - typeof(AssetsField), - field => ResolveAssets(assetListType) - }, - { - typeof(ReferencesField), - field => ResolveReferences(field) - } - }; - - this.schemas = schemas.ToDictionary(x => x.Id); - - graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) }; - - foreach (var schemaType in schemaTypes.Values) - { - schemaType.Initialize(); - } - } - - private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(string name) - { - return (new NoopGraphType(name), new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName))); - } - - public IFieldResolver ResolveAssetUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetSourceUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetThumbnailUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - - return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); - }); - - return resolver; - } - - public IFieldResolver ResolveContentUrl(ISchemaEntity schema) - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - - return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); - }); - - return resolver; - } - - private static ValueTuple ResolveAssets(IGraphType assetListType) - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentIds = c.Source.GetOrDefault(c.FieldName); - - return context.GetReferencedAssetsAsync(contentIds); - }); - - return (assetListType, resolver); - } - - private ValueTuple ResolveReferences(Field field) - { - var schemaId = ((ReferencesField)field).Properties.SchemaId; - var schemaType = GetSchemaType(schemaId); - - if (schemaType == null) - { - return (null, null); - } - - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentIds = c.Source.GetOrDefault(c.FieldName); - - return context.GetReferencedContentsAsync(schemaId, contentIds); - }); - - var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); - - return (schemaFieldType, resolver); - } - - public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLQueryContext context, GraphQLQuery query) - { - Guard.NotNull(context, nameof(context)); - - var result = await new DocumentExecuter().ExecuteAsync(options => - { - options.Query = query.Query; - options.Schema = graphQLSchema; - options.Inputs = query.Variables?.ToInputs() ?? new Inputs(); - options.UserContext = context; - options.OperationName = query.OperationName; - }).ConfigureAwait(false); - - return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); - } - - public IFieldPartitioning ResolvePartition(Partitioning key) - { - return partitionResolver(key); - } - - public IGraphType GetAssetType() - { - return assetType; - } - - public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) - { - return fieldInfos[field.GetType()](field); - } - - public IGraphType GetSchemaType(Guid schemaId) - { - var schema = schemas.GetOrDefault(schemaId); - - return schema != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schema, this)) : null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs deleted file mode 100644 index 476230a3a..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// GraphQLQuery.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Newtonsoft.Json.Linq; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public class GraphQLQuery - { - public string OperationName { get; set; } - - public string NamedQuery { get; set; } - - public string Query { get; set; } - - public JObject Variables { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQueryContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQueryContext.cs deleted file mode 100644 index e1abc0316..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQueryContext.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// GraphQLQueryContext.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public sealed class GraphQLQueryContext : QueryContext - { - public IGraphQLUrlGenerator UrlGenerator { get; } - - public GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user, - IGraphQLUrlGenerator urlGenerator) - : base(app, assetRepository, contentQuery, user) - { - UrlGenerator = urlGenerator; - } - - public Task> GetReferencedAssetsAsync(JToken value) - { - var ids = ParseIds(value); - - return GetReferencedAssetsAsync(ids); - } - - public Task> GetReferencedContentsAsync(Guid schemaId, JToken value) - { - var ids = ParseIds(value); - - return GetReferencedContentsAsync(schemaId, ids); - } - - private static ICollection ParseIds(JToken value) - { - try - { - var result = new List(); - - if (value is JArray) - { - foreach (var id in value) - { - result.Add(Guid.Parse(id.ToString())); - } - } - - return result; - } - catch - { - return new List(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs deleted file mode 100644 index 30e8afdcf..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// SchemaGraphType.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Schemas; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public interface IGraphQLContext - { - bool CanGenerateAssetSourceUrl { get; } - - IFieldPartitioning ResolvePartition(Partitioning key); - - IGraphType GetAssetType(); - - IGraphType GetSchemaType(Guid schemaId); - - IFieldResolver ResolveAssetUrl(); - - IFieldResolver ResolveAssetSourceUrl(); - - IFieldResolver ResolveAssetThumbnailUrl(); - - IFieldResolver ResolveContentUrl(ISchemaEntity schema); - - (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLService.cs deleted file mode 100644 index 1181813c3..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLService.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// IGraphQLService.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Security.Claims; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public interface IGraphQLService - { - Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs deleted file mode 100644 index 849004c0f..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// IGraphQLUrlGenerator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Schemas; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL -{ - public interface IGraphQLUrlGenerator - { - bool CanGenerateAssetSourceUrl { get; } - - string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); - - string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); - - string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); - - string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs deleted file mode 100644 index 4d2b04e26..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs +++ /dev/null @@ -1,170 +0,0 @@ -// ========================================================================== -// AssetGraphType.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types -{ - public sealed class AssetGraphType : ObjectGraphType - { - public AssetGraphType(IGraphQLContext context) - { - Name = "AssetDto"; - - AddField(new FieldType - { - Name = "id", - Resolver = Resolver(x => x.Id.ToString()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The id of the asset." - }); - - AddField(new FieldType - { - Name = "version", - Resolver = Resolver(x => x.Version), - ResolvedType = new NonNullGraphType(new IntGraphType()), - Description = "The version of the asset." - }); - - AddField(new FieldType - { - Name = "created", - Resolver = Resolver(x => x.Created.ToDateTimeUtc()), - ResolvedType = new NonNullGraphType(new DateGraphType()), - Description = "The date and time when the asset has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - Resolver = Resolver(x => x.CreatedBy.ToString()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The user that has created the asset." - }); - - AddField(new FieldType - { - Name = "lastModified", - Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), - ResolvedType = new NonNullGraphType(new DateGraphType()), - Description = "The date and time when the asset has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - Resolver = Resolver(x => x.LastModifiedBy.ToString()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The user that has updated the asset last." - }); - - AddField(new FieldType - { - Name = "mimeType", - Resolver = Resolver(x => x.MimeType), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The mime type." - }); - - AddField(new FieldType - { - Name = "url", - Resolver = context.ResolveAssetUrl(), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The url to the asset." - }); - - AddField(new FieldType - { - Name = "thumbnailUrl", - Resolver = context.ResolveAssetThumbnailUrl(), - ResolvedType = new StringGraphType(), - Description = "The thumbnail url to the asset." - }); - - AddField(new FieldType - { - Name = "fileName", - Resolver = Resolver(x => x.FileName), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The file name." - }); - - AddField(new FieldType - { - Name = "fileType", - Resolver = Resolver(x => x.FileName.FileType()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = "The file type." - }); - - AddField(new FieldType - { - Name = "fileSize", - Resolver = Resolver(x => x.FileSize), - ResolvedType = new NonNullGraphType(new IntGraphType()), - Description = "The size of the file in bytes." - }); - - AddField(new FieldType - { - Name = "fileVersion", - Resolver = Resolver(x => x.FileVersion), - ResolvedType = new NonNullGraphType(new IntGraphType()), - Description = "The version of the file." - }); - - AddField(new FieldType - { - Name = "isImage", - Resolver = Resolver(x => x.IsImage), - ResolvedType = new NonNullGraphType(new BooleanGraphType()), - Description = "Determines of the created file is an image." - }); - - AddField(new FieldType - { - Name = "pixelWidth", - Resolver = Resolver(x => x.PixelWidth), - ResolvedType = new IntGraphType(), - Description = "The width of the image in pixels if the asset is an image." - }); - - AddField(new FieldType - { - Name = "pixelHeight", - Resolver = Resolver(x => x.PixelHeight), - ResolvedType = new IntGraphType(), - Description = "The height of the image in pixels if the asset is an image." - }); - - if (context.CanGenerateAssetSourceUrl) - { - AddField(new FieldType - { - Name = "sourceUrl", - Resolver = context.ResolveAssetSourceUrl(), - ResolvedType = new StringGraphType(), - Description = "The source url of the asset." - }); - } - - Description = "An asset"; - } - - private static IFieldResolver Resolver(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs deleted file mode 100644 index b342efa6b..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ========================================================================== -// ContentDataGraphType.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; -using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types -{ - public sealed class ContentDataGraphType : ObjectGraphType - { - public ContentDataGraphType(Schema schema, IGraphQLContext context) - { - var schemaName = schema.Properties.Label.WithFallback(schema.Name); - - Name = $"{schema.Name.ToPascalCase()}DataDto"; - - foreach (var field in schema.Fields.Where(x => !x.IsHidden)) - { - var fieldInfo = context.GetGraphType(field); - - if (fieldInfo.ResolveType != null) - { - var fieldName = field.RawProperties.Label.WithFallback(field.Name); - - var fieldGraphType = new ObjectGraphType - { - Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" - }; - - var partition = context.ResolvePartition(field.Partitioning); - - foreach (var partitionItem in partition) - { - fieldGraphType.AddField(new FieldType - { - Name = partitionItem.Key, - Resolver = fieldInfo.Resolver, - ResolvedType = fieldInfo.ResolveType, - Description = field.RawProperties.Hints - }); - } - - fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; - - var fieldResolver = new FuncFieldResolver(c => c.Source.GetOrDefault(field.Name)); - - AddField(new FieldType - { - Name = field.Name.ToCamelCase(), - Resolver = fieldResolver, - ResolvedType = fieldGraphType - }); - } - } - - Description = $"The structure of a {schemaName} content type."; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs deleted file mode 100644 index 93019e19d..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// SchemaGraphType.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types -{ - public sealed class ContentGraphType : ObjectGraphType - { - private readonly ISchemaEntity schema; - private readonly IGraphQLContext context; - - public ContentGraphType(ISchemaEntity schema, IGraphQLContext context) - { - this.context = context; - this.schema = schema; - - Name = $"{schema.Name.ToPascalCase()}Dto"; - } - - public void Initialize() - { - var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.Name); - - AddField(new FieldType - { - Name = "id", - Resolver = Resolver(x => x.Id.ToString()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = $"The id of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "version", - Resolver = Resolver(x => x.Version), - ResolvedType = new NonNullGraphType(new IntGraphType()), - Description = $"The version of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "created", - Resolver = Resolver(x => x.Created.ToDateTimeUtc()), - ResolvedType = new NonNullGraphType(new DateGraphType()), - Description = $"The date and time when the {schemaName} content has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - Resolver = Resolver(x => x.CreatedBy.ToString()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = $"The user that has created the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "lastModified", - Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), - ResolvedType = new NonNullGraphType(new DateGraphType()), - Description = $"The date and time when the {schemaName} content has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - Resolver = Resolver(x => x.LastModifiedBy.ToString()), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = $"The user that has updated the {schemaName} content last." - }); - - AddField(new FieldType - { - Name = "url", - Resolver = context.ResolveContentUrl(schema), - ResolvedType = new NonNullGraphType(new StringGraphType()), - Description = $"The url to the the {schemaName} content." - }); - - var dataType = new ContentDataGraphType(schema.SchemaDef, context); - - if (dataType.Fields.Any()) - { - AddField(new FieldType - { - Name = "data", - Resolver = Resolver(x => x.Data), - ResolvedType = new NonNullGraphType(dataType), - Description = $"The data of the {schemaName} content." - }); - } - - Description = $"The structure of a {schemaName} content type."; - } - - private static IFieldResolver Resolver(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs deleted file mode 100644 index b0e3fb5ba..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs +++ /dev/null @@ -1,192 +0,0 @@ -// ========================================================================== -// GraphModelType.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types -{ - public sealed class ContentQueryGraphType : ObjectGraphType - { - public ContentQueryGraphType(IGraphQLContext graphQLContext, IEnumerable schemas) - { - AddAssetFind(graphQLContext); - AddAssetsQuery(graphQLContext); - - foreach (var schema in schemas) - { - var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); - var schemaType = graphQLContext.GetSchemaType(schema.Id); - - AddContentFind(schema, schemaType, schemaName); - AddContentQuery(schema, schemaType, schemaName); - } - - Description = "The app queries."; - } - - private void AddAssetFind(IGraphQLContext graphQLContext) - { - AddField(new FieldType - { - Name = "findAsset", - Arguments = new QueryArguments - { - new QueryArgument(typeof(StringGraphType)) - { - Name = "id", - Description = "The id of the asset.", - DefaultValue = string.Empty - } - }, - ResolvedType = graphQLContext.GetAssetType(), - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); - - return context.FindAssetAsync(contentId); - }), - Description = "Find an asset by id." - }); - } - - private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) - { - AddField(new FieldType - { - Name = $"find{schema.Name.ToPascalCase()}Content", - Arguments = new QueryArguments - { - new QueryArgument(typeof(StringGraphType)) - { - Name = "id", - Description = $"The id of the {schemaName} content.", - DefaultValue = string.Empty - } - }, - ResolvedType = schemaType, - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); - - return context.FindContentAsync(schema.Id, contentId); - }), - Description = $"Find an {schemaName} content by id." - }); - } - - private void AddAssetsQuery(IGraphQLContext graphQLContext) - { - AddField(new FieldType - { - Name = "queryAssets", - Arguments = new QueryArguments - { - new QueryArgument(typeof(IntGraphType)) - { - Name = "top", - Description = "Optional number of assets to take.", - DefaultValue = 20 - }, - new QueryArgument(typeof(IntGraphType)) - { - Name = "skip", - Description = "Optional number of assets to skip.", - DefaultValue = 0 - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "search", - Description = "Optional query.", - DefaultValue = string.Empty - } - }, - ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - - var argTop = c.GetArgument("top", 20); - var argSkip = c.GetArgument("skip", 0); - var argQuery = c.GetArgument("search", string.Empty); - - return context.QueryAssetsAsync(argQuery, argSkip, argTop); - }), - Description = "Query assets items." - }); - } - - private void AddContentQuery(ISchemaEntity schema, IGraphType schemaType, string schemaName) - { - AddField(new FieldType - { - Name = $"query{schema.Name.ToPascalCase()}Contents", - Arguments = new QueryArguments - { - new QueryArgument(typeof(IntGraphType)) - { - Name = "top", - Description = "Optional number of contents to take.", - DefaultValue = 20 - }, - new QueryArgument(typeof(IntGraphType)) - { - Name = "skip", - Description = "Optional number of contents to skip.", - DefaultValue = 0 - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "filter", - Description = "Optional OData filter.", - DefaultValue = string.Empty - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "search", - Description = "Optional OData full text search.", - DefaultValue = string.Empty - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "orderby", - Description = "Optional OData order definition.", - DefaultValue = string.Empty - } - }, - ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentQuery = BuildODataQuery(c); - - return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); - }), - Description = $"Query {schemaName} content items." - }); - } - - private static string BuildODataQuery(ResolveFieldContext c) - { - var odataQuery = "?" + - string.Join("&", - c.Arguments - .Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => $"${x.Key}={x.Value}")); - - return odataQuery; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs deleted file mode 100644 index 36218f6eb..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// NoopGraphType.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using GraphQL.Language.AST; -using GraphQL.Types; - -namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types -{ - public sealed class NoopGraphType : ScalarGraphType - { - public NoopGraphType(string name) - { - Name = name; - } - - public override object Serialize(object value) - { - return value; - } - - public override object ParseValue(object value) - { - return value; - } - - public override object ParseLiteral(IValue value) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs deleted file mode 100644 index 380335382..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// IContentEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public interface IContentEntity : - IEntity, - IEntityWithAppRef, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - Status Status { get; } - - NamedContentData Data { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs deleted file mode 100644 index 0af89adcf..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentQueryService.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// IContentQueryService.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Schemas; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public interface IContentQueryService - { - Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet ids); - - Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); - - Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); - - Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/QueryContext.cs b/src/Squidex.Domain.Apps.Read/Contents/QueryContext.cs deleted file mode 100644 index 195808e11..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/QueryContext.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ========================================================================== -// QueryContext.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.Contents -{ - public class QueryContext - { - private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); - private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); - private readonly IContentQueryService contentQuery; - private readonly IAssetRepository assetRepository; - private readonly IAppEntity app; - private readonly ClaimsPrincipal user; - - public QueryContext( - IAppEntity app, - IAssetRepository assetRepository, - IContentQueryService contentQuery, - ClaimsPrincipal user) - { - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(app, nameof(app)); - Guard.NotNull(user, nameof(user)); - - this.assetRepository = assetRepository; - this.contentQuery = contentQuery; - - this.user = user; - - this.app = app; - } - - public async Task FindAssetAsync(Guid id) - { - var asset = cachedAssets.GetOrDefault(id); - - if (asset == null) - { - asset = await assetRepository.FindAssetAsync(id); - - if (asset != null) - { - cachedAssets[asset.Id] = asset; - } - } - - return asset; - } - - public async Task FindContentAsync(Guid schemaId, Guid id) - { - var content = cachedContents.GetOrDefault(id); - - if (content == null) - { - content = (await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id)).Content; - - if (content != null) - { - cachedContents[content.Id] = content; - } - } - - return content; - } - - public async Task> QueryAssetsAsync(string query, int skip = 0, int take = 10) - { - var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip); - - foreach (var asset in assets) - { - cachedAssets[asset.Id] = asset; - } - - return assets; - } - - public async Task> QueryContentsAsync(string schemaIdOrName, string query) - { - var contents = await contentQuery.QueryWithCountAsync(app, schemaIdOrName, user, false, query); - - foreach (var content in contents.Items) - { - cachedContents[content.Id] = content; - } - - return contents.Items; - } - - public async Task> GetReferencedAssetsAsync(ICollection ids) - { - Guard.NotNull(ids, nameof(ids)); - - var notLoadedAssets = new HashSet(ids.Where(id => !cachedAssets.ContainsKey(id))); - - if (notLoadedAssets.Count > 0) - { - var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue); - - foreach (var asset in assets) - { - cachedAssets[asset.Id] = asset; - } - } - - return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); - } - - public async Task> GetReferencedContentsAsync(Guid schemaId, ICollection ids) - { - Guard.NotNull(ids, nameof(ids)); - - var notLoadedContents = new HashSet(ids.Where(id => !cachedContents.ContainsKey(id))); - - if (notLoadedContents.Count > 0) - { - var contents = await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, notLoadedContents); - - foreach (var content in contents.Items) - { - cachedContents[content.Id] = content; - } - } - - return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs deleted file mode 100644 index 85887ce9a..000000000 --- a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// IContentRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.OData.UriParser; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Schemas; - -namespace Squidex.Domain.Apps.Read.Contents.Repositories -{ - public interface IContentRepository - { - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); - - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); - - Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds); - - Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids); - - Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); - - Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Read/EntityMapper.cs b/src/Squidex.Domain.Apps.Read/EntityMapper.cs deleted file mode 100644 index 9dfe40a6d..000000000 --- a/src/Squidex.Domain.Apps.Read/EntityMapper.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ========================================================================== -// EntityMapper.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read -{ - public static class EntityMapper - { - public static T Create(SquidexEvent @event, EnvelopeHeaders headers, Action updater = null) where T : IEntity, new() - { - var entity = new T(); - - SetId(headers, entity); - - SetVersion(headers, entity); - SetCreated(headers, entity); - SetCreatedBy(@event, entity); - - SetAppId(@event, entity); - - return entity.Update(@event, headers, updater); - } - - public static T Update(this T entity, SquidexEvent @event, EnvelopeHeaders headers, Action updater = null) where T : IEntity, new() - { - SetVersion(headers, entity); - SetLastModified(headers, entity); - SetLastModifiedBy(@event, entity); - - updater?.Invoke(entity); - - return entity; - } - - private static void SetId(EnvelopeHeaders headers, IEntity entity) - { - entity.Id = headers.AggregateId(); - } - - private static void SetCreated(EnvelopeHeaders headers, IEntity entity) - { - entity.Created = headers.Timestamp(); - } - - private static void SetLastModified(EnvelopeHeaders headers, IEntity entity) - { - entity.LastModified = headers.Timestamp(); - } - - private static void SetVersion(EnvelopeHeaders headers, IEntity entity) - { - if (entity is IUpdateableEntityWithVersion withVersion) - { - withVersion.Version = headers.EventStreamNumber(); - } - } - - private static void SetCreatedBy(SquidexEvent @event, IEntity entity) - { - if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy) - { - withCreatedBy.CreatedBy = @event.Actor; - } - } - - private static void SetLastModifiedBy(SquidexEvent @event, IEntity entity) - { - if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) - { - withModifiedBy.LastModifiedBy = @event.Actor; - } - } - - private static void SetAppId(SquidexEvent @event, IEntity entity) - { - if (entity is IUpdateableEntityWithAppRef app && @event is AppEvent appEvent) - { - app.AppId = appEvent.AppId.Id; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/History/HistoryEventToStore.cs b/src/Squidex.Domain.Apps.Read/History/HistoryEventToStore.cs deleted file mode 100644 index 1ca32b31a..000000000 --- a/src/Squidex.Domain.Apps.Read/History/HistoryEventToStore.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// HistoryEventToStore.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.History -{ - public sealed class HistoryEventToStore - { - private readonly Dictionary parameters = new Dictionary(); - - public string Channel { get; } - - public string Message { get; } - - public IReadOnlyDictionary Parameters - { - get { return parameters; } - } - - public HistoryEventToStore(string channel, string message) - { - Guard.NotNullOrEmpty(channel, nameof(channel)); - Guard.NotNullOrEmpty(message, nameof(message)); - - Channel = channel; - - Message = message; - } - - public HistoryEventToStore AddParameter(string key, object value) - { - parameters[key] = value.ToString(); - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/History/HistoryEventsCreatorBase.cs b/src/Squidex.Domain.Apps.Read/History/HistoryEventsCreatorBase.cs deleted file mode 100644 index a267d1572..000000000 --- a/src/Squidex.Domain.Apps.Read/History/HistoryEventsCreatorBase.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// HistoryEventsCreatorBase.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.History -{ - public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator - { - private readonly Dictionary texts = new Dictionary(); - private readonly TypeNameRegistry typeNameRegistry; - - public IReadOnlyDictionary Texts - { - get { return texts; } - } - - protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.typeNameRegistry = typeNameRegistry; - } - - protected void AddEventMessage(string message) where TEvent : IEvent - { - Guard.NotNullOrEmpty(message, nameof(message)); - - texts[typeNameRegistry.GetName()] = message; - } - - protected bool HasEventText(IEvent @event) - { - var message = typeNameRegistry.GetName(@event.GetType()); - - return texts.ContainsKey(message); - } - - protected HistoryEventToStore ForEvent(IEvent @event, string channel) - { - var message = typeNameRegistry.GetName(@event.GetType()); - - return new HistoryEventToStore(channel, message); - } - - public Task CreateEventAsync(Envelope @event) - { - if (HasEventText(@event.Payload)) - { - return CreateEventCoreAsync(@event); - } - - return Task.FromResult(null); - } - - protected abstract Task CreateEventCoreAsync(Envelope @event); - } -} diff --git a/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs deleted file mode 100644 index a9b213b5c..000000000 --- a/src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// IHistoryEventEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.History -{ - public interface IHistoryEventEntity : IEntity - { - Guid EventId { get; } - - RefToken Actor { get; } - - string Message { get; } - - long Version { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/History/IHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Read/History/IHistoryEventsCreator.cs deleted file mode 100644 index 91d37234e..000000000 --- a/src/Squidex.Domain.Apps.Read/History/IHistoryEventsCreator.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// IHistoryEventsCreator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.History -{ - public interface IHistoryEventsCreator - { - IReadOnlyDictionary Texts { get; } - - Task CreateEventAsync(Envelope @event); - } -} diff --git a/src/Squidex.Domain.Apps.Read/History/Repositories/IHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Read/History/Repositories/IHistoryEventRepository.cs deleted file mode 100644 index b0875f5d2..000000000 --- a/src/Squidex.Domain.Apps.Read/History/Repositories/IHistoryEventRepository.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// IHistoryEventRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.History.Repositories -{ - public interface IHistoryEventRepository - { - Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count); - } -} diff --git a/src/Squidex.Domain.Apps.Read/IAppProvider.cs b/src/Squidex.Domain.Apps.Read/IAppProvider.cs deleted file mode 100644 index 296f8ff2d..000000000 --- a/src/Squidex.Domain.Apps.Read/IAppProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// IApps.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Schemas; - -namespace Squidex.Domain.Apps.Read -{ - public interface IAppProvider - { - Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id); - - Task GetAppAsync(string appName); - - Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false); - - Task GetSchemaAsync(string appName, string name, bool provideDeleted = false); - - Task> GetSchemasAsync(string appName); - - Task> GetRulesAsync(string appName); - - Task> GetUserApps(string userId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/IEntity.cs b/src/Squidex.Domain.Apps.Read/IEntity.cs deleted file mode 100644 index 89aea8bed..000000000 --- a/src/Squidex.Domain.Apps.Read/IEntity.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// IEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; - -namespace Squidex.Domain.Apps.Read -{ - public interface IEntity - { - Guid Id { get; set; } - - Instant Created { get; set; } - - Instant LastModified { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs deleted file mode 100644 index fdd81b0e1..000000000 --- a/src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// IEntityWithAppRef.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Read -{ - public interface IEntityWithAppRef - { - Guid AppId { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs b/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs deleted file mode 100644 index 4661a15ee..000000000 --- a/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// IEntityWithCreatedBy.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read -{ - public interface IEntityWithCreatedBy - { - RefToken CreatedBy { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithLastModifiedBy.cs b/src/Squidex.Domain.Apps.Read/IEntityWithLastModifiedBy.cs deleted file mode 100644 index eb42b4e93..000000000 --- a/src/Squidex.Domain.Apps.Read/IEntityWithLastModifiedBy.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// IEntityWithLastModifiedBy.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read -{ - public interface IEntityWithLastModifiedBy - { - RefToken LastModifiedBy { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs b/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs deleted file mode 100644 index 860918deb..000000000 --- a/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// IEntityWithVersion.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read -{ - public interface IEntityWithVersion - { - long Version { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs deleted file mode 100644 index 316962a96..000000000 --- a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// IUpdateableEntityWithAppRef.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Read -{ - public interface IUpdateableEntityWithAppRef - { - Guid AppId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs deleted file mode 100644 index 3675937c4..000000000 --- a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// IUpdateableEntityWithCreatedBy.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read -{ - public interface IUpdateableEntityWithCreatedBy - { - RefToken CreatedBy { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs deleted file mode 100644 index d1aedc9f4..000000000 --- a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// IUpdateableEntityWithLastModifiedBy.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read -{ - public interface IUpdateableEntityWithLastModifiedBy - { - RefToken LastModifiedBy { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs deleted file mode 100644 index 8d08b4c6a..000000000 --- a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// IUpdateableEntityWithVersion.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read -{ - public interface IUpdateableEntityWithVersion - { - long Version { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs deleted file mode 100644 index b9f1aeaa2..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// IRuleEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Read.Rules -{ - public interface IRuleEntity : - IEntity, - IEntityWithAppRef, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - Rule RuleDef { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs deleted file mode 100644 index 631ad2322..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// IRuleEventEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Read.Rules -{ - public interface IRuleEventEntity : IEntity - { - RuleJob Job { get; } - - Instant? NextAttempt { get; } - - RuleJobResult JobResult { get; } - - RuleResult Result { get; } - - int NumCalls { get; } - - string LastDump { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs deleted file mode 100644 index 2b05e6741..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// IRuleEventRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Read.Rules.Repositories -{ - public interface IRuleEventRepository - { - Task EnqueueAsync(RuleJob job, Instant nextAttempt); - - Task EnqueueAsync(Guid id, Instant nextAttempt); - - Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); - - Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)); - - Task CountByAppAsync(Guid appId); - - Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); - - Task FindAsync(Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs deleted file mode 100644 index b738dfeef..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs +++ /dev/null @@ -1,157 +0,0 @@ -// ========================================================================== -// RuleDequeuer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using NodaTime; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Timers; - -namespace Squidex.Domain.Apps.Read.Rules -{ - public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem - { - private readonly ActionBlock requestBlock; - private readonly IRuleEventRepository ruleEventRepository; - private readonly RuleService ruleService; - private readonly CompletionTimer timer; - private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); - private readonly IClock clock; - private readonly ISemanticLog log; - - public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) - { - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleService, nameof(ruleService)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - - this.clock = clock; - - this.log = log; - - requestBlock = - new ActionBlock(HandleAsync, - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); - - timer = new CompletionTimer(5000, QueryAsync); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - timer.StopAsync().Wait(); - - requestBlock.Complete(); - requestBlock.Completion.Wait(); - } - } - - public void Connect() - { - } - - public void Next() - { - timer.SkipCurrentDelay(); - } - - private async Task QueryAsync(CancellationToken cancellationToken) - { - try - { - var now = clock.GetCurrentInstant(); - - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, cancellationToken); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "QueueWebhookEvents") - .WriteProperty("status", "Failed")); - } - } - - public async Task HandleAsync(IRuleEventEntity @event) - { - if (!executing.TryAdd(@event.Id, false)) - { - return; - } - - try - { - var job = @event.Job; - - var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData); - - var jobInvoke = ComputeJobInvoke(response.Result, @event, job); - var jobResult = ComputeJobResult(response.Result, jobInvoke); - - await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, jobInvoke); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "SendWebhookEvent") - .WriteProperty("status", "Failed")); - } - finally - { - executing.TryRemove(@event.Id, out var value); - } - } - - private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) - { - if (result != RuleResult.Success && !nextCall.HasValue) - { - return RuleJobResult.Failed; - } - else if (result != RuleResult.Success && nextCall.HasValue) - { - return RuleJobResult.Retry; - } - else - { - return RuleJobResult.Success; - } - } - - private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) - { - if (result != RuleResult.Success) - { - switch (@event.NumCalls) - { - case 0: - return job.Created.Plus(Duration.FromMinutes(5)); - case 1: - return job.Created.Plus(Duration.FromHours(1)); - case 2: - return job.Created.Plus(Duration.FromHours(6)); - case 3: - return job.Created.Plus(Duration.FromHours(12)); - } - } - - return null; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs deleted file mode 100644 index 867b13937..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// RuleEnqueuer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.Rules -{ - public sealed class RuleEnqueuer : IEventConsumer - { - private readonly IRuleEventRepository ruleEventRepository; - private readonly IAppProvider appProvider; - private readonly RuleService ruleService; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return ".*"; } - } - - public RuleEnqueuer( - IRuleEventRepository ruleEventRepository, IAppProvider appProvider, - RuleService ruleService) - { - Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleService, nameof(ruleService)); - - Guard.NotNull(appProvider, nameof(appProvider)); - - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - - this.appProvider = appProvider; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (@event.Payload is AppEvent appEvent) - { - var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name); - - foreach (var ruleEntity in rules) - { - var job = ruleService.CreateJob(ruleEntity.RuleDef, @event); - - if (job != null) - { - await ruleEventRepository.EnqueueAsync(job, job.Created); - } - } - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs deleted file mode 100644 index d8fb997e7..000000000 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// RuleJobResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Read.Rules -{ - public enum RuleJobResult - { - Pending, - Success, - Retry, - Failed - } -} diff --git a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs deleted file mode 100644 index 3cb98ef78..000000000 --- a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// ISchemaEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Read.Schemas -{ - public interface ISchemaEntity : - IEntity, - IEntityWithAppRef, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - string Name { get; } - - bool IsPublished { get; } - - bool IsDeleted { get; } - - string ScriptQuery { get; } - - string ScriptCreate { get; } - - string ScriptUpdate { get; } - - string ScriptDelete { get; } - - string ScriptChange { get; } - - Schema SchemaDef { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Read/Schemas/SchemaHistoryEventsCreator.cs deleted file mode 100644 index 426d97473..000000000 --- a/src/Squidex.Domain.Apps.Read/Schemas/SchemaHistoryEventsCreator.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// SchemaHistoryEventsCreator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Read.History; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.Schemas -{ - public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase - { - public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "created schema {[Name]}"); - - AddEventMessage( - "updated schema {[Name]}"); - - AddEventMessage( - "deleted schema {[Name]}"); - - AddEventMessage( - "published schema {[Name]}"); - - AddEventMessage( - "unpublished schema {[Name]}"); - - AddEventMessage( - "reordered fields of schema {[Name]}"); - - AddEventMessage( - "added field {[Field]} to schema {[Name]}"); - - AddEventMessage( - "deleted field {[Field]} from schema {[Name]}"); - - AddEventMessage( - "has locked field {[Field]} of schema {[Name]}"); - - AddEventMessage( - "has hidden field {[Field]} of schema {[Name]}"); - - AddEventMessage( - "has shown field {[Field]} of schema {[Name]}"); - - AddEventMessage( - "disabled field {[Field]} of schema {[Name]}"); - - AddEventMessage( - "disabled field {[Field]} of schema {[Name]}"); - - AddEventMessage( - "has updated field {[Field]} of schema {[Name]}"); - - AddEventMessage( - "deleted field {[Field]} of schema {[Name]}"); - } - - protected override Task CreateEventCoreAsync(Envelope @event) - { - if (@event.Payload is SchemaEvent schemaEvent) - { - var channel = $"schemas.{schemaEvent.SchemaId.Name}"; - - var result = ForEvent(@event.Payload, channel).AddParameter("Name", schemaEvent.SchemaId.Name); - - if (schemaEvent is FieldEvent fieldEvent) - { - result.AddParameter("Field", fieldEvent.FieldId.Name); - } - - return Task.FromResult(result); - } - - return Task.FromResult(null); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj b/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj deleted file mode 100644 index 58922eba3..000000000 --- a/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - netstandard2.0 - - - full - True - - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - diff --git a/src/Squidex.Domain.Apps.Read/State/AppProvider.cs b/src/Squidex.Domain.Apps.Read/State/AppProvider.cs deleted file mode 100644 index 9a4a155d1..000000000 --- a/src/Squidex.Domain.Apps.Read/State/AppProvider.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// AppProvider.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.State.Grains; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Read.State -{ - public sealed class AppProvider : IAppProvider - { - private readonly IStateFactory factory; - - public AppProvider(IStateFactory factory) - { - Guard.NotNull(factory, nameof(factory)); - - this.factory = factory; - } - - public async Task GetAppAsync(string appName) - { - var app = await factory.GetSynchronizedAsync(appName); - - return await app.GetAppAsync(); - } - - public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) - { - var app = await factory.GetSynchronizedAsync(appName); - - return await app.GetAppWithSchemaAsync(id); - } - - public async Task> GetRulesAsync(string appName) - { - var app = await factory.GetSynchronizedAsync(appName); - - return await app.GetRulesAsync(); - } - - public async Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) - { - var app = await factory.GetSynchronizedAsync(appName); - - return await app.GetSchemaAsync(id, provideDeleted); - } - - public async Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) - { - var app = await factory.GetSynchronizedAsync(appName); - - return await app.GetSchemaAsync(name, provideDeleted); - } - - public async Task> GetSchemasAsync(string appName) - { - var app = await factory.GetSynchronizedAsync(appName); - - return await app.GetSchemasAsync(); - } - - public async Task> GetUserApps(string userId) - { - var appUser = await factory.GetSynchronizedAsync(userId); - var appNames = await appUser.GetAppNamesAsync(); - - var tasks = appNames.Select(x => GetAppAsync(x)); - - var apps = await Task.WhenAll(tasks); - - return apps.Where(a => a != null && a.Contributors.ContainsKey(userId)).ToList(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs b/src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs deleted file mode 100644 index fd142607b..000000000 --- a/src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// AppStateEventConsumer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Read.State.Grains; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.State -{ - public sealed class AppStateEventConsumer : IEventConsumer - { - private readonly IStateFactory factory; - - public string Name - { - get { return typeof(AppStateEventConsumer).Name; } - } - - public string EventsFilter - { - get { return @"(^app-)|(^schema-)|(^rule\-)"; } - } - - public AppStateEventConsumer(IStateFactory factory) - { - Guard.NotNull(factory, nameof(factory)); - - this.factory = factory; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (@event.Payload is AppEvent appEvent) - { - var appGrain = await factory.GetSynchronizedAsync(appEvent.AppId.Name); - - await appGrain.HandleAsync(@event); - } - - if (@event.Payload is AppContributorAssigned contributorAssigned) - { - var userGrain = await factory.GetSynchronizedAsync(contributorAssigned.ContributorId); - - await userGrain.AddAppAsync(contributorAssigned.AppId.Name); - } - - if (@event.Payload is AppContributorRemoved contributorRemoved) - { - var userGrain = await factory.GetSynchronizedAsync(contributorRemoved.ContributorId); - - await userGrain.RemoveAppAsync(contributorRemoved.AppId.Name); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs deleted file mode 100644 index bca5cd380..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ========================================================================== -// AppStateGrain.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public class AppStateGrain : IStatefulObject - { - private readonly FieldRegistry fieldRegistry; - private IPersistence persistence; - private Exception exception; - private AppStateGrainState state; - - public AppStateGrain(FieldRegistry fieldRegistry) - { - Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); - - this.fieldRegistry = fieldRegistry; - } - - public async Task ActivateAsync(string key, IStore store) - { - persistence = store.WithSnapshots(key, s => state = s); - - try - { - await persistence.ReadAsync(); - } - catch (Exception ex) - { - exception = ex; - } - - if (state == null) - { - state = new AppStateGrainState(); - } - - state.SetRegistry(fieldRegistry); - } - - public virtual Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid id) - { - var schema = state.FindSchema(x => x.Id == id && !x.IsDeleted); - - return Task.FromResult((state.GetApp(), schema)); - } - - public virtual Task GetAppAsync() - { - var result = state.GetApp(); - - return Task.FromResult(result); - } - - public virtual Task> GetRulesAsync() - { - var result = state.FindRules(); - - return Task.FromResult(result); - } - - public virtual Task> GetSchemasAsync() - { - var result = state.FindSchemas(x => !x.IsDeleted); - - return Task.FromResult(result); - } - - public virtual Task GetSchemaAsync(Guid id, bool provideDeleted = false) - { - var result = state.FindSchema(x => x.Id == id && (!x.IsDeleted || provideDeleted)); - - return Task.FromResult(result); - } - - public virtual Task GetSchemaAsync(string name, bool provideDeleted = false) - { - var result = state.FindSchema(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && (!x.IsDeleted || provideDeleted)); - - return Task.FromResult(result); - } - - public async virtual Task HandleAsync(Envelope message) - { - if (exception != null) - { - if (message.Payload is AppCreated) - { - exception = null; - } - else - { - throw exception; - } - } - - if (message.Payload is AppEvent appEvent && (state.App == null || state.App.Id == appEvent.AppId.Id)) - { - try - { - state = state.Apply(message); - - await persistence.WriteSnapshotAsync(state); - } - catch (InconsistentStateException) - { - await persistence.ReadAsync(); - - state = state.Apply(message); - - await persistence.WriteSnapshotAsync(state); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs deleted file mode 100644 index 70803d76a..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// AppStateGrainState.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed partial class AppStateGrainState : Cloneable - { - private FieldRegistry registry; - - [JsonProperty] - public JsonAppEntity App { get; set; } - - [JsonProperty] - public ImmutableDictionary Rules { get; set; } = ImmutableDictionary.Empty; - - [JsonProperty] - public ImmutableDictionary Schemas { get; set; } = ImmutableDictionary.Empty; - - public void SetRegistry(FieldRegistry registry) - { - this.registry = registry; - } - - public IAppEntity GetApp() - { - return App; - } - - public ISchemaEntity FindSchema(Func filter) - { - return Schemas?.Values.FirstOrDefault(filter); - } - - public List FindSchemas(Func filter) - { - return Schemas?.Values.Where(filter).OfType().ToList() ?? new List(); - } - - public List FindRules() - { - return Rules?.Values.OfType().ToList() ?? new List(); - } - - public AppStateGrainState Apply(Envelope envelope) - { - return Clone(c => - { - c.DispatchAction(envelope.Payload, envelope.Headers); - - if (c.App != null) - { - c.App.Etag = Guid.NewGuid().ToString(); - } - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs deleted file mode 100644 index 58789f36c..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// AppStateGrainState_Apps.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Apps.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed partial class AppStateGrainState - { - public void On(AppCreated @event, EnvelopeHeaders headers) - { - App = EntityMapper.Create(@event, headers, a => - { - SimpleMapper.Map(@event, a); - - a.LanguagesConfig = LanguagesConfig.Build(Language.EN); - }); - } - - public void On(AppLanguageAdded @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.LanguagesConfig = a.LanguagesConfig.Apply(@event); - }); - } - - public void On(AppLanguageRemoved @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.LanguagesConfig = a.LanguagesConfig.Apply(@event); - }); - } - - public void On(AppLanguageUpdated @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.LanguagesConfig = a.LanguagesConfig.Apply(@event); - }); - } - - public void On(AppContributorAssigned @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.Contributors = a.Contributors.Apply(@event); - }); - } - - public void On(AppContributorRemoved @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.Contributors = a.Contributors.Apply(@event); - }); - } - - public void On(AppClientAttached @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.Clients = a.Clients.Apply(@event); - }); - } - - public void On(AppClientUpdated @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.Clients = a.Clients.Apply(@event); - }); - } - - public void On(AppClientRenamed @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.Clients = a.Clients.Apply(@event); - }); - } - - public void On(AppClientRevoked @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - a.Clients = a.Clients.Apply(@event); - }); - } - - public void On(AppPlanChanged @event, EnvelopeHeaders headers) - { - UpdateApp(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - private void UpdateApp(AppEvent @event, EnvelopeHeaders headers, Action updater = null) - { - App = App.Clone().Update(@event, headers, updater); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs deleted file mode 100644 index 2e3626843..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// AppStateGrainState_Rules.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Domain.Apps.Events.Rules.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed partial class AppStateGrainState - { - public void On(RuleCreated @event, EnvelopeHeaders headers) - { - var id = @event.RuleId; - - Rules = Rules.SetItem(id, EntityMapper.Create(@event, headers, r => - { - r.RuleDef = RuleEventDispatcher.Create(@event); - })); - } - - public void On(RuleUpdated @event, EnvelopeHeaders headers) - { - UpdateRule(@event, headers, r => - { - r.RuleDef = r.RuleDef.Apply(@event); - }); - } - - public void On(RuleEnabled @event, EnvelopeHeaders headers) - { - UpdateRule(@event, headers, r => - { - r.RuleDef = r.RuleDef.Apply(@event); - }); - } - - public void On(RuleDisabled @event, EnvelopeHeaders headers) - { - UpdateRule(@event, headers, r => - { - r.RuleDef = r.RuleDef.Apply(@event); - }); - } - - public void On(RuleDeleted @event, EnvelopeHeaders headers) - { - Rules = Rules.Remove(@event.RuleId); - } - - private void UpdateRule(RuleEvent @event, EnvelopeHeaders headers, Action updater = null) - { - var id = @event.RuleId; - - Rules = Rules.SetItem(id, x => x.Clone().Update(@event, headers, updater)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs deleted file mode 100644 index 42ce9ae30..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs +++ /dev/null @@ -1,162 +0,0 @@ -// ========================================================================== -// AppStateGrainState_Schemas.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Events.Schemas.Old; -using Squidex.Domain.Apps.Events.Schemas.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed partial class AppStateGrainState - { - public void On(SchemaCreated @event, EnvelopeHeaders headers) - { - var id = @event.SchemaId.Id; - - Schemas = Schemas.SetItem(id, EntityMapper.Create(@event, headers, s => - { - s.SchemaDef = SchemaEventDispatcher.Create(@event, registry); - - SimpleMapper.Map(@event, s); - })); - } - - public void On(SchemaPublished @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(SchemaUnpublished @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(ScriptsConfigured @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - SimpleMapper.Map(s, @event); - }); - } - - public void On(SchemaUpdated @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(SchemaFieldsReordered @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldAdded @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event, registry); - }); - } - - public void On(FieldUpdated @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldLocked @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldDisabled @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldEnabled @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldHidden @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldShown @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(FieldDeleted @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers, s => - { - s.SchemaDef = s.SchemaDef.Apply(@event); - }); - } - - public void On(WebhookAdded @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers); - } - - public void On(WebhookDeleted @event, EnvelopeHeaders headers) - { - UpdateSchema(@event, headers); - } - - public void On(SchemaDeleted @event, EnvelopeHeaders headers) - { - Schemas = Schemas.Remove(@event.SchemaId.Id); - } - - private void UpdateSchema(SchemaEvent @event, EnvelopeHeaders headers, Action updater = null) - { - var id = @event.SchemaId.Id; - - Schemas = Schemas.SetItem(id, x => x.Clone().Update(@event, headers, updater)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs deleted file mode 100644 index 08be064e1..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// AppUserGrain.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed class AppUserGrain : IStatefulObject - { - private IPersistence persistence; - private AppUserGrainState state = new AppUserGrainState(); - - public Task ActivateAsync(string key, IStore store) - { - persistence = store.WithSnapshots(key, s => state = s); - - return persistence.ReadAsync(); - } - - public Task AddAppAsync(string appName) - { - state = state.AddApp(appName); - - return persistence.WriteSnapshotAsync(state); - } - - public Task RemoveAppAsync(string appName) - { - state = state.RemoveApp(appName); - - return persistence.WriteSnapshotAsync(state); - } - - public Task> GetAppNamesAsync() - { - return Task.FromResult(state.AppNames.ToList()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs deleted file mode 100644 index 10da85cb0..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// AppUserGrainState.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Immutable; -using Newtonsoft.Json; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed class AppUserGrainState : Cloneable - { - [JsonProperty] - public ImmutableHashSet AppNames { get; set; } = ImmutableHashSet.Empty; - - public AppUserGrainState AddApp(string appName) - { - return Clone(c => c.AppNames = c.AppNames.Add(appName)); - } - - public AppUserGrainState RemoveApp(string appName) - { - return Clone(c => c.AppNames = c.AppNames.Remove(appName)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs deleted file mode 100644 index fbe491584..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// JsonAppEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed class JsonAppEntity : JsonEntity, IAppEntity - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string PlanId { get; set; } - - [JsonProperty] - public string Etag { get; set; } - - [JsonProperty] - public string PlanOwner { get; set; } - - [JsonProperty] - public AppClients Clients { get; set; } = AppClients.Empty; - - [JsonProperty] - public AppContributors Contributors { get; set; } = AppContributors.Empty; - - [JsonProperty] - public LanguagesConfig LanguagesConfig { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs deleted file mode 100644 index 94d38176e..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// JsonEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public abstract class JsonEntity : Cloneable, IUpdateableEntityWithVersion where T : Cloneable - { - [JsonProperty] - public Guid Id { get; set; } - - [JsonProperty] - public Instant Created { get; set; } - - [JsonProperty] - public Instant LastModified { get; set; } - - [JsonProperty] - public long Version { get; set; } - - public T Clone() - { - return Clone(x => { }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs deleted file mode 100644 index 287d1b2ab..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// JsonRuleEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed class JsonRuleEntity : - JsonEntity, - IRuleEntity, - IUpdateableEntityWithAppRef, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy - { - [JsonProperty] - public Guid AppId { get; set; } - - [JsonProperty] - public RefToken CreatedBy { get; set; } - - [JsonProperty] - public RefToken LastModifiedBy { get; set; } - - [JsonProperty] - public Rule RuleDef { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs deleted file mode 100644 index 004467008..000000000 --- a/src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// JsonSchemaEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Read.State.Grains -{ - public sealed class JsonSchemaEntity : - JsonEntity, - ISchemaEntity, - IUpdateableEntityWithAppRef, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public Guid AppId { get; set; } - - [JsonProperty] - public RefToken CreatedBy { get; set; } - - [JsonProperty] - public RefToken LastModifiedBy { get; set; } - - [JsonProperty] - public bool IsDeleted { get; set; } - - [JsonProperty] - public string ScriptQuery { get; set; } - - [JsonProperty] - public string ScriptCreate { get; set; } - - [JsonProperty] - public string ScriptUpdate { get; set; } - - [JsonProperty] - public string ScriptDelete { get; set; } - - [JsonProperty] - public string ScriptChange { get; set; } - - [JsonProperty] - public Schema SchemaDef { get; set; } - - [JsonIgnore] - public bool IsPublished - { - get { return SchemaDef.IsPublished; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/AppAggregateCommand.cs b/src/Squidex.Domain.Apps.Write/AppAggregateCommand.cs deleted file mode 100644 index cc46b1dbf..000000000 --- a/src/Squidex.Domain.Apps.Write/AppAggregateCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// AppAggregateCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write -{ - public class AppAggregateCommand : AppCommand, IAggregateCommand - { - Guid IAggregateCommand.AggregateId - { - get { return AppId.Id; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/AppCommand.cs b/src/Squidex.Domain.Apps.Write/AppCommand.cs deleted file mode 100644 index a948421a5..000000000 --- a/src/Squidex.Domain.Apps.Write/AppCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// AppCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write -{ - public abstract class AppCommand : SquidexCommand - { - public NamedId AppId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs deleted file mode 100644 index 7c3978cc7..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs +++ /dev/null @@ -1,174 +0,0 @@ -// ========================================================================== -// AppCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Domain.Apps.Write.Apps.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAppProvider appProvider; - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; - private readonly IUserResolver userResolver; - - public AppCommandMiddleware( - IAggregateHandler handler, - IAppProvider appProvider, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager, - IUserResolver userResolver) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(userResolver, nameof(userResolver)); - Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); - Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); - - this.handler = handler; - this.userResolver = userResolver; - this.appProvider = appProvider; - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; - } - - protected async Task On(CreateApp command, CommandContext context) - { - await handler.CreateAsync(context, async a => - { - await GuardApp.CanCreate(command, appProvider); - - a.Create(command); - - context.Complete(EntityCreatedResult.Create(a.Id, a.Version)); - }); - } - - protected async Task On(AssignContributor command, CommandContext context) - { - await handler.UpdateAsync(context, async a => - { - await GuardAppContributors.CanAssign(a.Contributors, command, userResolver, appPlansProvider.GetPlan(a.Plan?.PlanId)); - - a.AssignContributor(command); - }); - } - - protected Task On(RemoveContributor command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppContributors.CanRemove(a.Contributors, command); - - a.RemoveContributor(command); - }); - } - - protected Task On(AttachClient command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppClients.CanAttach(a.Clients, command); - - a.AttachClient(command); - }); - } - - protected Task On(UpdateClient command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppClients.CanUpdate(a.Clients, command); - - a.UpdateClient(command); - }); - } - - protected Task On(RevokeClient command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppClients.CanRevoke(a.Clients, command); - - a.RevokeClient(command); - }); - } - - protected Task On(AddLanguage command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppLanguages.CanAdd(a.LanguagesConfig, command); - - a.AddLanguage(command); - }); - } - - protected Task On(RemoveLanguage command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppLanguages.CanRemove(a.LanguagesConfig, command); - - a.RemoveLanguage(command); - }); - } - - protected Task On(UpdateLanguage command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAppLanguages.CanUpdate(a.LanguagesConfig, command); - - a.UpdateLanguage(command); - }); - } - - protected Task On(ChangePlan command, CommandContext context) - { - return handler.UpdateAsync(context, async a => - { - GuardApp.CanChangePlan(command, a.Plan, appPlansProvider); - - if (command.FromCallback) - { - a.ChangePlan(command); - } - else - { - var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId); - - if (result is PlanChangedResult) - { - a.ChangePlan(command); - } - - context.Complete(result); - } - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs deleted file mode 100644 index 6c2a7b7bb..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs +++ /dev/null @@ -1,260 +0,0 @@ -// ========================================================================== -// AppDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Apps.Utils; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppDomainObject : DomainObjectBase - { - private AppContributors contributors = AppContributors.Empty; - private AppClients clients = AppClients.Empty; - private LanguagesConfig languagesConfig = LanguagesConfig.English; - private AppPlan plan; - private string name; - - public string Name - { - get { return name; } - } - - public AppPlan Plan - { - get { return plan; } - } - - public AppClients Clients - { - get { return clients; } - } - - public AppContributors Contributors - { - get { return contributors; } - } - - public LanguagesConfig LanguagesConfig - { - get { return languagesConfig; } - } - - public AppDomainObject(Guid id, int version) - : base(id, version) - { - } - - protected void On(AppCreated @event) - { - name = @event.Name; - } - - protected void On(AppContributorAssigned @event) - { - contributors = contributors.Apply(@event); - } - - protected void On(AppContributorRemoved @event) - { - contributors = contributors.Apply(@event); - } - - protected void On(AppClientAttached @event) - { - clients = clients.Apply(@event); - } - - protected void On(AppClientUpdated @event) - { - clients = clients.Apply(@event); - } - - protected void On(AppClientRenamed @event) - { - clients = clients.Apply(@event); - } - - protected void On(AppClientRevoked @event) - { - clients = clients.Apply(@event); - } - - protected void On(AppLanguageAdded @event) - { - languagesConfig = languagesConfig.Apply(@event); - } - - protected void On(AppLanguageRemoved @event) - { - languagesConfig = languagesConfig.Apply(@event); - } - - protected void On(AppLanguageUpdated @event) - { - languagesConfig = languagesConfig.Apply(@event); - } - - protected void On(AppPlanChanged @event) - { - plan = string.IsNullOrWhiteSpace(@event.PlanId) ? null : new AppPlan(@event.Actor, @event.PlanId); - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - - public AppDomainObject Create(CreateApp command) - { - ThrowIfCreated(); - - var appId = new NamedId(command.AppId, command.Name); - - RaiseEvent(SimpleMapper.Map(command, new AppCreated { AppId = appId })); - - RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); - RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId))); - - return this; - } - - public AppDomainObject UpdateClient(UpdateClient command) - { - ThrowIfNotCreated(); - - if (!string.IsNullOrWhiteSpace(command.Name)) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - } - - if (command.Permission.HasValue) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value })); - } - - return this; - } - - public AppDomainObject AssignContributor(AssignContributor command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); - - return this; - } - - public AppDomainObject RemoveContributor(RemoveContributor command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); - - return this; - } - - public AppDomainObject AttachClient(AttachClient command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); - - return this; - } - - public AppDomainObject RevokeClient(RevokeClient command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); - - return this; - } - - public AppDomainObject AddLanguage(AddLanguage command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); - - return this; - } - - public AppDomainObject RemoveLanguage(RemoveLanguage command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); - - return this; - } - - public AppDomainObject UpdateLanguage(UpdateLanguage command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); - - return this; - } - - public AppDomainObject ChangePlan(ChangePlan command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); - - return this; - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = new NamedId(Id, name); - } - - RaiseEvent(Envelope.Create(@event)); - } - - private static AppLanguageAdded CreateInitialLanguage(NamedId id) - { - return new AppLanguageAdded { AppId = id, Language = Language.EN }; - } - - private static AppContributorAssigned CreateInitialOwner(NamedId id, SquidexCommand command) - { - return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = AppContributorPermission.Owner }; - } - - private void ThrowIfNotCreated() - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new DomainException("App has not been created."); - } - } - - private void ThrowIfCreated() - { - if (!string.IsNullOrWhiteSpace(name)) - { - throw new DomainException("App has already been created."); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs deleted file mode 100644 index 0d6f3396e..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// AddLanguage.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class AddLanguage : AppAggregateCommand - { - public Language Language { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs deleted file mode 100644 index 361ee348f..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// AssignContributor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class AssignContributor : AppAggregateCommand - { - public string ContributorId { get; set; } - - public AppContributorPermission Permission { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs deleted file mode 100644 index f69cfb384..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// AttachClient.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class AttachClient : AppAggregateCommand - { - public string Id { get; set; } - - public string Secret { get; } = RandomHash.New(); - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs deleted file mode 100644 index 4d84ee36c..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// ChangePlan.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class ChangePlan : AppAggregateCommand - { - public bool FromCallback { get; set; } - - public string PlanId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs deleted file mode 100644 index 9ef163160..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// CreateApp.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class CreateApp : SquidexCommand, IAggregateCommand - { - public string Name { get; set; } - - public Guid AppId { get; set; } - - Guid IAggregateCommand.AggregateId - { - get { return AppId; } - } - - public CreateApp() - { - AppId = Guid.NewGuid(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs deleted file mode 100644 index c579247a3..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// RemoveContributor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class RemoveContributor : AppAggregateCommand - { - public string ContributorId { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs deleted file mode 100644 index 8a08d3c93..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// RemoveLanguage.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class RemoveLanguage : AppAggregateCommand - { - public Language Language { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs deleted file mode 100644 index 68abb555e..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// RevokeClient.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class RevokeClient : AppAggregateCommand - { - public string Id { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs deleted file mode 100644 index 5cbe2496e..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// RenameClient.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class UpdateClient : AppAggregateCommand - { - public string Id { get; set; } - - public string Name { get; set; } - - public AppClientPermission? Permission { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs deleted file mode 100644 index 22092874f..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// UpdateLanguage.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Commands -{ - public sealed class UpdateLanguage : AppAggregateCommand - { - public Language Language { get; set; } - - public bool IsOptional { get; set; } - - public bool IsMaster { get; set; } - - public List Fallback { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs deleted file mode 100644 index 89540ff13..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// GuardApp.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public static class GuardApp - { - public static Task CanCreate(CreateApp command, IAppProvider appProvider) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create app.", async error => - { - if (await appProvider.GetAppAsync(command.Name) != null) - { - error(new ValidationError($"An app with name '{command.Name}' already exists", nameof(command.Name))); - } - - if (!command.Name.IsSlug()) - { - error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); - } - }); - } - - public static void CanChangePlan(ChangePlan command, AppPlan plan, IAppPlansProvider appPlans) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot change plan.", error => - { - if (string.IsNullOrWhiteSpace(command.PlanId)) - { - error(new ValidationError("PlanId is not defined.", nameof(command.PlanId))); - } - else if (appPlans.GetPlan(command.PlanId) == null) - { - error(new ValidationError("Plan id not available.", nameof(command.PlanId))); - } - - if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) - { - error(new ValidationError("Plan can only be changed from current user.")); - } - - if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) - { - error(new ValidationError("App has already this plan.")); - } - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs deleted file mode 100644 index 3a19fe5ea..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// GuardAppClients.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public static class GuardAppClients - { - public static void CanAttach(AppClients clients, AttachClient command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot attach client.", error => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - error(new ValidationError("Client id must be defined.", nameof(command.Id))); - } - else if (clients.ContainsKey(command.Id)) - { - error(new ValidationError("Client id already added.", nameof(command.Id))); - } - }); - } - - public static void CanRevoke(AppClients clients, RevokeClient command) - { - Guard.NotNull(command, nameof(command)); - - GetClientOrThrow(clients, command.Id); - - Validate.It(() => "Cannot revoke client.", error => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - error(new ValidationError("Client id must be defined.", nameof(command.Id))); - } - }); - } - - public static void CanUpdate(AppClients clients, UpdateClient command) - { - Guard.NotNull(command, nameof(command)); - - var client = GetClientOrThrow(clients, command.Id); - - Validate.It(() => "Cannot revoke client.", error => - { - if (string.IsNullOrWhiteSpace(command.Id)) - { - error(new ValidationError("Client id must be defined.", nameof(command.Id))); - } - - if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null) - { - error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission))); - } - - if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue()) - { - error(new ValidationError("Permission is not valid.", nameof(command.Permission))); - } - - if (client != null) - { - if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) - { - error(new ValidationError("Client already has this name.", nameof(command.Permission))); - } - - if (command.Permission == client.Permission) - { - error(new ValidationError("Client already has this permission.", nameof(command.Permission))); - } - } - }); - } - - private static AppClient GetClientOrThrow(AppClients clients, string id) - { - if (id == null) - { - return null; - } - - if (!clients.TryGetValue(id, out var client)) - { - throw new DomainObjectNotFoundException(id, "Clients", typeof(AppDomainObject)); - } - - return client; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs deleted file mode 100644 index 64ecb94f1..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ========================================================================== -// GuardApp.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public static class GuardAppContributors - { - public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot assign contributor.", async error => - { - if (!command.Permission.IsEnumValue()) - { - error(new ValidationError("Permission is not valid.", nameof(command.Permission))); - } - - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); - } - else - { - if (await users.FindByIdAsync(command.ContributorId) == null) - { - error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId))); - } - else if (contributors.TryGetValue(command.ContributorId, out var existing)) - { - if (existing == command.Permission) - { - error(new ValidationError("Contributor has already this permission.", nameof(command.Permission))); - } - } - else if (plan.MaxContributors == contributors.Count) - { - error(new ValidationError("You have reached the maximum number of contributors for your plan.")); - } - } - }); - } - - public static void CanRemove(AppContributors contributors, RemoveContributor command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot remove contributor.", error => - { - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); - } - - var ownerIds = contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList(); - - if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) - { - error(new ValidationError("Cannot remove the only owner.", nameof(command.ContributorId))); - } - }); - - if (!contributors.ContainsKey(command.ContributorId)) - { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppDomainObject)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs deleted file mode 100644 index 1ac39c4b2..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// GuardApp.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps.Guards -{ - public static class GuardAppLanguages - { - public static void CanAdd(LanguagesConfig languages, AddLanguage command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add language.", error => - { - if (command.Language == null) - { - error(new ValidationError("Language cannot be null.", nameof(command.Language))); - } - else if (languages.Contains(command.Language)) - { - error(new ValidationError("Language already added.", nameof(command.Language))); - } - }); - } - - public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) - { - Guard.NotNull(command, nameof(command)); - - var languageConfig = GetLanguageConfigOrThrow(languages, command.Language); - - Validate.It(() => "Cannot remove language.", error => - { - if (command.Language == null) - { - error(new ValidationError("Language cannot be null.", nameof(command.Language))); - } - - if (languages.Master == languageConfig) - { - error(new ValidationError("Language config is master.", nameof(command.Language))); - } - }); - } - - public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) - { - Guard.NotNull(command, nameof(command)); - - var languageConfig = GetLanguageConfigOrThrow(languages, command.Language); - - Validate.It(() => "Cannot update language.", error => - { - if (command.Language == null) - { - error(new ValidationError("Language cannot be null.", nameof(command.Language))); - } - - if ((languages.Master == languageConfig || command.IsMaster) && command.IsOptional) - { - error(new ValidationError("Cannot make master language optional.", nameof(command.IsMaster))); - } - - if (command.Fallback != null) - { - foreach (var fallback in command.Fallback) - { - if (!languages.Contains(fallback)) - { - error(new ValidationError($"Config does not contain fallback language {fallback}.", nameof(command.Fallback))); - } - } - } - }); - } - - private static LanguageConfig GetLanguageConfigOrThrow(LanguagesConfig languages, Language language) - { - if (language == null) - { - return null; - } - - if (!languages.TryGetConfig(language, out var languageConfig)) - { - throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject)); - } - - return languageConfig; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs deleted file mode 100644 index de46cceda..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// AssetCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Domain.Apps.Write.Assets.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - - public AssetCommandMiddleware( - IAggregateHandler handler, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(assetStore, nameof(assetStore)); - Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); - - this.handler = handler; - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - } - - protected async Task On(CreateAsset command, CommandContext context) - { - command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - try - { - var asset = await handler.CreateAsync(context, async a => - { - GuardAsset.CanCreate(command); - - a.Create(command); - - await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); - - context.Complete(EntityCreatedResult.Create(a.Id, a.Version)); - }); - - await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.Id.ToString(), asset.FileVersion, null); - } - finally - { - await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); - } - } - - protected async Task On(UpdateAsset command, CommandContext context) - { - command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - - try - { - var asset = await handler.UpdateAsync(context, async a => - { - GuardAsset.CanUpdate(command); - - a.Update(command); - - await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); - - context.Complete(new AssetSavedResult(a.Version, a.FileVersion)); - }); - - await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.Id.ToString(), asset.FileVersion, null); - } - finally - { - await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); - } - } - - protected Task On(RenameAsset command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAsset.CanRename(command, a.FileName); - - a.Rename(command); - }); - } - - protected Task On(DeleteAsset command, CommandContext context) - { - return handler.UpdateAsync(context, a => - { - GuardAsset.CanDelete(command); - - a.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs deleted file mode 100644 index 6fa06c5a7..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs +++ /dev/null @@ -1,150 +0,0 @@ -// ========================================================================== -// AssetDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetDomainObject : DomainObjectBase - { - private bool isDeleted; - private long fileVersion = -1; - private long totalSize; - private string fileName; - - public bool IsDeleted - { - get { return isDeleted; } - } - - public long FileVersion - { - get { return fileVersion; } - } - - public string FileName - { - get { return fileName; } - } - - public AssetDomainObject(Guid id, int version) - : base(id, version) - { - } - - protected void On(AssetCreated @event) - { - fileVersion = @event.FileVersion; - fileName = @event.FileName; - - totalSize += @event.FileSize; - } - - protected void On(AssetUpdated @event) - { - fileVersion = @event.FileVersion; - - totalSize += @event.FileSize; - } - - protected void On(AssetRenamed @event) - { - fileName = @event.FileName; - } - - protected void On(AssetDeleted @event) - { - isDeleted = true; - } - - public AssetDomainObject Create(CreateAsset command) - { - VerifyNotCreated(); - - var @event = SimpleMapper.Map(command, new AssetCreated - { - FileName = command.File.FileName, - FileSize = command.File.FileSize, - FileVersion = fileVersion + 1, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - IsImage = command.ImageInfo != null - }); - - RaiseEvent(@event); - - return this; - } - - public AssetDomainObject Update(UpdateAsset command) - { - VerifyCreatedAndNotDeleted(); - - var @event = SimpleMapper.Map(command, new AssetUpdated - { - FileVersion = fileVersion + 1, - FileSize = command.File.FileSize, - MimeType = command.File.MimeType, - PixelWidth = command.ImageInfo?.PixelWidth, - PixelHeight = command.ImageInfo?.PixelHeight, - IsImage = command.ImageInfo != null - }); - - RaiseEvent(@event); - - return this; - } - - public AssetDomainObject Delete(DeleteAsset command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = totalSize })); - - return this; - } - - public AssetDomainObject Rename(RenameAsset command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); - - return this; - } - - private void VerifyNotCreated() - { - if (!string.IsNullOrWhiteSpace(fileName)) - { - throw new DomainException("Asset has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (isDeleted || string.IsNullOrWhiteSpace(fileName)) - { - throw new DomainException("Asset has already been deleted or not created yet."); - } - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/AssetSavedResult.cs b/src/Squidex.Domain.Apps.Write/Assets/AssetSavedResult.cs deleted file mode 100644 index 5d38142c6..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/AssetSavedResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// AssetSavedResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetSavedResult : EntitySavedResult - { - public long FileVersion { get; } - - public AssetSavedResult(long version, long fileVersion) - : base(version) - { - FileVersion = fileVersion; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/Commands/AssetAggregateCommand.cs b/src/Squidex.Domain.Apps.Write/Assets/Commands/AssetAggregateCommand.cs deleted file mode 100644 index b62d9dee9..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/Commands/AssetAggregateCommand.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// AssetAggregateCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write.Assets.Commands -{ - public abstract class AssetAggregateCommand : AppCommand, IAggregateCommand - { - public Guid AssetId { get; set; } - - Guid IAggregateCommand.AggregateId - { - get { return AssetId; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Commands/CreateAsset.cs deleted file mode 100644 index 7e11b36e6..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/Commands/CreateAsset.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// CreateAsset.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Assets; - -namespace Squidex.Domain.Apps.Write.Assets.Commands -{ - public sealed class CreateAsset : AssetAggregateCommand - { - public AssetFile File { get; set; } - - public ImageInfo ImageInfo { get; set; } - - public CreateAsset() - { - AssetId = Guid.NewGuid(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/Commands/DeleteAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Commands/DeleteAsset.cs deleted file mode 100644 index a9cfd5cfe..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/Commands/DeleteAsset.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DeleteAsset.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Assets.Commands -{ - public sealed class DeleteAsset : AssetAggregateCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs deleted file mode 100644 index 493acc979..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// RenameAsset.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Assets.Commands -{ - public sealed class RenameAsset : AssetAggregateCommand - { - public string FileName { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Commands/UpdateAsset.cs deleted file mode 100644 index 32b8b3935..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/Commands/UpdateAsset.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// UpdateAsset.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure.Assets; - -namespace Squidex.Domain.Apps.Write.Assets.Commands -{ - public sealed class UpdateAsset : AssetAggregateCommand - { - public AssetFile File { get; set; } - - public ImageInfo ImageInfo { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs deleted file mode 100644 index 1e3a7a57d..000000000 --- a/src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// GuardAsset.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Assets.Guards -{ - public static class GuardAsset - { - public static void CanRename(RenameAsset command, string oldName) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot rename asset.", error => - { - if (string.IsNullOrWhiteSpace(command.FileName)) - { - error(new ValidationError("Name must be defined.", nameof(command.FileName))); - } - - if (string.Equals(command.FileName, oldName)) - { - error(new ValidationError("Name is equal to old name.", nameof(command.FileName))); - } - }); - } - - public static void CanCreate(CreateAsset command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanUpdate(UpdateAsset command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanDelete(DeleteAsset command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs deleted file mode 100644 index 292f257ee..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// ChangeContentStatus.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================= - -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public sealed class ChangeContentStatus : ContentCommand - { - public Status Status { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs deleted file mode 100644 index edf62f437..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// ContentCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Security.Claims; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public abstract class ContentCommand : SchemaCommand, IAggregateCommand - { - public ClaimsPrincipal User { get; set; } - - public Guid ContentId { get; set; } - - Guid IAggregateCommand.AggregateId - { - get { return ContentId; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs deleted file mode 100644 index 02c147574..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// ContentDataCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public abstract class ContentDataCommand : ContentCommand - { - public NamedContentData Data { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs deleted file mode 100644 index 3065c0202..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// CreateContent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public sealed class CreateContent : ContentDataCommand - { - public bool Publish { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/DeleteContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/DeleteContent.cs deleted file mode 100644 index 23638c95b..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/DeleteContent.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DeleteContent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public sealed class DeleteContent : ContentCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs deleted file mode 100644 index ff38a5638..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// PatchContent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public sealed class PatchContent : ContentDataCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs deleted file mode 100644 index be9546173..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// UpdateContent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Contents.Commands -{ - public sealed class UpdateContent : ContentDataCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs deleted file mode 100644 index 5c111c5bd..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================================== -// ContentCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Domain.Apps.Write.Contents.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAppProvider appProvider; - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; - private readonly IScriptEngine scriptEngine; - - public ContentCommandMiddleware( - IAggregateHandler handler, - IAppProvider appProvider, - IAssetRepository assetRepository, - IScriptEngine scriptEngine, - IContentRepository contentRepository) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(contentRepository, nameof(contentRepository)); - - this.handler = handler; - this.appProvider = appProvider; - this.scriptEngine = scriptEngine; - this.assetRepository = assetRepository; - this.contentRepository = contentRepository; - } - - protected async Task On(CreateContent command, CommandContext context) - { - await handler.CreateAsync(context, async content => - { - GuardContent.CanCreate(command); - - var operationContext = await CreateContext(command, content, () => "Failed to create content."); - - if (command.Publish) - { - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); - } - - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); - await operationContext.EnrichAsync(); - await operationContext.ValidateAsync(false); - - content.Create(command); - - context.Complete(EntityCreatedResult.Create(command.Data, content.Version)); - }); - } - - protected async Task On(UpdateContent command, CommandContext context) - { - await handler.UpdateAsync(context, async content => - { - GuardContent.CanUpdate(command); - - var operationContext = await CreateContext(command, content, () => "Failed to update content."); - - await operationContext.ValidateAsync(true); - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); - - content.Update(command); - - context.Complete(new ContentDataChangedResult(content.Data, content.Version)); - }); - } - - protected async Task On(PatchContent command, CommandContext context) - { - await handler.UpdateAsync(context, async content => - { - GuardContent.CanPatch(command); - - var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - - await operationContext.ValidateAsync(true); - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); - - content.Patch(command); - - context.Complete(new ContentDataChangedResult(content.Data, content.Version)); - }); - } - - protected Task On(ChangeContentStatus command, CommandContext context) - { - return handler.UpdateAsync(context, async content => - { - GuardContent.CanChangeContentStatus(content.Status, command); - - var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); - - content.ChangeStatus(command); - }); - } - - protected Task On(DeleteContent command, CommandContext context) - { - return handler.UpdateAsync(context, async content => - { - GuardContent.CanDelete(command); - - var operationContext = await CreateContext(command, content, () => "Failed to delete content."); - - await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); - - content.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - - private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message) - { - var operationContext = - await ContentOperationContext.CreateAsync( - contentRepository, - content, - command, - appProvider, - assetRepository, - scriptEngine, - message); - - return operationContext; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentDataChangedResult.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentDataChangedResult.cs deleted file mode 100644 index 9eb6b0432..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentDataChangedResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// ContentChangedResult.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public sealed class ContentDataChangedResult : EntitySavedResult - { - public NamedContentData Data { get; } - - public ContentDataChangedResult(NamedContentData data, long version) - : base(version) - { - Data = data; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs deleted file mode 100644 index d4c66bec2..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// ContentDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentDomainObject : DomainObjectBase - { - private bool isDeleted; - private bool isCreated; - private Status status; - private NamedContentData data; - - public bool IsDeleted - { - get { return isDeleted; } - } - - public Status Status - { - get { return status; } - } - - public NamedContentData Data - { - get { return data; } - } - - public ContentDomainObject(Guid id, int version) - : base(id, version) - { - } - - protected void On(ContentCreated @event) - { - isCreated = true; - - data = @event.Data; - } - - protected void On(ContentUpdated @event) - { - data = @event.Data; - } - - protected void On(ContentStatusChanged @event) - { - status = @event.Status; - } - - protected void On(ContentDeleted @event) - { - isDeleted = true; - } - - public ContentDomainObject Create(CreateContent command) - { - VerifyNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); - - if (command.Publish) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); - } - - return this; - } - - public ContentDomainObject Delete(DeleteContent command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); - - return this; - } - - public ContentDomainObject ChangeStatus(ChangeContentStatus command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); - - return this; - } - - public ContentDomainObject Update(UpdateContent command) - { - VerifyCreatedAndNotDeleted(); - - if (!command.Data.Equals(Data)) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); - } - - return this; - } - - public ContentDomainObject Patch(PatchContent command) - { - VerifyCreatedAndNotDeleted(); - - var newData = Data.MergeInto(command.Data); - - if (!newData.Equals(Data)) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = newData })); - } - - return this; - } - - private void VerifyNotCreated() - { - if (isCreated) - { - throw new DomainException("Content has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (isDeleted || !isCreated) - { - throw new DomainException("Content has already been deleted or not created yet."); - } - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs deleted file mode 100644 index 530c7505e..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs +++ /dev/null @@ -1,141 +0,0 @@ -// ========================================================================== -// ContentOperationContext.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.EnrichContent; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Tasks; - -#pragma warning disable IDE0017 // Simplify object initialization - -namespace Squidex.Domain.Apps.Write.Contents -{ - public sealed class ContentOperationContext - { - private ContentDomainObject content; - private ContentCommand command; - private IContentRepository contentRepository; - private IAssetRepository assetRepository; - private IScriptEngine scriptEngine; - private ISchemaEntity schemaEntity; - private IAppEntity appEntity; - private Func message; - - public static async Task CreateAsync( - IContentRepository contentRepository, - ContentDomainObject content, - ContentCommand command, - IAppProvider appProvider, - IAssetRepository assetRepository, - IScriptEngine scriptEngine, - Func message) - { - var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Name, command.SchemaId.Id); - - var context = new ContentOperationContext(); - - context.appEntity = appEntity; - context.assetRepository = assetRepository; - context.contentRepository = contentRepository; - context.content = content; - context.command = command; - context.message = message; - context.schemaEntity = schemaEntity; - context.scriptEngine = scriptEngine; - - return context; - } - - public Task EnrichAsync() - { - if (command is ContentDataCommand dataCommand) - { - dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); - } - - return TaskHelper.Done; - } - - public async Task ValidateAsync(bool partial) - { - if (command is ContentDataCommand dataCommand) - { - var errors = new List(); - - var appId = command.AppId.Id; - - var ctx = - new ValidationContext( - (contentIds, schemaId) => - { - return QueryContentsAsync(appId, schemaId, contentIds); - }, - assetIds => - { - return QueryAssetsAsync(appId, assetIds); - }); - - if (partial) - { - await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); - } - else - { - await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); - } - - if (errors.Count > 0) - { - throw new ValidationException(message(), errors.ToArray()); - } - } - } - - private async Task> QueryAssetsAsync(Guid appId, IEnumerable assetIds) - { - return await assetRepository.QueryAsync(appId, null, new HashSet(assetIds), null, int.MaxValue, 0); - } - - private async Task> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable contentIds) - { - return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); - } - - public Task ExecuteScriptAndTransformAsync(Func script, object operation) - { - if (command is ContentDataCommand dataCommand) - { - var ctx = new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data }; - - dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity)); - } - - return TaskHelper.Done; - } - - public Task ExecuteScriptAsync(Func script, object operation) - { - var ctx = new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation.ToString() }; - - scriptEngine.Execute(ctx, script(schemaEntity)); - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs deleted file mode 100644 index 7e6203d5d..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// ContentVersionLoader.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public sealed class ContentVersionLoader : IContentVersionLoader - { - private readonly IStreamNameResolver nameResolver; - private readonly IEventStore eventStore; - private readonly IEventDataFormatter formatter; - - public ContentVersionLoader(IEventStore eventStore, IStreamNameResolver nameResolver, IEventDataFormatter formatter) - { - Guard.NotNull(formatter, nameof(formatter)); - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(nameResolver, nameof(nameResolver)); - - this.formatter = formatter; - this.eventStore = eventStore; - this.nameResolver = nameResolver; - } - - public async Task LoadAsync(Guid appId, Guid id, long version) - { - var streamName = nameResolver.GetStreamName(typeof(ContentDomainObject), id.ToString()); - - var events = await eventStore.GetEventsAsync(streamName); - - if (events.Count == 0 || events.Count < version - 1) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); - } - - NamedContentData contentData = null; - - foreach (var storedEvent in events.Where(x => x.EventStreamNumber <= version)) - { - var envelope = ParseKnownEvent(storedEvent); - - if (envelope != null) - { - if (envelope.Payload is ContentCreated contentCreated) - { - if (contentCreated.AppId.Id != appId) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); - } - - contentData = contentCreated.Data; - } - else if (envelope.Payload is ContentUpdated contentUpdated) - { - contentData = contentUpdated.Data; - } - } - } - - return contentData; - } - - private Envelope ParseKnownEvent(StoredEvent storedEvent) - { - try - { - return formatter.Parse(storedEvent.Data); - } - catch (TypeNameNotFoundException) - { - return null; - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs deleted file mode 100644 index 06178cdb1..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// GuardContent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Contents.Guards -{ - public static class GuardContent - { - public static void CanCreate(CreateContent command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot created content.", error => - { - if (command.Data == null) - { - error(new ValidationError("Data cannot be null.", nameof(command.Data))); - } - }); - } - - public static void CanUpdate(UpdateContent command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot update content.", error => - { - if (command.Data == null) - { - error(new ValidationError("Data cannot be null.", nameof(command.Data))); - } - }); - } - - public static void CanPatch(PatchContent command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot patch content.", error => - { - if (command.Data == null) - { - error(new ValidationError("Data cannot be null.", nameof(command.Data))); - } - }); - } - - public static void CanChangeContentStatus(Status status, ChangeContentStatus command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot change status.", error => - { - if (!StatusFlow.Exists(command.Status) || !StatusFlow.CanChange(status, command.Status)) - { - error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); - } - }); - } - - public static void CanDelete(DeleteContent command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs b/src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs deleted file mode 100644 index e3968fbfc..000000000 --- a/src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// IContentVersionLoader.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public interface IContentVersionLoader - { - Task LoadAsync(Guid appId, Guid id, long version); - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs deleted file mode 100644 index 1c6bafd7e..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// CreateRule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public sealed class CreateRule : RuleEditCommand - { - public CreateRule() - { - RuleId = Guid.NewGuid(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs deleted file mode 100644 index c97f2c6b1..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DeleteRule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public sealed class DeleteRule : RuleAggregateCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs deleted file mode 100644 index ccfa2a9be..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DisableRule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public sealed class DisableRule : RuleAggregateCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs deleted file mode 100644 index ac3bf7f4c..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// EnableRule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public sealed class EnableRule : RuleAggregateCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs deleted file mode 100644 index d240c8957..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// RuleAggregateCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public abstract class RuleAggregateCommand : AppCommand, IAggregateCommand - { - public Guid RuleId { get; set; } - - Guid IAggregateCommand.AggregateId - { - get { return RuleId; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs deleted file mode 100644 index b63be728a..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ========================================================================== -// RuleEditCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Rules; - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public abstract class RuleEditCommand : RuleAggregateCommand - { - public RuleTrigger Trigger { get; set; } - - public RuleAction Action { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs deleted file mode 100644 index 6fe43df4d..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// UpdateRule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Rules.Commands -{ - public sealed class UpdateRule : RuleEditCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs deleted file mode 100644 index 5848f9f1e..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// GuardRule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Write.Rules.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Rules.Guards -{ - public static class GuardRule - { - public static Task CanCreate(CreateRule command, IAppProvider appProvider) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create rule.", async error => - { - if (command.Trigger == null) - { - error(new ValidationError("Trigger must be defined.", nameof(command.Trigger))); - } - else - { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); - - errors.Foreach(error); - } - - if (command.Action == null) - { - error(new ValidationError("Trigger must be defined.", nameof(command.Action))); - } - else - { - var errors = await RuleActionValidator.ValidateAsync(command.Action); - - errors.Foreach(error); - } - }); - } - - public static Task CanUpdate(UpdateRule command, IAppProvider appProvider) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot update rule.", async error => - { - if (command.Trigger == null && command.Action == null) - { - error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action))); - } - - if (command.Trigger != null) - { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); - - errors.Foreach(error); - } - - if (command.Action != null) - { - var errors = await RuleActionValidator.ValidateAsync(command.Action); - - errors.Foreach(error); - } - }); - } - - public static void CanEnable(EnableRule command, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot enable rule.", error => - { - if (rule.IsEnabled) - { - error(new ValidationError("Rule is already enabled.")); - } - }); - } - - public static void CanDisable(DisableRule command, Rule rule) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot disable rule.", error => - { - if (!rule.IsEnabled) - { - error(new ValidationError("Rule is already disabled.")); - } - }); - } - - public static void CanDelete(DeleteRule command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs deleted file mode 100644 index 4ead12956..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// RuleActionValidator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Rules.Guards -{ - public sealed class RuleActionValidator : IRuleActionVisitor>> - { - public static Task> ValidateAsync(RuleAction action) - { - Guard.NotNull(action, nameof(action)); - - var visitor = new RuleActionValidator(); - - return action.Accept(visitor); - } - - public Task> Visit(WebhookAction action) - { - var errors = new List(); - - if (action.Url == null || !action.Url.IsAbsoluteUri) - { - errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); - } - - return Task.FromResult>(errors); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs deleted file mode 100644 index 48dfec259..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// RuleTriggerValidator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Rules.Guards -{ - public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> - { - public Func> SchemaProvider { get; } - - public RuleTriggerValidator(Func> schemaProvider) - { - SchemaProvider = schemaProvider; - } - - public static Task> ValidateAsync(string appName, RuleTrigger action, IAppProvider appProvider) - { - Guard.NotNull(action, nameof(action)); - Guard.NotNull(appProvider, nameof(appProvider)); - - var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appName, x)); - - return action.Accept(visitor); - } - - public async Task> Visit(ContentChangedTrigger trigger) - { - if (trigger.Schemas != null) - { - var schemaErrors = await Task.WhenAll( - trigger.Schemas.Select(async s => - await SchemaProvider(s.SchemaId) == null - ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) - : null)); - - return schemaErrors.Where(x => x != null).ToList(); - } - - return new List(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs deleted file mode 100644 index 302951f90..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// RuleCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Write.Rules.Commands; -using Squidex.Domain.Apps.Write.Rules.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Write.Rules -{ - public class RuleCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAppProvider appProvider; - - public RuleCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.handler = handler; - - this.appProvider = appProvider; - } - - protected Task On(CreateRule command, CommandContext context) - { - return handler.CreateAsync(context, async w => - { - await GuardRule.CanCreate(command, appProvider); - - w.Create(command); - }); - } - - protected Task On(UpdateRule command, CommandContext context) - { - return handler.UpdateAsync(context, async c => - { - await GuardRule.CanUpdate(command, appProvider); - - c.Update(command); - }); - } - - protected Task On(EnableRule command, CommandContext context) - { - return handler.UpdateAsync(context, r => - { - GuardRule.CanEnable(command, r.Rule); - - r.Enable(command); - }); - } - - protected Task On(DisableRule command, CommandContext context) - { - return handler.UpdateAsync(context, r => - { - GuardRule.CanDisable(command, r.Rule); - - r.Disable(command); - }); - } - - protected Task On(DeleteRule command, CommandContext context) - { - return handler.UpdateAsync(context, c => - { - GuardRule.CanDelete(command); - - c.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs deleted file mode 100644 index 82a679cc8..000000000 --- a/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// RuleDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Domain.Apps.Events.Rules.Utils; -using Squidex.Domain.Apps.Write.Rules.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Rules -{ - public class RuleDomainObject : DomainObjectBase - { - private Rule rule; - private bool isDeleted; - - public Rule Rule - { - get { return rule; } - } - - public RuleDomainObject(Guid id, int version) - : base(id, version) - { - } - - protected void On(RuleCreated @event) - { - rule = RuleEventDispatcher.Create(@event); - } - - protected void On(RuleUpdated @event) - { - rule = rule.Apply(@event); - } - - protected void On(RuleEnabled @event) - { - rule = rule.Apply(@event); - } - - protected void On(RuleDisabled @event) - { - rule = rule.Apply(@event); - } - - protected void On(RuleDeleted @event) - { - isDeleted = true; - } - - public void Create(CreateRule command) - { - VerifyNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); - } - - public void Update(UpdateRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); - } - - public void Enable(EnableRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); - } - - public void Disable(DisableRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); - } - - public void Delete(DeleteRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); - } - - private void VerifyNotCreated() - { - if (rule != null) - { - throw new DomainException("Webhook has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (isDeleted || rule == null) - { - throw new DomainException("Webhook has already been deleted or not created yet."); - } - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/SchemaAggregateCommand.cs b/src/Squidex.Domain.Apps.Write/SchemaAggregateCommand.cs deleted file mode 100644 index fc5e43ae8..000000000 --- a/src/Squidex.Domain.Apps.Write/SchemaAggregateCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// SchemaAggregateCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write -{ - public abstract class SchemaAggregateCommand : SchemaCommand, IAggregateCommand - { - Guid IAggregateCommand.AggregateId - { - get { return SchemaId.Id; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/SchemaCommand.cs b/src/Squidex.Domain.Apps.Write/SchemaCommand.cs deleted file mode 100644 index 1d36b3a18..000000000 --- a/src/Squidex.Domain.Apps.Write/SchemaCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// SchemaCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write -{ - public abstract class SchemaCommand : AppCommand - { - public NamedId SchemaId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs deleted file mode 100644 index 8b78836df..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// AddField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class AddField : SchemaAggregateCommand - { - public string Name { get; set; } - - public string Partitioning { get; set; } - - public FieldProperties Properties { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs deleted file mode 100644 index 08453ecb7..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// ConfigureScripts.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class ConfigureScripts : SchemaAggregateCommand - { - public string ScriptQuery { get; set; } - - public string ScriptCreate { get; set; } - - public string ScriptUpdate { get; set; } - - public string ScriptDelete { get; set; } - - public string ScriptChange { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs deleted file mode 100644 index b8a9bbcd0..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// CreateSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.Commands; -using SchemaFields = System.Collections.Generic.List; - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class CreateSchema : AppCommand, IAggregateCommand - { - public Guid SchemaId { get; set; } - - public SchemaFields Fields { get; set; } - - public SchemaProperties Properties { get; set; } - - public string Name { get; set; } - - Guid IAggregateCommand.AggregateId - { - get { return SchemaId; } - } - - public CreateSchema() - { - SchemaId = Guid.NewGuid(); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs deleted file mode 100644 index d532a0a76..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// CreateSchemaField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class CreateSchemaField - { - public string Partitioning { get; set; } - - public string Name { get; set; } - - public bool IsHidden { get; set; } - - public bool IsLocked { get; set; } - - public bool IsDisabled { get; set; } - - public FieldProperties Properties { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteField.cs deleted file mode 100644 index 36ea19310..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteField.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DeleteField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class DeleteField : FieldCommand - { - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteSchema.cs deleted file mode 100644 index 1384453b0..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteSchema.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DeleteSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class DeleteSchema : SchemaAggregateCommand - { - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/DisableField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/DisableField.cs deleted file mode 100644 index 95dc2d327..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/DisableField.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// DisableField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class DisableField : FieldCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/EnableField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/EnableField.cs deleted file mode 100644 index 1ac8930b6..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/EnableField.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// EnableField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class EnableField : FieldCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/FieldCommand.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/FieldCommand.cs deleted file mode 100644 index 4fe5f6b6f..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/FieldCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// FieldCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public class FieldCommand : SchemaAggregateCommand - { - public long FieldId { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/HideField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/HideField.cs deleted file mode 100644 index b8fcde46f..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/HideField.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// HideField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class HideField : FieldCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs deleted file mode 100644 index 3109fcb6e..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// LockField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class LockField : FieldCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/PublishSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/PublishSchema.cs deleted file mode 100644 index a0385926d..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/PublishSchema.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// PublishSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class PublishSchema : SchemaAggregateCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs deleted file mode 100644 index 2d3805fb1..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// ReorderFields.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class ReorderFields : SchemaAggregateCommand - { - public List FieldIds { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ShowField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ShowField.cs deleted file mode 100644 index 024148139..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ShowField.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// ShowField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class ShowField : FieldCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UnpublishSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UnpublishSchema.cs deleted file mode 100644 index f8e83ce02..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UnpublishSchema.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// UnpublishSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class UnpublishSchema : SchemaAggregateCommand - { - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs deleted file mode 100644 index 1f70668cb..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// UpdateField.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class UpdateField : FieldCommand - { - public FieldProperties Properties { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs deleted file mode 100644 index ba711a5a0..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// UpdateSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Write.Schemas.Commands -{ - public sealed class UpdateSchema : SchemaAggregateCommand - { - public SchemaProperties Properties { get; set; } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs deleted file mode 100644 index f08405643..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs +++ /dev/null @@ -1,232 +0,0 @@ -// ========================================================================== -// FieldPropertiesValidator.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public sealed class FieldPropertiesValidator : IFieldPropertiesVisitor> - { - private static readonly FieldPropertiesValidator Instance = new FieldPropertiesValidator(); - - private FieldPropertiesValidator() - { - } - - public static IEnumerable Validate(FieldProperties properties) - { - return properties?.Accept(Instance) ?? Enumerable.Empty(); - } - - public IEnumerable Visit(AssetsFieldProperties properties) - { - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value) - { - yield return new ValidationError("Max items must be greater than min items.", - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } - - if (properties.MaxHeight.HasValue && properties.MinHeight.HasValue && properties.MinHeight.Value >= properties.MaxHeight.Value) - { - yield return new ValidationError("Max height must be greater than min height.", - nameof(properties.MaxHeight), - nameof(properties.MinHeight)); - } - - if (properties.MaxWidth.HasValue && properties.MinWidth.HasValue && properties.MinWidth.Value >= properties.MaxWidth.Value) - { - yield return new ValidationError("Max width must be greater than min width.", - nameof(properties.MaxWidth), - nameof(properties.MinWidth)); - } - - if (properties.MaxSize.HasValue && properties.MinSize.HasValue && properties.MinSize.Value >= properties.MaxSize.Value) - { - yield return new ValidationError("Max size must be greater than min size.", - nameof(properties.MaxSize), - nameof(properties.MinSize)); - } - - if (properties.AspectWidth.HasValue != properties.AspectHeight.HasValue) - { - yield return new ValidationError("Aspect width and height must be defined.", - nameof(properties.AspectWidth), - nameof(properties.AspectHeight)); - } - } - - public IEnumerable Visit(BooleanFieldProperties properties) - { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError("Editor is not a valid value.", - nameof(properties.Editor)); - } - } - - public IEnumerable Visit(DateTimeFieldProperties properties) - { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError("Editor is not a valid value.", - nameof(properties.Editor)); - } - - if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) - { - yield return new ValidationError("Default value must be greater than min value.", - nameof(properties.DefaultValue)); - } - - if (properties.DefaultValue.HasValue && properties.MaxValue.HasValue && properties.DefaultValue.Value > properties.MaxValue.Value) - { - yield return new ValidationError("Default value must be less than max value.", - nameof(properties.DefaultValue)); - } - - if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue.Value >= properties.MaxValue.Value) - { - yield return new ValidationError("Max value must be greater than min value.", - nameof(properties.MinValue), - nameof(properties.MaxValue)); - } - - if (properties.CalculatedDefaultValue.HasValue) - { - if (!properties.CalculatedDefaultValue.Value.IsEnumValue()) - { - yield return new ValidationError("Calculated default value is not valid.", - nameof(properties.CalculatedDefaultValue)); - } - - if (properties.DefaultValue.HasValue) - { - yield return new ValidationError("Calculated default value and default value cannot be used together.", - nameof(properties.CalculatedDefaultValue), - nameof(properties.DefaultValue)); - } - } - } - - public IEnumerable Visit(GeolocationFieldProperties properties) - { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError("Editor is not a valid value.", - nameof(properties.Editor)); - } - } - - public IEnumerable Visit(JsonFieldProperties properties) - { - yield break; - } - - public IEnumerable Visit(NumberFieldProperties properties) - { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError("Editor is not a valid value.", - nameof(properties.Editor)); - } - - if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) - { - yield return new ValidationError("Radio buttons or dropdown list need allowed values.", - nameof(properties.AllowedValues)); - } - - if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) - { - yield return new ValidationError("Default value must be greater than min value.", - nameof(properties.DefaultValue)); - } - - if (properties.DefaultValue.HasValue && properties.MaxValue.HasValue && properties.DefaultValue.Value > properties.MaxValue.Value) - { - yield return new ValidationError("Default value must be less than max value.", - nameof(properties.DefaultValue)); - } - - if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue.Value >= properties.MaxValue.Value) - { - yield return new ValidationError("Max value must be greater than min value.", - nameof(properties.MinValue), - nameof(properties.MaxValue)); - } - - if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) - { - yield return new ValidationError("Either allowed values or min and max value can be defined.", - nameof(properties.AllowedValues), - nameof(properties.MinValue), - nameof(properties.MaxValue)); - } - } - - public IEnumerable Visit(ReferencesFieldProperties properties) - { - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value) - { - yield return new ValidationError("Max items must be greater than min items.", - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } - } - - public IEnumerable Visit(StringFieldProperties properties) - { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError("Editor is not a valid value.", - nameof(properties.Editor)); - } - - if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) - { - yield return new ValidationError("Radio buttons or dropdown list need allowed values.", - nameof(properties.AllowedValues)); - } - - if (properties.Pattern != null && !properties.Pattern.IsValidRegex()) - { - yield return new ValidationError("Pattern is not a valid expression.", - nameof(properties.Pattern)); - } - - if (properties.MaxLength.HasValue && properties.MinLength.HasValue && properties.MinLength.Value >= properties.MaxLength.Value) - { - yield return new ValidationError("Max length must be greater than min length.", - nameof(properties.MinLength), - nameof(properties.MaxLength)); - } - - if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) - { - yield return new ValidationError("Either allowed values or min and max length can be defined.", - nameof(properties.AllowedValues), - nameof(properties.MinLength), - nameof(properties.MaxLength)); - } - } - - public IEnumerable Visit(TagsFieldProperties properties) - { - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value) - { - yield return new ValidationError("Max items must be greater than min items.", - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs deleted file mode 100644 index b9006a315..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ========================================================================== -// GuardSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public static class GuardSchema - { - public static Task CanCreate(CreateSchema command, IAppProvider appProvider) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create schema.", async error => - { - if (!command.Name.IsSlug()) - { - error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); - } - - if (await appProvider.GetSchemaAsync(command.AppId.Name, command.Name) != null) - { - error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name))); - } - - if (command.Fields != null && command.Fields.Any()) - { - var index = 0; - - foreach (var field in command.Fields) - { - var prefix = $"Fields.{index}"; - - if (!field.Partitioning.IsValidPartitioning()) - { - error(new ValidationError("Partitioning is not valid.", $"{prefix}.{nameof(field.Partitioning)}")); - } - - if (!field.Name.IsPropertyName()) - { - error(new ValidationError("Name must be a valid property name.", $"{prefix}.{nameof(field.Name)}")); - } - - if (field.Properties == null) - { - error(new ValidationError("Properties must be defined.", $"{prefix}.{nameof(field.Properties)}")); - } - - var propertyErrors = FieldPropertiesValidator.Validate(field.Properties); - - foreach (var propertyError in propertyErrors) - { - error(propertyError); - } - } - - if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count) - { - error(new ValidationError("Fields cannot have duplicate names.", nameof(command.Fields))); - } - } - }); - } - - public static void CanReorder(Schema schema, ReorderFields command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot reorder schema fields.", error => - { - if (command.FieldIds == null) - { - error(new ValidationError("Field ids must be specified.", nameof(command.FieldIds))); - } - - if (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x))) - { - error(new ValidationError("Ids must cover all fields.", nameof(command.FieldIds))); - } - }); - } - - public static void CanPublish(Schema schema, PublishSchema command) - { - Guard.NotNull(command, nameof(command)); - - if (schema.IsPublished) - { - throw new DomainException("Schema is already published."); - } - } - - public static void CanUnpublish(Schema schema, UnpublishSchema command) - { - Guard.NotNull(command, nameof(command)); - - if (!schema.IsPublished) - { - throw new DomainException("Schema is not published."); - } - } - - public static void CanUpdate(Schema schema, UpdateSchema command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanConfigureScripts(Schema schema, ConfigureScripts command) - { - Guard.NotNull(command, nameof(command)); - } - - public static void CanDelete(Schema schema, DeleteSchema command) - { - Guard.NotNull(command, nameof(command)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs deleted file mode 100644 index 67a9eeb77..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs +++ /dev/null @@ -1,160 +0,0 @@ -// ========================================================================== -// SchemaFieldGuard.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public static class GuardSchemaField - { - public static void CanAdd(Schema schema, AddField command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot add a new field.", error => - { - if (!command.Partitioning.IsValidPartitioning()) - { - error(new ValidationError("Partitioning is not valid.", nameof(command.Partitioning))); - } - - if (!command.Name.IsPropertyName()) - { - error(new ValidationError("Name must be a valid property name.", nameof(command.Name))); - } - - if (command.Properties == null) - { - error(new ValidationError("Properties must be defined.", nameof(command.Properties))); - } - - var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); - - foreach (var propertyError in propertyErrors) - { - error(propertyError); - } - - if (schema.FieldsByName.ContainsKey(command.Name)) - { - error(new ValidationError($"There is already a field with name '{command.Name}'", nameof(command.Name))); - } - }); - } - - public static void CanUpdate(Schema schema, UpdateField command) - { - Guard.NotNull(command, nameof(command)); - - Validate.It(() => "Cannot update field.", error => - { - if (command.Properties == null) - { - error(new ValidationError("Properties must be defined.", nameof(command.Properties))); - } - - var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); - - foreach (var propertyError in propertyErrors) - { - error(propertyError); - } - }); - - var field = GetFieldOrThrow(schema, command.FieldId); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } - } - - public static void CanDelete(Schema schema, DeleteField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GetFieldOrThrow(schema, command.FieldId); - - if (field.IsLocked) - { - throw new DomainException("Schema field is locked."); - } - } - - public static void CanHide(Schema schema, HideField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GetFieldOrThrow(schema, command.FieldId); - - if (field.IsHidden) - { - throw new DomainException("Schema field is already hidden."); - } - } - - public static void CanShow(Schema schema, ShowField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GetFieldOrThrow(schema, command.FieldId); - - if (!field.IsHidden) - { - throw new DomainException("Schema field is already visible."); - } - } - - public static void CanDisable(Schema schema, DisableField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GetFieldOrThrow(schema, command.FieldId); - - if (field.IsDisabled) - { - throw new DomainException("Schema field is already disabled."); - } - } - - public static void CanEnable(Schema schema, EnableField command) - { - var field = GetFieldOrThrow(schema, command.FieldId); - - if (!field.IsDisabled) - { - throw new DomainException("Schema field is already enabled."); - } - } - - public static void CanLock(Schema schema, LockField command) - { - Guard.NotNull(command, nameof(command)); - - var field = GetFieldOrThrow(schema, command.FieldId); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } - } - - private static Field GetFieldOrThrow(Schema schema, long fieldId) - { - if (!schema.FieldsById.TryGetValue(fieldId, out var field)) - { - throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema)); - } - - return field; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs deleted file mode 100644 index 71837b91a..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs +++ /dev/null @@ -1,198 +0,0 @@ -// ========================================================================== -// SchemaCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Domain.Apps.Write.Schemas.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Write.Schemas -{ - public class SchemaCommandMiddleware : ICommandMiddleware - { - private readonly IAppProvider appProvider; - private readonly IAggregateHandler handler; - - public SchemaCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.handler = handler; - - this.appProvider = appProvider; - } - - protected Task On(CreateSchema command, CommandContext context) - { - return handler.CreateAsync(context, async s => - { - await GuardSchema.CanCreate(command, appProvider); - - s.Create(command); - - context.Complete(EntityCreatedResult.Create(s.Id, s.Version)); - }); - } - - protected Task On(AddField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanAdd(s.Schema, command); - - s.Add(command); - - context.Complete(EntityCreatedResult.Create(s.Schema.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version)); - }); - } - - protected Task On(DeleteField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanDelete(s.Schema, command); - - s.DeleteField(command); - }); - } - - protected Task On(LockField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanLock(s.Schema, command); - - s.LockField(command); - }); - } - - protected Task On(HideField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanHide(s.Schema, command); - - s.HideField(command); - }); - } - - protected Task On(ShowField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanShow(s.Schema, command); - - s.ShowField(command); - }); - } - - protected Task On(DisableField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanDisable(s.Schema, command); - - s.DisableField(command); - }); - } - - protected Task On(EnableField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanEnable(s.Schema, command); - - s.EnableField(command); - }); - } - - protected Task On(UpdateField command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchemaField.CanUpdate(s.Schema, command); - - s.UpdateField(command); - }); - } - - protected Task On(ReorderFields command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchema.CanReorder(s.Schema, command); - - s.Reorder(command); - }); - } - - protected Task On(UpdateSchema command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchema.CanUpdate(s.Schema, command); - - s.Update(command); - }); - } - - protected Task On(PublishSchema command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchema.CanPublish(s.Schema, command); - - s.Publish(command); - }); - } - - protected Task On(UnpublishSchema command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchema.CanUnpublish(s.Schema, command); - - s.Unpublish(command); - }); - } - - protected Task On(ConfigureScripts command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchema.CanConfigureScripts(s.Schema, command); - - s.ConfigureScripts(command); - }); - } - - protected Task On(DeleteSchema command, CommandContext context) - { - return handler.UpdateAsync(context, s => - { - GuardSchema.CanDelete(s.Schema, command); - - s.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs deleted file mode 100644 index 3c264b974..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs +++ /dev/null @@ -1,304 +0,0 @@ -// ========================================================================== -// SchemaDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Events.Schemas.Utils; -using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Schemas -{ - public class SchemaDomainObject : DomainObjectBase - { - private readonly FieldRegistry registry; - private bool isDeleted; - private long totalFields; - private Schema schema; - - public Schema Schema - { - get { return schema; } - } - - public bool IsDeleted - { - get { return isDeleted; } - } - - public SchemaDomainObject(Guid id, int version, FieldRegistry registry) - : base(id, version) - { - Guard.NotNull(registry, nameof(registry)); - - this.registry = registry; - } - - protected void On(SchemaCreated @event) - { - totalFields += @event.Fields?.Count ?? 0; - - schema = SchemaEventDispatcher.Create(@event, registry); - } - - public void On(FieldAdded @event) - { - totalFields++; - - schema = schema.Apply(@event, registry); - } - - protected void On(FieldUpdated @event) - { - schema = schema.Apply(@event); - } - - protected void On(FieldLocked @event) - { - schema = schema.Apply(@event); - } - - protected void On(FieldHidden @event) - { - schema = schema.Apply(@event); - } - - protected void On(FieldShown @event) - { - schema = schema.Apply(@event); - } - - protected void On(FieldDisabled @event) - { - schema = schema.Apply(@event); - } - - protected void On(FieldEnabled @event) - { - schema = schema.Apply(@event); - } - - protected void On(SchemaUpdated @event) - { - schema = schema.Apply(@event); - } - - protected void On(FieldDeleted @event) - { - schema = schema.Apply(@event); - } - - protected void On(SchemaFieldsReordered @event) - { - schema = schema.Apply(@event); - } - - protected void On(SchemaPublished @event) - { - schema = schema.Apply(@event); - } - - protected void On(SchemaUnpublished @event) - { - schema = schema.Apply(@event); - } - - protected void On(SchemaDeleted @event) - { - isDeleted = true; - } - - public SchemaDomainObject Create(CreateSchema command) - { - VerifyNotCreated(); - - var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(Id, command.Name) }); - - if (command.Fields != null) - { - @event.Fields = new List(); - - foreach (var commandField in command.Fields) - { - var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField()); - - @event.Fields.Add(eventField); - } - } - - RaiseEvent(@event); - - return this; - } - - public SchemaDomainObject Add(AddField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(totalFields + 1, command.Name) })); - - return this; - } - - public SchemaDomainObject UpdateField(UpdateField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated())); - - return this; - } - - public SchemaDomainObject LockField(LockField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldLocked()); - - return this; - } - - public SchemaDomainObject HideField(HideField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldHidden()); - - return this; - } - - public SchemaDomainObject ShowField(ShowField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldShown()); - - return this; - } - - public SchemaDomainObject DisableField(DisableField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldDisabled()); - - return this; - } - - public SchemaDomainObject EnableField(EnableField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldEnabled()); - - return this; - } - - public SchemaDomainObject DeleteField(DeleteField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldDeleted()); - - return this; - } - - public SchemaDomainObject Reorder(ReorderFields command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); - - return this; - } - - public SchemaDomainObject Publish(PublishSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaPublished())); - - return this; - } - - public SchemaDomainObject Unpublish(UnpublishSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished())); - - return this; - } - - public SchemaDomainObject ConfigureScripts(ConfigureScripts command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured())); - - return this; - } - - public SchemaDomainObject Delete(DeleteSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted())); - - return this; - } - - public SchemaDomainObject Update(UpdateSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated())); - - return this; - } - - protected void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event) - { - SimpleMapper.Map(fieldCommand, @event); - - if (schema.FieldsById.TryGetValue(fieldCommand.FieldId, out var field)) - { - @event.FieldId = new NamedId(field.Id, field.Name); - } - - RaiseEvent(@event); - } - - private void VerifyNotCreated() - { - if (schema != null) - { - throw new DomainException("Schema has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (isDeleted || schema == null) - { - throw new DomainException("Schema has already been deleted or not created yet."); - } - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj b/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj deleted file mode 100644 index 54328c3a8..000000000 --- a/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - netstandard2.0 - - - full - True - - - - - - - - - - - - - - - - - ..\..\Squidex.ruleset - - diff --git a/src/Squidex.Domain.Apps.Write/SquidexCommand.cs b/src/Squidex.Domain.Apps.Write/SquidexCommand.cs deleted file mode 100644 index 09507a691..000000000 --- a/src/Squidex.Domain.Apps.Write/SquidexCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// SquidexCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Write -{ - public abstract class SquidexCommand : ICommand - { - public RefToken Actor { get; set; } - - public long? ExpectedVersion { get; set; } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs index e81626185..b9abb0515 100644 --- a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs +++ b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs @@ -21,9 +21,9 @@ namespace Squidex.Infrastructure.Migrations { } - public override void Connect() + protected override string CollectionName() { - base.Connect(); + return "Migration"; } public async Task GetVersionAsync() diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 5183bc696..056dd8595 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -106,7 +106,7 @@ namespace Squidex.Infrastructure.MongoDb } } - public virtual void Connect() + public void Connect() { try { diff --git a/src/Squidex.Infrastructure.Redis/RedisSubscription.cs b/src/Squidex.Infrastructure.Redis/RedisSubscription.cs index 1a9d72b1f..2bd3fbe75 100644 --- a/src/Squidex.Infrastructure.Redis/RedisSubscription.cs +++ b/src/Squidex.Infrastructure.Redis/RedisSubscription.cs @@ -55,7 +55,7 @@ namespace Squidex.Infrastructure { log.LogError(ex, w => w .WriteProperty("action", "PublishRedisMessage") - .WriteProperty("state", "Failed") + .WriteProperty("status", "Failed") .WriteProperty("channel", channelName)); } } @@ -78,7 +78,7 @@ namespace Squidex.Infrastructure log.LogDebug(w => w .WriteProperty("action", "ReceiveRedisMessage") .WriteProperty("channel", channelName) - .WriteProperty("state", "Received")); + .WriteProperty("status", "Received")); } } catch (Exception ex) @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure log.LogError(ex, w => w .WriteProperty("action", "ReceiveRedisMessage") .WriteProperty("channel", channelName) - .WriteProperty("state", "Failed")); + .WriteProperty("status", "Failed")); } } diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index b8351396c..56149843b 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -82,6 +82,13 @@ namespace Squidex.Infrastructure.Commands { } + public Task WriteStateAsync(long version) + { + state.Version = version; + + return persistence.WriteSnapshotAsync(state); + } + public async Task WriteAsync(ISemanticLog log) { var events = uncomittedEvents.ToArray(); diff --git a/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs index 7356207cb..f0df32720 100644 --- a/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs +++ b/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs @@ -30,13 +30,13 @@ namespace Squidex.Infrastructure.Commands log.LogInformation(w => w .WriteProperty("action", "HandleCommand.") .WriteProperty("actionId", context.ContextId.ToString()) - .WriteProperty("state", "Started") + .WriteProperty("status", "Started") .WriteProperty("commandType", context.Command.GetType().Name)); using (log.MeasureInformation(w => w .WriteProperty("action", "HandleCommand.") .WriteProperty("actionId", context.ContextId.ToString()) - .WriteProperty("state", "Completed") + .WriteProperty("status", "Completed") .WriteProperty("commandType", context.Command.GetType().Name))) { await next(); @@ -45,7 +45,7 @@ namespace Squidex.Infrastructure.Commands log.LogInformation(w => w .WriteProperty("action", "HandleCommand.") .WriteProperty("actionId", context.ContextId.ToString()) - .WriteProperty("state", "Succeeded") + .WriteProperty("status", "Succeeded") .WriteProperty("commandType", context.Command.GetType().Name)); } catch (Exception ex) @@ -53,7 +53,7 @@ namespace Squidex.Infrastructure.Commands log.LogError(ex, w => w .WriteProperty("action", "HandleCommand.") .WriteProperty("actionId", context.ContextId.ToString()) - .WriteProperty("state", "Failed") + .WriteProperty("status", "Failed") .WriteProperty("commandType", context.Command.GetType().Name)); throw; @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.Commands log.LogFatal(w => w .WriteProperty("action", "HandleCommand.") .WriteProperty("actionId", context.ContextId.ToString()) - .WriteProperty("state", "Unhandled") + .WriteProperty("status", "Unhandled") .WriteProperty("commandType", context.Command.GetType().Name)); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index ef4cd9978..346e69345 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -213,7 +213,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains log.LogFatal(ex, w => w .WriteProperty("action", caller) - .WriteProperty("state", "Failed") + .WriteProperty("status", "Failed") .WriteProperty("eventConsumer", eventConsumer.Name)); state = state.Failed(ex); @@ -229,13 +229,13 @@ namespace Squidex.Infrastructure.EventSourcing.Grains log.LogInformation(w => w .WriteProperty("action", "EventConsumerReset") .WriteProperty("actionId", actionId) - .WriteProperty("state", "Started") + .WriteProperty("status", "Started") .WriteProperty("eventConsumer", eventConsumer.Name)); using (log.MeasureTrace(w => w .WriteProperty("action", "EventConsumerReset") .WriteProperty("actionId", actionId) - .WriteProperty("state", "Completed") + .WriteProperty("status", "Completed") .WriteProperty("eventConsumer", eventConsumer.Name))) { await eventConsumer.ClearAsync(); @@ -250,7 +250,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains log.LogInformation(w => w .WriteProperty("action", "HandleEvent") .WriteProperty("actionId", eventId) - .WriteProperty("state", "Started") + .WriteProperty("status", "Started") .WriteProperty("eventId", eventId) .WriteProperty("eventType", eventType) .WriteProperty("eventConsumer", eventConsumer.Name)); @@ -258,7 +258,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains using (log.MeasureTrace(w => w .WriteProperty("action", "HandleEvent") .WriteProperty("actionId", eventId) - .WriteProperty("state", "Completed") + .WriteProperty("status", "Completed") .WriteProperty("eventId", eventId) .WriteProperty("eventType", eventType) .WriteProperty("eventConsumer", eventConsumer.Name))) diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs index 0be44904a..3ba7e29ce 100644 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -55,9 +55,17 @@ namespace Squidex.Infrastructure.Migrations foreach (var migrator in migrationPath) { + var name = migrator.GetType().ToString(); + + log.LogInformation(w => w + .WriteProperty("action", "Migration") + .WriteProperty("status", "Started") + .WriteProperty("migrator", name)); + using (log.MeasureInformation(w => w .WriteProperty("action", "Migration") - .WriteProperty("migrator", migrator.GetType().ToString()))) + .WriteProperty("status", "Completed") + .WriteProperty("migrator", name))) { await migrator.UpdateAsync(); diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 5552c561a..b8cd37168 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -19,6 +19,7 @@ using Squidex.Infrastructure.Assets.ImageSharp; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; using Squidex.Pipeline; @@ -91,6 +92,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + + services.AddSingletonAs() + .AsSelf(); } } } diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index 2fd571e14..54c9958ce 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -109,8 +109,11 @@ namespace Squidex.Config.Domain return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); }); - services.AddSingletonAs(); - services.AddSingleton(); + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); } } } diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index e95127b89..fe051bd44 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -7,6 +7,7 @@ // ========================================================================== using Microsoft.Extensions.DependencyInjection; +using Migrate_01; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NodaTime; @@ -26,7 +27,12 @@ namespace Squidex.Config.Domain { public static class SerializationServices { - private static readonly TypeNameRegistry TypeNameRegistry = new TypeNameRegistry(); + private static readonly TypeNameRegistry TypeNameRegistry = + new TypeNameRegistry() + .MapUnmapped(typeof(Migration01).Assembly) + .MapUnmapped(typeof(SquidexCoreModel).Assembly) + .MapUnmapped(typeof(SquidexEvents).Assembly) + .MapUnmapped(typeof(SquidexInfrastructure).Assembly); private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry); @@ -68,10 +74,6 @@ namespace Squidex.Config.Domain static SerializationServices() { - TypeNameRegistry.MapUnmapped(typeof(SquidexCoreModel).Assembly); - TypeNameRegistry.MapUnmapped(typeof(SquidexEvents).Assembly); - TypeNameRegistry.MapUnmapped(typeof(SquidexInfrastructure).Assembly); - ConfigureJson(SerializerSettings, TypeNameHandling.Auto); BsonJsonConvention.Register(JsonSerializer.Create(SerializerSettings)); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 416402e05..ce4fe184d 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -107,7 +107,7 @@ namespace Squidex.Config.Domain .As>() .As(); - services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase)) + services.AddSingletonAs(c => new MongoRuleRepository(mongoDatabase)) .As() .As>() .As(); diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 6a910e333..524c3e121 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -7,6 +7,7 @@ // ========================================================================== using Microsoft.Extensions.DependencyInjection; +using Migrate_01; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; @@ -15,6 +16,7 @@ using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Migrations; using Squidex.Pipeline.CommandMiddlewares; namespace Squidex.Config.Domain @@ -59,11 +61,23 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); } } } diff --git a/src/Squidex/Config/ServiceExtensions.cs b/src/Squidex/Config/ServiceExtensions.cs index 61050959f..f2c670304 100644 --- a/src/Squidex/Config/ServiceExtensions.cs +++ b/src/Squidex/Config/ServiceExtensions.cs @@ -25,6 +25,11 @@ namespace Squidex.Config this.services = services; } + public InterfaceRegistrator AsSelf() + { + return this; + } + public InterfaceRegistrator As() { if (typeof(TInterface) != typeof(T)) @@ -39,6 +44,20 @@ namespace Squidex.Config } } + public static InterfaceRegistrator AddTransientAs(this IServiceCollection services, Func factory) where T : class + { + services.AddTransient(typeof(T), factory); + + return new InterfaceRegistrator(services); + } + + public static InterfaceRegistrator AddTransientAs(this IServiceCollection services) where T : class + { + services.AddTransient(); + + return new InterfaceRegistrator(services); + } + public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, Func factory) where T : class { services.AddSingleton(typeof(T), factory); diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index bd330a626..2d5357db2 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index fef509b0b..d88b28055 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -39,8 +39,8 @@ namespace Squidex public void Configure(IApplicationBuilder app) { app.ApplicationServices.LogConfiguration(); - app.ApplicationServices.TestExternalSystems(); app.ApplicationServices.Migrate(); + app.ApplicationServices.TestExternalSystems(); app.UseMyCors(); app.UseMyForwardingRules(); diff --git a/tools/Migrate_01/Migrate_01.csproj b/tools/Migrate_01/Migrate_01.csproj new file mode 100644 index 000000000..c782b2dec --- /dev/null +++ b/tools/Migrate_01/Migrate_01.csproj @@ -0,0 +1,14 @@ + + + netstandard2.0 + + + + + + + + + ..\..\Squidex.ruleset + + diff --git a/tools/Migrate_01/Migration01.cs b/tools/Migrate_01/Migration01.cs new file mode 100644 index 000000000..e2b1d6d0d --- /dev/null +++ b/tools/Migrate_01/Migration01.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// MigrateToEntities.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using System.Timers; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Migrate_01 +{ + public sealed class Migration01 : IMigration, IEventSubscriber + { + private readonly FieldRegistry fieldRegistry; + private readonly IEventStore eventStore; + private readonly IEventDataFormatter eventDataFormatter; + private readonly IStateFactory stateFactory; + private readonly Timer timer = new Timer { AutoReset = false, Interval = 5000 }; + private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + + public int FromVersion { get; } = 0; + + public int ToVersion { get; } = 1; + + public Migration01( + FieldRegistry fieldRegistry, + IEventDataFormatter eventDataFormatter, + IEventStore eventStore, + IStateFactory stateFactory) + { + this.fieldRegistry = fieldRegistry; + this.eventDataFormatter = eventDataFormatter; + this.eventStore = eventStore; + this.stateFactory = stateFactory; + + timer.Elapsed += (sender, e) => + { + tcs.TrySetResult(true); + }; + } + + public async Task UpdateAsync() + { + var subscription = eventStore.CreateSubscription(this, ".*"); + + await tcs.Task; + await subscription.StopAsync(); + } + + public async Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + var @event = ParseKnownEvent(storedEvent); + + if (@event != null) + { + var version = storedEvent.EventStreamNumber; + + if (@event.Payload is AssetEvent assetEvent) + { + var asset = await stateFactory.CreateAsync(assetEvent.AssetId); + + asset.UpdateState(asset.State.Apply(@event)); + + await asset.WriteStateAsync(version); + } + else if (@event.Payload is ContentEvent contentEvent) + { + var content = await stateFactory.CreateAsync(contentEvent.ContentId); + + content.UpdateState(content.State.Apply(@event)); + + await content.WriteStateAsync(version); + } + else if (@event.Payload is SchemaEvent schemaEvent) + { + var schema = await stateFactory.GetSingleAsync(schemaEvent.SchemaId.Id); + + schema.UpdateState(schema.State.Apply(@event, fieldRegistry)); + + await schema.WriteStateAsync(version); + } + else if (@event.Payload is AppEvent appEvent) + { + var app = await stateFactory.GetSingleAsync(appEvent.AppId.Id); + + app.UpdateState(app.State.Apply(@event)); + + await app.WriteStateAsync(version); + } + } + + timer.Stop(); + timer.Start(); + } + + public Task OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return TaskHelper.Done; + } + + private Envelope ParseKnownEvent(StoredEvent storedEvent) + { + try + { + return eventDataFormatter.Parse(storedEvent.Data); + } + catch (TypeNameNotFoundException) + { + return null; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/Old/AppClientChanged.cs b/tools/Migrate_01/OldEvents/AppClientChanged.cs similarity index 87% rename from src/Squidex.Domain.Apps.Events/Apps/Old/AppClientChanged.cs rename to tools/Migrate_01/OldEvents/AppClientChanged.cs index 7e459b244..63df3e55a 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/Old/AppClientChanged.cs +++ b/tools/Migrate_01/OldEvents/AppClientChanged.cs @@ -6,13 +6,17 @@ // All rights reserved. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Events.Apps.Old +namespace Migrate_01.OldEvents { [EventType(nameof(AppClientChanged))] + [Obsolete] public sealed class AppClientChanged : AppEvent, IMigratedEvent { public string Id { get; set; } diff --git a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs b/tools/Migrate_01/OldEvents/ContentArchived.cs similarity index 91% rename from src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs rename to tools/Migrate_01/OldEvents/ContentArchived.cs index 7402dd378..797342e21 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs +++ b/tools/Migrate_01/OldEvents/ContentArchived.cs @@ -8,10 +8,11 @@ using System; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Events.Contents.Old +namespace Migrate_01.OldEvents { [EventType(nameof(ContentArchived))] [Obsolete] diff --git a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs b/tools/Migrate_01/OldEvents/ContentPublished.cs similarity index 91% rename from src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs rename to tools/Migrate_01/OldEvents/ContentPublished.cs index 578eac373..4dd3f9c26 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs +++ b/tools/Migrate_01/OldEvents/ContentPublished.cs @@ -8,10 +8,11 @@ using System; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Events.Contents.Old +namespace Migrate_01.OldEvents { [EventType(nameof(ContentPublished))] [Obsolete] diff --git a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs b/tools/Migrate_01/OldEvents/ContentRestored.cs similarity index 91% rename from src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs rename to tools/Migrate_01/OldEvents/ContentRestored.cs index 5e1526eec..e67d9288e 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs +++ b/tools/Migrate_01/OldEvents/ContentRestored.cs @@ -8,10 +8,11 @@ using System; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Events.Contents.Old +namespace Migrate_01.OldEvents { [EventType(nameof(ContentRestored))] [Obsolete] diff --git a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs b/tools/Migrate_01/OldEvents/ContentUnpublished.cs similarity index 91% rename from src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs rename to tools/Migrate_01/OldEvents/ContentUnpublished.cs index 2908f8ee2..3d22e3dd9 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs +++ b/tools/Migrate_01/OldEvents/ContentUnpublished.cs @@ -8,10 +8,11 @@ using System; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Events.Contents.Old +namespace Migrate_01.OldEvents { [EventType(nameof(ContentUnpublished))] [Obsolete] diff --git a/src/Squidex.Domain.Apps.Events/Schemas/Old/WebhookAdded.cs b/tools/Migrate_01/OldEvents/WebhookAdded.cs similarity index 90% rename from src/Squidex.Domain.Apps.Events/Schemas/Old/WebhookAdded.cs rename to tools/Migrate_01/OldEvents/WebhookAdded.cs index f39b91618..8735e9ab4 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/Old/WebhookAdded.cs +++ b/tools/Migrate_01/OldEvents/WebhookAdded.cs @@ -7,9 +7,10 @@ // ========================================================================== using System; +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas.Old +namespace Migrate_01.OldEvents { [EventType(nameof(WebhookAdded))] [Obsolete] diff --git a/src/Squidex.Domain.Apps.Events/Schemas/Old/WebhookDeleted.cs b/tools/Migrate_01/OldEvents/WebhookDeleted.cs similarity index 89% rename from src/Squidex.Domain.Apps.Events/Schemas/Old/WebhookDeleted.cs rename to tools/Migrate_01/OldEvents/WebhookDeleted.cs index c579d224a..3615e36ce 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/Old/WebhookDeleted.cs +++ b/tools/Migrate_01/OldEvents/WebhookDeleted.cs @@ -7,9 +7,10 @@ // ========================================================================== using System; +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas.Old +namespace Migrate_01.OldEvents { [EventType(nameof(WebhookDeleted))] [Obsolete] From fc703426541d761898ae7626d3351b52deca9ec3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 12 Dec 2017 00:01:33 +0100 Subject: [PATCH 23/30] Migrator fixed. --- .../Contents/MongoContentRepository.cs | 4 +- .../Config/Domain/SerializationServices.cs | 27 +++---- tools/Migrate_01/Migration01.cs | 81 ++++++++++--------- 3 files changed, 57 insertions(+), 55 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 05ea8586e..6643fac7a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var documentId = $"{key}_{newVersion}"; - var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId); + var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId, true); if (schema == null) { @@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (contentEntity != null) { - var schema = await appProvider.GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId); + var schema = await appProvider.GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId, true); if (schema == null) { diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index fe051bd44..bbbd00046 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -33,15 +33,10 @@ namespace Squidex.Config.Domain .MapUnmapped(typeof(SquidexCoreModel).Assembly) .MapUnmapped(typeof(SquidexEvents).Assembly) .MapUnmapped(typeof(SquidexInfrastructure).Assembly); - private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry); - public static JsonSerializerSettings DefaultJsonSettings - { - get { return SerializerSettings; } - } + private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry); - private static void ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) + private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) { settings.SerializationBinder = new TypeNameSerializationBinder(TypeNameRegistry); @@ -70,21 +65,21 @@ namespace Squidex.Config.Domain settings.TypeNameHandling = typeNameHandling; settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - } - static SerializationServices() - { - ConfigureJson(SerializerSettings, TypeNameHandling.Auto); - - BsonJsonConvention.Register(JsonSerializer.Create(SerializerSettings)); + return settings; } public static IServiceCollection AddMySerializers(this IServiceCollection services) { - services.AddSingletonAs(t => TypeNameRegistry); + var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); + var serializerInstance = JsonSerializer.Create(serializerSettings); + services.AddSingletonAs(t => FieldRegistry); - services.AddSingletonAs(t => SerializerSettings); - services.AddSingletonAs(t => JsonSerializer.Create(SerializerSettings)); + services.AddSingletonAs(t => serializerSettings); + services.AddSingletonAs(t => serializerInstance); + services.AddSingletonAs(t => TypeNameRegistry); + + BsonJsonConvention.Register(serializerInstance); return services; } diff --git a/tools/Migrate_01/Migration01.cs b/tools/Migrate_01/Migration01.cs index e2b1d6d0d..7bc699334 100644 --- a/tools/Migrate_01/Migration01.cs +++ b/tools/Migrate_01/Migration01.cs @@ -7,8 +7,8 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; -using System.Timers; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; @@ -31,8 +31,8 @@ namespace Migrate_01 private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; private readonly IStateFactory stateFactory; - private readonly Timer timer = new Timer { AutoReset = false, Interval = 5000 }; - private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + private readonly Timer timer; + private readonly TaskCompletionSource subscriptionTcs = new TaskCompletionSource(); public int FromVersion { get; } = 0; @@ -49,68 +49,75 @@ namespace Migrate_01 this.eventStore = eventStore; this.stateFactory = stateFactory; - timer.Elapsed += (sender, e) => - { - tcs.TrySetResult(true); - }; + timer = new Timer(d => subscriptionTcs.TrySetResult(true)); } public async Task UpdateAsync() { var subscription = eventStore.CreateSubscription(this, ".*"); - await tcs.Task; + await subscriptionTcs.Task; await subscription.StopAsync(); } public async Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) { - var @event = ParseKnownEvent(storedEvent); - - if (@event != null) + try { - var version = storedEvent.EventStreamNumber; + timer.Change(Timeout.Infinite, Timeout.Infinite); + + var @event = ParseKnownEvent(storedEvent); - if (@event.Payload is AssetEvent assetEvent) + if (@event != null) { - var asset = await stateFactory.CreateAsync(assetEvent.AssetId); + var version = storedEvent.EventStreamNumber; - asset.UpdateState(asset.State.Apply(@event)); + if (@event.Payload is AssetEvent assetEvent) + { + var asset = await stateFactory.CreateAsync(assetEvent.AssetId); - await asset.WriteStateAsync(version); - } - else if (@event.Payload is ContentEvent contentEvent) - { - var content = await stateFactory.CreateAsync(contentEvent.ContentId); + asset.UpdateState(asset.State.Apply(@event)); - content.UpdateState(content.State.Apply(@event)); + await asset.WriteStateAsync(version); + } + else if (@event.Payload is ContentEvent contentEvent) + { + var content = await stateFactory.CreateAsync(contentEvent.ContentId); - await content.WriteStateAsync(version); - } - else if (@event.Payload is SchemaEvent schemaEvent) - { - var schema = await stateFactory.GetSingleAsync(schemaEvent.SchemaId.Id); + content.UpdateState(content.State.Apply(@event)); - schema.UpdateState(schema.State.Apply(@event, fieldRegistry)); + await content.WriteStateAsync(version); + } + else if (@event.Payload is SchemaEvent schemaEvent) + { + var schema = await stateFactory.GetSingleAsync(schemaEvent.SchemaId.Id); - await schema.WriteStateAsync(version); - } - else if (@event.Payload is AppEvent appEvent) - { - var app = await stateFactory.GetSingleAsync(appEvent.AppId.Id); + schema.UpdateState(schema.State.Apply(@event, fieldRegistry)); - app.UpdateState(app.State.Apply(@event)); + await schema.WriteStateAsync(version); + } + else if (@event.Payload is AppEvent appEvent) + { + var app = await stateFactory.GetSingleAsync(appEvent.AppId.Id); - await app.WriteStateAsync(version); + app.UpdateState(app.State.Apply(@event)); + + await app.WriteStateAsync(version); + } } - } - timer.Stop(); - timer.Start(); + timer.Change(5000, 0); + } + catch (Exception ex) + { + subscriptionTcs.SetException(ex); + } } public Task OnErrorAsync(IEventSubscription subscription, Exception exception) { + subscriptionTcs.TrySetException(exception); + return TaskHelper.Done; } From 5e82c66e7397dc454505a0b4b012faf7040b76b3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 12 Dec 2017 00:06:07 +0100 Subject: [PATCH 24/30] Sealed It --- src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs | 2 +- src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs | 2 +- .../Contents/ContentDomainObject.cs | 2 +- src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs | 2 +- .../Schemas/SchemaDomainObject.cs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index fc8ddec2e..d11519917 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppDomainObject : DomainObjectBase + public sealed class AppDomainObject : DomainObjectBase { public AppDomainObject Create(CreateApp command) { diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index b9bd87fb4..ada2fab99 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetDomainObject : DomainObjectBase + public sealed class AssetDomainObject : DomainObjectBase { public AssetDomainObject Create(CreateAsset command) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index fe4114a0e..21eabbe51 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentDomainObject : DomainObjectBase + public sealed class ContentDomainObject : DomainObjectBase { public ContentDomainObject Create(CreateContent command) { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index 7ce3be03d..8b7a94918 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDomainObject : DomainObjectBase + public sealed class RuleDomainObject : DomainObjectBase { public void Create(CreateRule command) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index 15aa08588..ec377f593 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas { - public class SchemaDomainObject : DomainObjectBase + public sealed class SchemaDomainObject : DomainObjectBase { private readonly FieldRegistry registry; @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas return this; } - protected void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event) + private void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event) { SimpleMapper.Map(fieldCommand, @event); From 37cd269e87ef7bb9557013a488883148a1ee3890 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 12 Dec 2017 20:46:43 +0100 Subject: [PATCH 25/30] 1) Cache removed 2) Angular updated 3) Migration fixed. --- src/Squidex/app/framework/declarations.ts | 1 - src/Squidex/app/framework/module.ts | 2 - .../services/local-cache.service.spec.ts | 64 ------------------- .../framework/services/local-cache.service.ts | 61 ------------------ .../shared/services/assets.service.spec.ts | 25 -------- .../app/shared/services/assets.service.ts | 21 +----- .../shared/services/contents.service.spec.ts | 43 +------------ .../app/shared/services/contents.service.ts | 23 +------ .../shared/services/schemas.service.spec.ts | 25 -------- .../app/shared/services/schemas.service.ts | 23 +------ src/Squidex/package.json | 42 ++++++------ tools/Migrate_01/Migration01.cs | 10 ++- 12 files changed, 38 insertions(+), 302 deletions(-) delete mode 100644 src/Squidex/app/framework/services/local-cache.service.spec.ts delete mode 100644 src/Squidex/app/framework/services/local-cache.service.ts diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 51fcb48ac..a6ebdcd8c 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -56,7 +56,6 @@ export * from './services/analytics.service'; export * from './services/clipboard.service'; export * from './services/dialog.service'; export * from './services/local-store.service'; -export * from './services/local-cache.service'; export * from './services/message-bus.service'; export * from './services/onboarding.service'; export * from './services/resource-loader.service'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 1edc0e5a5..ad1a9b076 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -39,7 +39,6 @@ import { JsonEditorComponent, KeysPipe, KNumberPipe, - LocalCacheService, LocalStoreService, LowerCaseInputDirective, MarkdownEditorComponent, @@ -197,7 +196,6 @@ export class SqxFrameworkModule { CanDeactivateGuard, ClipboardService, DialogService, - LocalCacheService, LocalStoreService, MessageBus, OnboardingService, diff --git a/src/Squidex/app/framework/services/local-cache.service.spec.ts b/src/Squidex/app/framework/services/local-cache.service.spec.ts deleted file mode 100644 index f2380bf6d..000000000 --- a/src/Squidex/app/framework/services/local-cache.service.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { LocalCacheService, LocalCacheServiceFactory } from './../'; - -describe('LocalCache', () => { - it('should instantiate from factory', () => { - const localCacheService = LocalCacheServiceFactory(); - - expect(localCacheService).toBeDefined(); - }); - - it('should instantiate', () => { - const localCacheService = new LocalCacheService(); - - expect(localCacheService).toBeDefined(); - }); - - it('should get and store item in cache', () => { - const localCacheService = new LocalCacheService(); - - const value = {}; - - localCacheService.set('key', value); - - expect(localCacheService.get('key')).toBe(value); - }); - - it('should not retrieve item if cleared', () => { - const localCacheService = new LocalCacheService(); - - const value = {}; - - localCacheService.set('key', value); - localCacheService.clear(true); - - expect(localCacheService.get('key')).toBeUndefined(); - }); - - it('should not retrieve item if removed', () => { - const localCacheService = new LocalCacheService(); - - const value = {}; - - localCacheService.set('key', value); - localCacheService.remove('key'); - - expect(localCacheService.get('key')).toBeUndefined(); - }); - - it('should not retrieve item if expired', () => { - const localCacheService = new LocalCacheService(); - - const value = {}; - - localCacheService.set('key', value); - - expect(localCacheService.get('key', new Date().getTime() + 400)).toBeUndefined(); - }); -}); \ No newline at end of file diff --git a/src/Squidex/app/framework/services/local-cache.service.ts b/src/Squidex/app/framework/services/local-cache.service.ts deleted file mode 100644 index 565115f5a..000000000 --- a/src/Squidex/app/framework/services/local-cache.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Injectable } from '@angular/core'; - -interface Entry { value: any; expires: number; } - -export const LocalCacheServiceFactory = () => { - return new LocalCacheService(); -}; - -@Injectable() -export class LocalCacheService { - private readonly entries: { [key: string]: Entry } = {}; - - public clear(force: boolean) { - const now = new Date().getTime(); - - for (let key in this.entries) { - if (this.entries.hasOwnProperty(key)) { - const entry = this.entries[key]; - - if (force || LocalCacheService.isExpired(now, entry)) { - delete this.entries[key]; - } - } - } - } - - public get(key: string, now?: number): T | undefined { - const entry = this.entries[key]; - - if (entry) { - now = now || new Date().getTime(); - - if (!LocalCacheService.isExpired(now, entry)) { - delete this.entries[key]; - - return entry.value; - } - } - - return undefined; - } - - public set(key: string, value: T, expiresIn = 100) { - this.entries[key] = { value, expires: new Date().getTime() + expiresIn }; - } - - public remove(key: string) { - delete this.entries[key]; - } - - private static isExpired(now: number, entry: Entry): boolean { - return entry.expires < now; - } -} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 25a6bc090..0fdb61ed0 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -16,7 +16,6 @@ import { AssetReplacedDto, AssetsService, DateTime, - LocalCacheService, UpdateAssetDto, Version, Versioned @@ -70,7 +69,6 @@ describe('AssetsService', () => { ], providers: [ AssetsService, - LocalCacheService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: AnalyticsService, useValue: new AnalyticsService() } ] @@ -216,29 +214,6 @@ describe('AssetsService', () => { new Version('2'))); })); - it('should provide entry from cache if not found', - inject([LocalCacheService, AssetsService, HttpTestingController], (localCache: LocalCacheService, assetsService: AssetsService, httpMock: HttpTestingController) => { - - const cached = {}; - - localCache.set('asset.123', cached, 10000); - - let asset: AssetDto | null = null; - - assetsService.getAsset('my-app', '123').subscribe(result => { - asset = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123'); - - expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush({}, { status: 404, statusText: '404' }); - - expect(asset).toBe(cached); - })); - it('should append query to find by name', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 5bf2850a7..ce2c3071c 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { HttpClient, HttpErrorResponse, HttpEventType, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpEventType, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @@ -13,7 +13,6 @@ import { AnalyticsService, ApiUrlConfig, DateTime, - LocalCacheService, HTTP, Version, Versioned @@ -106,8 +105,7 @@ export class AssetsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, - private readonly localCache: LocalCacheService + private readonly analytics: AnalyticsService ) { } @@ -202,8 +200,6 @@ export class AssetsService { assetUrl, new Version(event.headers.get('etag'))); - this.localCache.set(`asset.${dto.id}`, dto, 5000); - return dto; } }) @@ -239,17 +235,6 @@ export class AssetsService { assetUrl, response.version); }) - .catch(error => { - if (error instanceof HttpErrorResponse && error.status === 404) { - const cached = this.localCache.get(`asset.${id}`); - - if (cached) { - return Observable.of(cached); - } - } - - return Observable.throw(error); - }) .pretifyError('Failed to load assets. Please reload.'); } @@ -298,8 +283,6 @@ export class AssetsService { return HTTP.deleteVersioned(this.http, url, version) .do(() => { this.analytics.trackEvent('Analytics', 'Deleted', appName); - - this.localCache.remove(`asset.${id}`); }) .pretifyError('Failed to delete asset. Please reload.'); } diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index bc0569cd6..e8ba7abdc 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -15,7 +15,6 @@ import { ContentsDto, ContentsService, DateTime, - LocalCacheService, Version } from './../'; @@ -97,7 +96,6 @@ describe('ContentsService', () => { ], providers: [ ContentsService, - LocalCacheService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: AnalyticsService, useValue: new AnalyticsService() } ] @@ -166,11 +164,7 @@ describe('ContentsService', () => { it('should append query to get request as search', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - let contents: ContentsDto | null = null; - - contentsService.getContents('my-app', 'my-schema', 17, 13, 'my-query').subscribe(result => { - contents = result; - }); + contentsService.getContents('my-app', 'my-schema', 17, 13, 'my-query').subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$search="my-query"&$top=17&$skip=13'); @@ -183,11 +177,7 @@ describe('ContentsService', () => { it('should append ids to get request with ids', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - let contents: ContentsDto | null = null; - - contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, ['id1', 'id2']).subscribe(result => { - contents = result; - }); + contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, ['id1', 'id2']).subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$top=17&$skip=13&ids=id1,id2'); @@ -200,11 +190,7 @@ describe('ContentsService', () => { it('should append query to get request as plain query string', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - let contents: ContentsDto | null = null; - - contentsService.getContents('my-app', 'my-schema', 17, 13, '$filter=my-filter').subscribe(result => { - contents = result; - }); + contentsService.getContents('my-app', 'my-schema', 17, 13, '$filter=my-filter').subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$filter=my-filter&$top=17&$skip=13'); @@ -250,29 +236,6 @@ describe('ContentsService', () => { new Version('2'))); })); - it('should provide entry from cache if not found', - inject([LocalCacheService, ContentsService, HttpTestingController], (localCache: LocalCacheService, contentsService: ContentsService, httpMock: HttpTestingController) => { - - const cached = {}; - - localCache.set('content.1', cached, 10000); - - let content: ContentDto | null = null; - - contentsService.getContent('my-app', 'my-schema', '1').subscribe(result => { - content = result; - }); - - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1'); - - expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush({}, { status: 404, statusText: '404' }); - - expect(content).toBe(cached); - })); - it('should make post request to create content', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index d0bedab87..bc7706892 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @@ -15,7 +15,6 @@ import { AnalyticsService, ApiUrlConfig, DateTime, - LocalCacheService, HTTP, Version, Versioned @@ -96,8 +95,7 @@ export class ContentsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, - private readonly localCache: LocalCacheService + private readonly analytics: AnalyticsService ) { } @@ -172,17 +170,6 @@ export class ContentsService { body.data, response.version); }) - .catch(error => { - if (error instanceof HttpErrorResponse && error.status === 404) { - const cached = this.localCache.get(`content.${id}`); - - if (cached) { - return Observable.of(cached); - } - } - - return Observable.throw(error); - }) .pretifyError('Failed to load content. Please reload.'); } @@ -215,8 +202,6 @@ export class ContentsService { }) .do(content => { this.analytics.trackEvent('Content', 'Created', appName); - - this.localCache.set(`content.${content.id}`, content, 5000); }) .pretifyError('Failed to create content. Please reload.'); } @@ -232,8 +217,6 @@ export class ContentsService { }) .do(() => { this.analytics.trackEvent('Content', 'Updated', appName); - - this.localCache.set(`content.${id}`, dto, 5000); }) .pretifyError('Failed to update content. Please reload.'); } @@ -244,8 +227,6 @@ export class ContentsService { return HTTP.deleteVersioned(this.http, url, version) .do(() => { this.analytics.trackEvent('Content', 'Deleted', appName); - - this.localCache.remove(`content.${id}`); }) .pretifyError('Failed to delete content. Please reload.'); } diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index 2f8ec096e..35cd45ae8 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -16,7 +16,6 @@ import { createProperties, DateTime, FieldDto, - LocalCacheService, SchemaDetailsDto, SchemaDto, SchemaPropertiesDto, @@ -197,7 +196,6 @@ describe('SchemasService', () => { HttpClientTestingModule ], providers: [ - LocalCacheService, SchemasService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: AnalyticsService, useValue: new AnalyticsService() } @@ -433,29 +431,6 @@ describe('SchemasService', () => { '')); })); - it('should provide entry from cache if not found', - inject([LocalCacheService, SchemasService, HttpTestingController], (localCache: LocalCacheService, schemasService: SchemasService, httpMock: HttpTestingController) => { - - const cached = {}; - - localCache.set('schema.my-app.my-schema', cached, 10000); - - let schema: SchemaDetailsDto | null = null; - - schemasService.getSchema('my-app', 'my-schema').subscribe(result => { - schema = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema'); - - expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush({}, { status: 404, statusText: '404' }); - - expect(schema).toBe(cached); - })); - it('should make post request to create schema', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index c5904552d..440b11b18 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ValidatorFn, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; @@ -16,7 +16,6 @@ import { AnalyticsService, ApiUrlConfig, DateTime, - LocalCacheService, HTTP, ValidatorsEx, Version, @@ -786,8 +785,7 @@ export class SchemasService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, - private readonly localCache: LocalCacheService + private readonly analytics: AnalyticsService ) { } @@ -858,17 +856,6 @@ export class SchemasService { body.scriptDelete, body.scriptChange); }) - .catch(error => { - if (error instanceof HttpErrorResponse && error.status === 404) { - const cached = this.localCache.get(`schema.${appName}.${id}`); - - if (cached) { - return Observable.of(cached); - } - } - - return Observable.throw(error); - }) .pretifyError('Failed to load schema. Please reload.'); } @@ -900,9 +887,6 @@ export class SchemasService { }) .do(schema => { this.analytics.trackEvent('Schema', 'Created', appName); - - this.localCache.set(`schema.${appName}.${schema.id}`, schema, 5000); - this.localCache.set(`schema.${appName}.${schema.name}`, schema, 5000); }) .pretifyError('Failed to create schema. Please reload.'); } @@ -935,9 +919,6 @@ export class SchemasService { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); return HTTP.deleteVersioned(this.http, url, version) - .do(() => { - this.localCache.remove(`schema.${appName}.${schemaName}`); - }) .do(() => { this.analytics.trackEvent('Schema', 'Deleted', appName); }) diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 8ea7b3e2b..7e6427fb0 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -15,36 +15,36 @@ "build:clean": "rimraf wwwroot/build" }, "dependencies": { - "@angular/animations": "5.0.5", - "@angular/common": "5.0.5", - "@angular/compiler": "5.0.5", - "@angular/core": "5.0.5", - "@angular/forms": "5.0.5", - "@angular/http": "5.0.5", - "@angular/platform-browser": "5.0.5", - "@angular/platform-browser-dynamic": "5.0.5", - "@angular/platform-server": "5.0.5", - "@angular/router": "5.0.5", + "@angular/animations": "5.1.0", + "@angular/common": "5.1.0", + "@angular/compiler": "5.1.0", + "@angular/core": "5.1.0", + "@angular/forms": "5.1.0", + "@angular/http": "5.1.0", + "@angular/platform-browser": "5.1.0", + "@angular/platform-browser-dynamic": "5.1.0", + "@angular/platform-server": "5.1.0", + "@angular/router": "5.1.0", "angular2-chartjs": "0.4.1", "babel-polyfill": "6.26.0", "bootstrap": "4.0.0-alpha.6", - "core-js": "2.5.1", + "core-js": "2.5.3", "graphiql": "0.11.10", - "moment": "2.19.3", + "moment": "2.19.4", "mousetrap": "1.6.1", "ng2-dnd": "5.0.2", "oidc-client": "1.4.1", - "pikaday": "1.6.1", + "pikaday": "1.7.0", "progressbar.js": "1.0.1", "react": "16.2.0", "react-dom": "16.2.0", - "rxjs": "5.5.2", + "rxjs": "5.5.5", "zone.js": "0.8.18" }, "devDependencies": { - "@angular/compiler": "5.0.5", - "@angular/compiler-cli": "5.0.5", - "@ngtools/webpack": "1.8.5", + "@angular/compiler": "5.1.0", + "@angular/compiler-cli": "5.1.0", + "@ngtools/webpack": "1.9.0", "@types/core-js": "0.9.35", "@types/jasmine": "2.5.45", "@types/mousetrap": "1.5.34", @@ -54,7 +54,7 @@ "angular2-router-loader": "0.3.5", "angular2-template-loader": "0.6.2", "awesome-typescript-loader": "3.4.1", - "codelyzer": "4.0.1", + "codelyzer": "4.0.2", "cpx": "1.5.0", "css-loader": "0.28.7", "exports-loader": "0.6.4", @@ -88,10 +88,10 @@ "tslint": "5.8.0", "tslint-loader": "3.5.3", "typemoq": "2.1.0", - "typescript": "2.4.2", + "typescript": "2.5.3", "underscore": "1.8.3", - "webpack": "3.9.1", - "webpack-dev-server": "2.9.5", + "webpack": "3.10.0", + "webpack-dev-server": "2.9.7", "webpack-merge": "4.1.1" } } diff --git a/tools/Migrate_01/Migration01.cs b/tools/Migrate_01/Migration01.cs index 7bc699334..c0a8c9e2e 100644 --- a/tools/Migrate_01/Migration01.cs +++ b/tools/Migrate_01/Migration01.cs @@ -56,8 +56,14 @@ namespace Migrate_01 { var subscription = eventStore.CreateSubscription(this, ".*"); - await subscriptionTcs.Task; - await subscription.StopAsync(); + try + { + await subscriptionTcs.Task; + } + finally + { + await subscription.StopAsync(); + } } public async Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) From 48081744647603e7587cbfc2cd277ecac6b934a4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 12 Dec 2017 20:58:58 +0100 Subject: [PATCH 26/30] .NET Packages updated. --- .../Squidex.Domain.Apps.Core.Operations.csproj | 2 +- .../Squidex.Domain.Users.MongoDb.csproj | 4 ++-- src/Squidex.Domain.Users/Squidex.Domain.Users.csproj | 2 +- src/Squidex/Squidex.csproj | 10 +++++----- .../Squidex.Domain.Apps.Core.Tests.csproj | 2 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 2 +- .../Squidex.Domain.Users.Tests.csproj | 2 +- .../Squidex.Infrastructure.Tests.csproj | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index bd76d3726..ac30c2d2e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index d9b6e64e1..d61c984d0 100644 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -13,13 +13,13 @@ - + - + ..\..\Squidex.ruleset diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 987174138..d0090e18f 100644 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -16,7 +16,7 @@ - + ..\..\Squidex.ruleset diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 2d5357db2..f60bfd81e 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -49,7 +49,7 @@ - + @@ -61,19 +61,19 @@ - + - + - + - + diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 7b62d9673..e1188014a 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index f89e8cc4f..84be716c4 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index 743ad2bf9..957bb6b03 100644 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 50a236ab7..87a70e941 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -11,7 +11,7 @@ - + From bab2d1d6c68b222190ead170e662c86e069f36b0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 12 Dec 2017 22:46:38 +0100 Subject: [PATCH 27/30] Invalidate cache when writing failed. --- .../States/Persistence.cs | 3 +- .../States/Persistence{TOwner,TState,TKey}.cs | 85 ++++++++++++------- .../States/StateFactory.cs | 12 ++- src/Squidex.Infrastructure/States/Store.cs | 9 +- .../States/StateEventSourcingTests.cs | 48 ++++++----- .../States/StateSnapshotTests.cs | 33 +++---- 6 files changed, 112 insertions(+), 78 deletions(-) diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 7a6738d92..a306e3327 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -18,12 +18,13 @@ namespace Squidex.Infrastructure.States { public Persistence(TKey ownerKey, Action invalidate, + Action failed, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, Func, Task> applyEvent) - : base(ownerKey, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) + : base(ownerKey, invalidate, failed, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) { } } diff --git a/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs index ccc80d3aa..112b06099 100644 --- a/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs +++ b/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs @@ -25,6 +25,7 @@ namespace Squidex.Infrastructure.States private readonly IEventDataFormatter eventDataFormatter; private readonly PersistenceMode persistenceMode; private readonly Action invalidate; + private readonly Action failed; private readonly Func applyState; private readonly Func, Task> applyEvent; private long versionSnapshot = EtagVersion.Empty; @@ -38,6 +39,7 @@ namespace Squidex.Infrastructure.States public Persistence(TKey ownerKey, Action invalidate, + Action failed, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, @@ -49,9 +51,10 @@ namespace Squidex.Infrastructure.States this.ownerKey = ownerKey; this.applyState = applyState; this.applyEvent = applyEvent; - this.invalidate = invalidate; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; + this.invalidate = invalidate; + this.failed = failed; this.persistenceMode = persistenceMode; this.snapshotStore = snapshotStore; this.streamNameResolver = streamNameResolver; @@ -128,57 +131,75 @@ namespace Squidex.Infrastructure.States public async Task WriteSnapshotAsync(TState state) { - var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - - if (newVersion != versionSnapshot) + try { - try - { - await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); - } - catch (InconsistentStateException ex) + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; + + if (newVersion != versionSnapshot) { - throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + try + { + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); + } + catch (InconsistentStateException ex) + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + } + + versionSnapshot = newVersion; } - versionSnapshot = newVersion; - } + UpdateVersion(); - UpdateVersion(); + invalidate?.Invoke(); + } + catch + { + failed?.Invoke(); - invalidate?.Invoke(); + throw; + } } public async Task WriteEventsAsync(IEnumerable> events) { Guard.NotNull(events, nameof(@events)); - var eventArray = events.ToArray(); - - if (eventArray.Length > 0) + try { - var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; + var eventArray = events.ToArray(); - var commitId = Guid.NewGuid(); + if (eventArray.Length > 0) + { + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; - var eventStream = GetStreamName(); - var eventData = GetEventData(eventArray, commitId); + var commitId = Guid.NewGuid(); - try - { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); - } - catch (WrongEventVersionException ex) - { - throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + var eventStream = GetStreamName(); + var eventData = GetEventData(eventArray, commitId); + + try + { + await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); + } + catch (WrongEventVersionException ex) + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + } + + versionEvents += eventArray.Length; } - versionEvents += eventArray.Length; - } + UpdateVersion(); - UpdateVersion(); + invalidate?.Invoke(); + } + catch + { + failed?.Invoke(); - invalidate?.Invoke(); + throw; + } } private EventData[] GetEventData(Envelope[] events, Guid commitId) diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index 9ac202d72..abeb95d8a 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -126,10 +126,14 @@ namespace Squidex.Infrastructure.States var state = (T)services.GetService(typeof(T)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => - { - pubSub.Publish(new InvalidateMessage { Key = key.ToString() }, false); - }); + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, + () => + { + pubSub.Publish(new InvalidateMessage { Key = key.ToString() }, false); + }, () => + { + statesCache.Remove(key); + }); stateObj = new ObjectHolder(state, key, stateStore); diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index e200a9257..714fc3472 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -15,6 +15,7 @@ namespace Squidex.Infrastructure.States internal sealed class Store : IStore { private readonly Action invalidate; + private readonly Action failed; private readonly IServiceProvider services; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; @@ -25,10 +26,12 @@ namespace Squidex.Infrastructure.States IEventDataFormatter eventDataFormatter, IServiceProvider services, IStreamNameResolver streamNameResolver, - Action invalidate = null) + Action invalidate = null, + Action failed = null) { this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; + this.failed = failed; this.invalidate = invalidate; this.services = services; this.streamNameResolver = streamNameResolver; @@ -50,7 +53,7 @@ namespace Squidex.Infrastructure.States var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); + return new Persistence(key, invalidate, failed, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); } private IPersistence CreatePersistence(TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) @@ -59,7 +62,7 @@ namespace Squidex.Infrastructure.States var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + return new Persistence(key, invalidate, failed, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } } } diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index eae17945f..ac1581d2e 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.States private readonly List appliedEvents = new List(); private IPersistence persistence; - public long ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } = EtagVersion.Any; public List AppliedEvents { @@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.States { private IPersistence persistence; - public long ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } = EtagVersion.Any; public Task ActivateAsync(string key, IStore store) { @@ -110,10 +110,8 @@ namespace Squidex.Infrastructure.States } [Fact] - public async Task Should_read_events_from_snapshot() + public async Task Should_read_status_from_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -128,8 +126,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -141,8 +137,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -171,6 +165,19 @@ namespace Squidex.Infrastructure.States await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } + [Fact] + public async Task Should_throw_exception_if_other_version_found_from_snapshot() + { + statefulObjectWithSnapShot.ExpectedVersion = 1; + + A.CallTo(() => snapshotStore.ReadAsync(key)) + .Returns((2, 2L)); + + SetupEventStore(0); + + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + } + [Fact] public async Task Should_not_throw_exception_if_noting_expected() { @@ -197,8 +204,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = EtagVersion.Any; - SetupEventStore(0); var actualObject1 = await sut.GetSingleAsync(key); @@ -215,8 +220,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_position() { - statefulObject.ExpectedVersion = EtagVersion.Any; - InvalidateMessage message = null; pubSub.Subscribe(m => @@ -245,8 +248,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() { - statefulObject.ExpectedVersion = EtagVersion.Any; - SetupEventStore(3); var actualObject = await sut.GetSingleAsync(key); @@ -260,8 +261,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -270,10 +269,21 @@ namespace Squidex.Infrastructure.States } [Fact] - public async Task Should_return_same_instance_for_parallel_requests() + public async Task Should_remove_from_cache_when_write_failed() { - statefulObject.ExpectedVersion = EtagVersion.Any; + A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, A.Ignored, A.Ignored, A>.Ignored)) + .Throws(new InvalidOperationException()); + + var actualObject = await sut.GetSingleAsync(key); + + await Assert.ThrowsAsync(() => statefulObject.WriteEventsAsync(new MyEvent())); + + Assert.False(cache.TryGetValue(key, out var t)); + } + [Fact] + public async Task Should_return_same_instance_for_parallel_requests() + { A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index f003c6ef6..1a84194a0 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.States private IPersistence persistence; private int state; - public long ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } = EtagVersion.Any; public long Version { @@ -102,8 +102,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_set_to_empty_when_store_returns_not_found() { - statefulObject.ExpectedVersion = EtagVersion.Any; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, EtagVersion.NotFound)); @@ -138,8 +136,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = EtagVersion.Any; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); @@ -149,8 +145,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); @@ -160,8 +154,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); @@ -176,8 +168,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_serve_next_request_from_cache_when_detached() { - statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject1 = await sut.CreateAsync(key); Assert.Same(statefulObject, actualObject1); @@ -192,8 +182,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_version() { - statefulObject.ExpectedVersion = EtagVersion.Any; - InvalidateMessage message = null; pubSub.Subscribe(m => @@ -223,8 +211,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { - statefulObject.ExpectedVersion = EtagVersion.Any; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); @@ -239,8 +225,6 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -249,10 +233,21 @@ namespace Squidex.Infrastructure.States } [Fact] - public async Task Should_return_same_instance_for_parallel_requests() + public async Task Should_remove_from_cache_when_write_failed() { - statefulObject.ExpectedVersion = EtagVersion.Any; + A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + .Throws(new InvalidOperationException()); + var actualObject = await sut.GetSingleAsync(key); + + await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); + + Assert.False(cache.TryGetValue(key, out var t)); + } + + [Fact] + public async Task Should_return_same_instance_for_parallel_requests() + { A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); From c93f27b98dd89961f562a3dc11750bfa2588322b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 13 Dec 2017 07:51:59 +0100 Subject: [PATCH 28/30] Fixed the patch endpoint. --- .../Contents/ContentDomainObject.cs | 2 +- .../Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 21eabbe51..493064167 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { VerifyCreatedAndNotDeleted(); - var newData = State.Data.MergeInto(command.Data); + var newData = command.Data.MergeInto(State.Data); if (!newData.Equals(State.Data)) { diff --git a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs index 88d7500f9..e19f0a095 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs @@ -174,7 +174,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator operation.Summary = $"Patchs a {schemaName} content."; operation.Security = EditorSecurity; - operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription); + operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddResponse("201", $"{schemaName} item patched.", dataSchema); }); From d2f3f74db173a7ad5e2fecc02021b09fc589597d Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 13 Dec 2017 21:47:22 +0100 Subject: [PATCH 29/30] Fix in AppProvider. --- src/Squidex.Domain.Apps.Entities/AppProvider.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 55c030912..e6772c77b 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -63,6 +63,11 @@ namespace Squidex.Domain.Apps.Entities { var appId = await GetAppIdAsync(appName); + if (appId == Guid.Empty) + { + return null; + } + var app = await stateFactory.GetSingleAsync(appId); return IsNotFound(app) ? null : app.State; @@ -72,6 +77,11 @@ namespace Squidex.Domain.Apps.Entities { var schemaId = await GetSchemaIdAsync(appId, name); + if (schemaId == Guid.Empty) + { + return null; + } + var schema = await stateFactory.GetSingleAsync(schemaId); return IsNotFound(provideDeleted, schema) ? null : schema.State; From 6f090a41514df37022f2b6644da2b0bb4e931a1d Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 14 Dec 2017 20:03:31 +0100 Subject: [PATCH 30/30] Merging fixed. --- .../Contents/ContentData.cs | 10 +++++----- .../Contents/IdContentData.cs | 7 ++++++- .../Contents/NamedContentData.cs | 7 ++++++- .../Model/Contents/ContentDataTests.cs | 10 ++++++++-- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs index 548a88ae3..aa1fece95 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -31,15 +31,15 @@ namespace Squidex.Domain.Apps.Core.Contents { } - protected static TResult Merge(TResult target, TResult source1, TResult source2) where TResult : ContentData + protected static TResult MergeTo(TResult target, params TResult[] sources) where TResult : ContentData { - if (ReferenceEquals(source1, source2)) + Guard.NotEmpty(sources, nameof(sources)); + + if (sources.Length == 1 || sources.Skip(1).All(x => ReferenceEquals(x, sources[0]))) { - return source1; + return sources[0]; } - var sources = new[] { source1, source2 }; - foreach (var source in sources) { foreach (var otherValue in source) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs index e04e88310..20fbfe83f 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs @@ -24,9 +24,14 @@ namespace Squidex.Domain.Apps.Core.Contents { } + public static IdContentData Merge(params IdContentData[] contents) + { + return MergeTo(new IdContentData(), contents); + } + public IdContentData MergeInto(IdContentData target) { - return Merge(new IdContentData(), this, target); + return Merge(target, this); } public IdContentData ToCleaned() diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs index e6f452d66..7318c8214 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs @@ -24,9 +24,14 @@ namespace Squidex.Domain.Apps.Core.Contents { } + public static NamedContentData Merge(params NamedContentData[] contents) + { + return MergeTo(new NamedContentData(), contents); + } + public NamedContentData MergeInto(NamedContentData target) { - return Merge(new NamedContentData(), this, target); + return Merge(target, this); } public NamedContentData ToCleaned() diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs index f885789a3..0eeb4b842 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs @@ -86,12 +86,14 @@ namespace Squidex.Domain.Apps.Core.Model.Contents .AddValue("iv", 1)) .AddField("field2", new ContentFieldData() - .AddValue("de", 2)); + .AddValue("de", 2) + .AddValue("it", 2)); var rhs = new NamedContentData() .AddField("field2", new ContentFieldData() + .AddValue("it", 3) .AddValue("en", 3)) .AddField("field3", new ContentFieldData() @@ -104,6 +106,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents .AddValue("iv", 1)) .AddField("field2", new ContentFieldData() + .AddValue("it", 2) .AddValue("de", 2) .AddValue("en", 3)) .AddField("field3", @@ -127,12 +130,14 @@ namespace Squidex.Domain.Apps.Core.Model.Contents .AddValue("iv", 1)) .AddField(2, new ContentFieldData() - .AddValue("de", 2)); + .AddValue("de", 2) + .AddValue("it", 2)); var rhs = new IdContentData() .AddField(2, new ContentFieldData() + .AddValue("it", 3) .AddValue("en", 3)) .AddField(3, new ContentFieldData() @@ -145,6 +150,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents .AddValue("iv", 1)) .AddField(2, new ContentFieldData() + .AddValue("it", 2) .AddValue("de", 2) .AddValue("en", 3)) .AddField(3,