Browse Source

Domain objects moved.

pull/206/head
Sebastian Stehle 8 years ago
parent
commit
206566efa4
  1. 15
      Squidex.sln
  2. 21
      src/Squidex.Domain.Apps.Entities/AppAggregateCommand.cs
  3. 18
      src/Squidex.Domain.Apps.Entities/AppCommand.cs
  4. 173
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  5. 229
      src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  6. 20
      src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs
  7. 145
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  8. 17
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs
  9. 19
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
  10. 19
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs
  11. 17
      src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs
  12. 30
      src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
  13. 15
      src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs
  14. 17
      src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs
  15. 15
      src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs
  16. 21
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
  17. 24
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
  18. 65
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs
  19. 102
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  20. 82
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  21. 100
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  22. 25
      src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  23. 25
      src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs
  24. 22
      src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlanBillingManager.cs
  25. 27
      src/Squidex.Domain.Apps.Entities/Apps/Services/IAppPlansProvider.cs
  26. 14
      src/Squidex.Domain.Apps.Entities/Apps/Services/IChangePlanResult.cs
  27. 30
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs
  28. 86
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs
  29. 31
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs
  30. 19
      src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs
  31. 19
      src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs
  32. 25
      src/Squidex.Domain.Apps.Entities/Apps/Services/RedirectToCheckoutResult.cs
  33. 31
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  34. 117
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  35. 107
      src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  36. 23
      src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs
  37. 23
      src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs
  38. 25
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  39. 14
      src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
  40. 15
      src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs
  41. 19
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  42. 49
      src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  43. 25
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
  44. 16
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs
  45. 21
      src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs
  46. 23
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  47. 21
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs
  48. 55
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  49. 45
      src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
  50. 77
      src/Squidex.Domain.Apps.Entities/EntityMapper.cs
  51. 44
      src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs
  52. 66
      src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs
  53. 24
      src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs
  54. 21
      src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs
  55. 19
      src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs
  56. 34
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  57. 22
      src/Squidex.Domain.Apps.Entities/IEntity.cs
  58. 17
      src/Squidex.Domain.Apps.Entities/IEntityWithAppRef.cs
  59. 17
      src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs
  60. 17
      src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs
  61. 15
      src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs
  62. 17
      src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithAppRef.cs
  63. 17
      src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs
  64. 17
      src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs
  65. 15
      src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs
  66. 20
      src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs
  67. 14
      src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs
  68. 14
      src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs
  69. 14
      src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs
  70. 23
      src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleAggregateCommand.cs
  71. 19
      src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs
  72. 14
      src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs
  73. 107
      src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs
  74. 40
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs
  75. 56
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs
  76. 22
      src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs
  77. 29
      src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs
  78. 35
      src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs
  79. 92
      src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs
  80. 157
      src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs
  81. 93
      src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  82. 73
      src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  83. 18
      src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs
  84. 26
      src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
  85. 21
      src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs
  86. 18
      src/Squidex.Domain.Apps.Entities/SchemaCommand.cs
  87. 21
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs
  88. 23
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs
  89. 36
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs
  90. 27
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs
  91. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs
  92. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs
  93. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs
  94. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs
  95. 15
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs
  96. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs
  97. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs
  98. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs
  99. 17
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs
  100. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs

15
Squidex.sln

@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Op
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -344,6 +358,7 @@ Global
{7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892} {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892}
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}

21
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; }
}
}
}

18
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<Guid> AppId { get; set; }
}
}

173
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<AppDomainObject>(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<AppDomainObject>(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<AppDomainObject>(context, a =>
{
GuardAppContributors.CanRemove(a.State.Contributors, command);
a.RemoveContributor(command);
});
}
protected Task On(AttachClient command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
{
GuardAppClients.CanAttach(a.State.Clients, command);
a.AttachClient(command);
});
}
protected Task On(UpdateClient command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
{
GuardAppClients.CanUpdate(a.State.Clients, command);
a.UpdateClient(command);
});
}
protected Task On(RevokeClient command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
{
GuardAppClients.CanRevoke(a.State.Clients, command);
a.RevokeClient(command);
});
}
protected Task On(AddLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
{
GuardAppLanguages.CanAdd(a.State.LanguagesConfig, command);
a.AddLanguage(command);
});
}
protected Task On(RemoveLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
{
GuardAppLanguages.CanRemove(a.State.LanguagesConfig, command);
a.RemoveLanguage(command);
});
}
protected Task On(UpdateLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
{
GuardAppLanguages.CanUpdate(a.State.LanguagesConfig, command);
a.UpdateLanguage(command);
});
}
protected Task On(ChangePlan command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(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<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
}
}
}

229
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<AppDomainObject, AppState>
{
public AppDomainObject Create(CreateApp command)
{
ThrowIfCreated();
var appId = new NamedId<Guid>(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<LanguageConfig>().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<Guid>(State.Id, State.Name);
}
RaiseEvent(Envelope.Create(@event));
}
private static AppCreated CreateInitalEvent(NamedId<Guid> appId)
{
return new AppCreated { AppId = appId };
}
private static AppLanguageAdded CreateInitialLanguage(NamedId<Guid> id)
{
return new AppLanguageAdded { AppId = id, Language = Language.EN };
}
private static AppContributorAssigned CreateInitialOwner(NamedId<Guid> 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<AppClients, AppClients> updater)
{
UpdateState(command, s => s.Clients = updater(s.Clients));
}
private void UpdateContributors(ICommand command, Func<AppContributors, AppContributors> updater)
{
UpdateState(command, s => s.Contributors = updater(s.Contributors));
}
private void UpdateLanguages(ICommand command, Func<LanguagesConfig, LanguagesConfig> updater)
{
UpdateState(command, s => s.LanguagesConfig = updater(s.LanguagesConfig));
}
protected override AppState CloneState(ICommand command, Action<AppState> updater)
{
return State.Clone().Update((SquidexCommand)command, updater);
}
}
}

20
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();
}
}
}

145
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<AppContributorAssigned>(
"assigned {user:[Contributor]} as [Permission]");
AddEventMessage<AppContributorRemoved>(
"removed {user:[Contributor]} from app");
AddEventMessage<AppClientAttached>(
"added client {[Id]} to app");
AddEventMessage<AppClientRevoked>(
"revoked client {[Id]}");
AddEventMessage<AppClientUpdated>(
"updated client {[Id]}");
AddEventMessage<AppClientRenamed>(
"renamed client {[Id]} to {[Name]}");
AddEventMessage<AppLanguageAdded>(
"added language {[Language]}");
AddEventMessage<AppLanguageRemoved>(
"removed language {[Language]}");
AddEventMessage<AppLanguageUpdated>(
"updated language {[Language]}");
AddEventMessage<AppMasterLanguageSet>(
"changed master language to {[Language]}");
}
protected Task<HistoryEventToStore> On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
const string channel = "settings.contributors";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Contributor", @event.ContributorId));
}
protected Task<HistoryEventToStore> 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<HistoryEventToStore> On(AppClientAttached @event, EnvelopeHeaders headers)
{
const string channel = "settings.clients";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Id", @event.Id));
}
protected Task<HistoryEventToStore> On(AppClientRevoked @event, EnvelopeHeaders headers)
{
const string channel = "settings.clients";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Id", @event.Id));
}
protected Task<HistoryEventToStore> 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<HistoryEventToStore> On(AppLanguageAdded @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppLanguageRemoved @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppLanguageUpdated @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Language", @event.Language));
}
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @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;
}
}
}

17
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; }
}
}

19
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; }
}
}

19
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();
}
}

17
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; }
}
}

30
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();
}
}
}

15
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; }
}
}

17
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; }
}
}

15
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; }
}
}

21
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; }
}
}

24
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<Language> Fallback { get; set; }
}
}

65
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."));
}
});
}
}
}

102
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;
}
}
}

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

100
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;
}
}
}

25
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; }
}
}

25
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; }
}
}

22
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<IChangePlanResult> ChangePlanAsync(string userId, Guid appId, string appName, string planId);
Task<string> GetPortalLinkAsync(string userId);
}
}

27
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<IAppLimitsPlan> GetAvailablePlans();
bool IsConfiguredPlan(string planId);
IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app);
IAppLimitsPlan GetPlanUpgrade(string planId);
IAppLimitsPlan GetPlanForApp(IAppEntity app);
IAppLimitsPlan GetPlan(string planId);
}
}

14
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
{
}
}

30
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();
}
}
}

86
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<string, ConfigAppLimitsPlan> plansById;
private readonly List<ConfigAppLimitsPlan> plansList;
public ConfigAppPlansProvider(IEnumerable<ConfigAppLimitsPlan> 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<IAppLimitsPlan> 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;
}
}
}

31
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<IChangePlanResult> ChangePlanAsync(string userId, Guid appId, string appName, string planId)
{
return Task.FromResult<IChangePlanResult>(PlanChangedResult.Instance);
}
public Task<string> GetPortalLinkAsync(string userId)
{
return Task.FromResult(string.Empty);
}
}
}

19
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()
{
}
}
}

19
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()
{
}
}
}

25
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;
}
}
}

31
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<AppState>, 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; }
}
}

117
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<AssetDomainObject>(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<AssetDomainObject>(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<AssetDomainObject>(context, a =>
{
GuardAsset.CanRename(command, a.State.FileName);
a.Rename(command);
});
}
protected Task On(DeleteAsset command, CommandContext context)
{
return handler.UpdateAsync<AssetDomainObject>(context, a =>
{
GuardAsset.CanDelete(command);
a.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
}
}
}

107
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<AssetDomainObject, AssetState>
{
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<AssetState> updater)
{
return State.Clone().Update((SquidexCommand)command, updater);
}
}
}

23
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;
}
}
}

23
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; }
}
}
}

25
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();
}
}
}

14
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
{
}
}

15
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; }
}
}

19
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; }
}
}

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

25
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; }
}
}

16
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
{
}
}

21
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; }
}
}

23
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<IReadOnlyList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, HashSet<Guid> ids = null, string query = null, int take = 10, int skip = 0);
Task<IAssetEntity> FindAssetAsync(Guid id);
Task<long> CountAsync(Guid appId, HashSet<string> mimeTypes = null, HashSet<Guid> ids = null, string query = null);
}
}

21
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<IReadOnlyList<IAssetStatsEntity>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate);
Task<long> GetTotalSizeAsync(Guid appId);
}
}

55
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<AssetState>,
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; }
}
}
}

45
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<T> : Cloneable<T>,
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 => { });
}
}
}

77
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<T>(this T entity, SquidexCommand command, Action<T> 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;
}
}
}
}

44
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<string, string> parameters = new Dictionary<string, string>();
public string Channel { get; }
public string Message { get; }
public IReadOnlyDictionary<string, string> 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;
}
}
}

66
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<string, string> texts = new Dictionary<string, string>();
private readonly TypeNameRegistry typeNameRegistry;
public IReadOnlyDictionary<string, string> Texts
{
get { return texts; }
}
protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry)
{
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
this.typeNameRegistry = typeNameRegistry;
}
protected void AddEventMessage<TEvent>(string message) where TEvent : IEvent
{
Guard.NotNullOrEmpty(message, nameof(message));
texts[typeNameRegistry.GetName<TEvent>()] = 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<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event)
{
if (HasEventText(@event.Payload))
{
return CreateEventCoreAsync(@event);
}
return Task.FromResult<HistoryEventToStore>(null);
}
protected abstract Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event);
}
}

24
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; }
}
}

21
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<string, string> Texts { get; }
Task<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event);
}
}

19
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<IReadOnlyList<IHistoryEventEntity>> QueryByChannelAsync(Guid appId, string channelPrefix, int count);
}
}

34
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<IAppEntity> GetAppAsync(string appName);
Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false);
Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false);
Task<List<ISchemaEntity>> GetSchemasAsync(string appName);
Task<List<IRuleEntity>> GetRulesAsync(string appName);
Task<List<IAppEntity>> GetUserApps(string userId);
}
}

22
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; }
}
}

17
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; }
}
}

17
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; }
}
}

17
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; }
}
}

15
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; }
}
}

17
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; }
}
}

17
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; }
}
}

17
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; }
}
}

15
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; }
}
}

20
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();
}
}
}

14
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
{
}
}

14
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
{
}
}

14
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
{
}
}

23
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; }
}
}
}

19
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; }
}
}

14
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
{
}
}

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

40
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<Task<IEnumerable<ValidationError>>>
{
public static Task<IEnumerable<ValidationError>> ValidateAsync(RuleAction action)
{
Guard.NotNull(action, nameof(action));
var visitor = new RuleActionValidator();
return action.Accept(visitor);
}
public Task<IEnumerable<ValidationError>> Visit(WebhookAction action)
{
var errors = new List<ValidationError>();
if (action.Url == null || !action.Url.IsAbsoluteUri)
{
errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url)));
}
return Task.FromResult<IEnumerable<ValidationError>>(errors);
}
}
}

56
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<Task<IEnumerable<ValidationError>>>
{
public Func<Guid, Task<ISchemaEntity>> SchemaProvider { get; }
public RuleTriggerValidator(Func<Guid, Task<ISchemaEntity>> schemaProvider)
{
SchemaProvider = schemaProvider;
}
public static Task<IEnumerable<ValidationError>> 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<IEnumerable<ValidationError>> 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<ValidationError>();
}
}
}

22
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; }
}
}

29
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; }
}
}

35
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<IRuleEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken));
Task<int> CountByAppAsync(Guid appId);
Task<IReadOnlyList<IRuleEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20);
Task<IRuleEventEntity> FindAsync(Guid id);
}
}

92
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<RuleDomainObject>(context, async w =>
{
await GuardRule.CanCreate(command, appProvider);
w.Create(command);
});
}
protected Task On(UpdateRule command, CommandContext context)
{
return handler.UpdateAsync<RuleDomainObject>(context, async c =>
{
await GuardRule.CanUpdate(command, appProvider);
c.Update(command);
});
}
protected Task On(EnableRule command, CommandContext context)
{
return handler.UpdateAsync<RuleDomainObject>(context, r =>
{
GuardRule.CanEnable(command, r.State.RuleDef);
r.Enable(command);
});
}
protected Task On(DisableRule command, CommandContext context)
{
return handler.UpdateAsync<RuleDomainObject>(context, r =>
{
GuardRule.CanDisable(command, r.State.RuleDef);
r.Disable(command);
});
}
protected Task On(DeleteRule command, CommandContext context)
{
return handler.UpdateAsync<RuleDomainObject>(context, c =>
{
GuardRule.CanDelete(command);
c.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
}
}
}

157
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<IRuleEventEntity> requestBlock;
private readonly IRuleEventRepository ruleEventRepository;
private readonly RuleService ruleService;
private readonly CompletionTimer timer;
private readonly ConcurrentDictionary<Guid, bool> executing = new ConcurrentDictionary<Guid, bool>();
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<IRuleEventEntity>(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;
}
}
}

93
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<RuleDomainObject, RuleState>
{
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<Rule, Rule> updater)
{
UpdateState(command, s => s.RuleDef = updater(s.RuleDef));
}
protected override RuleState CloneState(ICommand command, Action<RuleState> updater)
{
return State.Clone().Update((SquidexCommand)command, updater);
}
}
}

73
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<IEvent> @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);
}
}
}
}
}
}

18
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
}
}

26
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<RuleState>, IRuleEntity
{
[JsonProperty]
public Guid AppId { get; set; }
[JsonProperty]
public Rule RuleDef { get; set; }
[JsonProperty]
public bool IsDeleted { get; set; }
}
}

21
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; }
}
}
}

18
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<Guid> SchemaId { get; set; }
}
}

21
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; }
}
}

23
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; }
}
}

36
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<Squidex.Domain.Apps.Entities.Schemas.Commands.CreateSchemaField>;
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();
}
}
}

27
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; }
}
}

14
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
{
}
}

14
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
{
}
}

14
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
{
}
}

14
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
{
}
}

15
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; }
}
}

14
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
{
}
}

14
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
{
}
}

14
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
{
}
}

17
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<long> FieldIds { get; set; }
}
}

14
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
{
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save