Browse Source

Merge pull request #341 from Squidex/feature_conditions_rule_scripting

Feature conditions rule scripting
pull/342/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
b609b84224
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      NuGet.Config
  2. 4
      extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
  3. 8
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  4. 4
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
  5. 10
      src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs
  6. 4
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  7. 12
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs
  8. 16
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs
  9. 6
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
  10. 2
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  11. 21
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs
  12. 18
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs
  13. 31
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs
  14. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs
  15. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs
  16. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
  17. 79
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  18. 10
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  19. 7
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  20. 29
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs
  21. 78
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
  22. 13
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  23. 10
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
  24. 15
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  25. 6
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs
  26. 24
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs
  27. 6
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  28. 6
      src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
  29. 149
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  30. 44
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
  31. 43
      src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs
  32. 6
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  33. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  34. 12
      src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs
  35. 4
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs
  36. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs
  37. 6
      src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  38. 13
      src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs
  39. 13
      src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs
  40. 2
      src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  41. 4
      src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs
  42. 6
      src/Squidex.Infrastructure/IMigrated.cs
  43. 10
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  44. 8
      src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs
  45. 54
      src/Squidex.Shared/Users/ClientUser.cs
  46. 4
      src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
  47. 21
      src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs
  48. 4
      src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs
  49. 34
      src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
  50. 16
      src/Squidex/Squidex.csproj
  51. 57
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html
  52. 33
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss
  53. 13
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts
  54. 86
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  55. 37
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss
  56. 134
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  57. 6
      tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs
  58. 233
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  59. 12
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  60. 92
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs
  61. 193
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs
  62. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs
  63. 2
      tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  64. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs
  65. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs
  66. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  67. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs
  68. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  69. 2
      tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  70. 2
      tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs
  71. 2
      tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  72. 4
      tests/Squidex.Tests/Squidex.Tests.csproj
  73. 3
      tools/Migrate_01/OldEvents/AppClientChanged.cs
  74. 3
      tools/Migrate_01/OldEvents/AppClientUpdated.cs
  75. 3
      tools/Migrate_01/OldEvents/AppContributorAssigned.cs
  76. 3
      tools/Migrate_01/OldEvents/ContentArchived.cs
  77. 3
      tools/Migrate_01/OldEvents/ContentPublished.cs
  78. 3
      tools/Migrate_01/OldEvents/ContentRestored.cs
  79. 3
      tools/Migrate_01/OldEvents/ContentUnpublished.cs
  80. 71
      tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs
  81. 49
      tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs
  82. 87
      tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs

1
NuGet.Config

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<packageSources> <packageSources>
<add key="myget.org" value="https://www.myget.org/F/jint/api/v3/index.json" protocolVersion="3" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources> </packageSources>
</configuration> </configuration>

4
extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs

@ -30,9 +30,11 @@ namespace Squidex.Extensions.Actions.Fastly
protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action)
{ {
var id = @event is EnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty;
var ruleJob = new FastlyJob var ruleJob = new FastlyJob
{ {
Key = @event.AggregateId.ToString(), Key = id,
FastlyApiKey = action.ApiKey, FastlyApiKey = action.ApiKey,
FastlyServiceID = action.ServiceId FastlyServiceID = action.ServiceId
}; };

8
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -10,13 +10,13 @@
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" /> <ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.2.1" /> <PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" /> <PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="6.4.0" /> <PackageReference Include="Elasticsearch.Net" Version="6.4.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NodaTime" Version="2.4.2" /> <PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

4
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs

@ -11,8 +11,8 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
public interface IRuleTriggerVisitor<out T> public interface IRuleTriggerVisitor<out T>
{ {
T Visit(AssetChangedTrigger trigger); T Visit(AssetChangedTriggerV2 trigger);
T Visit(ContentChangedTrigger trigger); T Visit(ContentChangedTriggerV2 trigger);
} }
} }

10
src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Json namespace Squidex.Domain.Apps.Core.Rules.Json
@ -32,7 +33,14 @@ namespace Squidex.Domain.Apps.Core.Rules.Json
public Rule ToRule() public Rule ToRule()
{ {
var rule = new Rule(Trigger, Action); var trigger = Trigger;
if (trigger is IMigrated<RuleTrigger> migrated)
{
trigger = migrated.Migrate();
}
var rule = new Rule(trigger, Action);
if (!IsEnabled) if (!IsEnabled)
{ {

4
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs

@ -16,8 +16,6 @@ namespace Squidex.Domain.Apps.Core.Rules
public Guid AppId { get; set; } public Guid AppId { get; set; }
public Guid AggregateId { get; set; }
public string EventName { get; set; } public string EventName { get; set; }
public string ActionName { get; set; } public string ActionName { get; set; }
@ -26,6 +24,8 @@ namespace Squidex.Domain.Apps.Core.Rules
public string Description { get; set; } public string Description { get; set; }
public long ExecutionPartition { get; set; }
public Instant Created { get; set; } public Instant Created { get; set; }
public Instant Expires { get; set; } public Instant Expires { get; set; }

12
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs → src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs

@ -9,16 +9,10 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(AssetChangedTrigger))] [TypeName(nameof(AssetChangedTriggerV2))]
public sealed class AssetChangedTrigger : RuleTrigger public sealed class AssetChangedTriggerV2 : RuleTrigger
{ {
public bool SendCreate { get; set; } public string Condition { get; set; }
public bool SendUpdate { get; set; }
public bool SendRename { get; set; }
public bool SendDelete { get; set; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {

16
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs → src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs

@ -9,22 +9,10 @@ using System;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
public sealed class ContentChangedTriggerSchema : Freezable public sealed class ContentChangedTriggerSchemaV2 : Freezable
{ {
public Guid SchemaId { get; set; } public Guid SchemaId { get; set; }
public bool SendCreate { get; set; } public string Condition { get; set; }
public bool SendUpdate { get; set; }
public bool SendDelete { get; set; }
public bool SendPublish { get; set; }
public bool SendUnpublish { get; set; }
public bool SendArchived { get; set; }
public bool SendRestore { get; set; }
} }
} }

6
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs → src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs

@ -10,10 +10,10 @@ using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(ContentChangedTrigger))] [TypeName(nameof(ContentChangedTriggerV2))]
public sealed class ContentChangedTrigger : RuleTrigger public sealed class ContentChangedTriggerV2 : RuleTrigger
{ {
public ReadOnlyCollection<ContentChangedTriggerSchema> Schemas { get; set; } public ReadOnlyCollection<ContentChangedTriggerSchemaV2> Schemas { get; set; }
public bool HandleAll { get; set; } public bool HandleAll { get; set; }

2
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -8,7 +8,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="3.3.3"> <PackageReference Include="Fody" Version="3.3.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>

21
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs

@ -5,26 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{ {
public sealed class EnrichedAssetEvent : EnrichedEvent public sealed class EnrichedAssetEvent : EnrichedEntityEvent
{ {
public EnrichedAssetEventType Type { get; set; } public EnrichedAssetEventType Type { get; set; }
public Guid Id { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public string MimeType { get; set; } public string MimeType { get; set; }
public string FileName { get; set; } public string FileName { get; set; }
@ -38,10 +24,5 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
public int? PixelWidth { get; set; } public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; } public int? PixelHeight { get; set; }
public override Guid AggregateId
{
get { return Id; }
}
} }
} }

18
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs

@ -5,10 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{ {
@ -16,23 +13,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{ {
public EnrichedContentEventType Type { get; set; } public EnrichedContentEventType Type { get; set; }
public Guid Id { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public NamedContentData Data { get; set; } public NamedContentData Data { get; set; }
public Status Status { get; set; } public Status Status { get; set; }
public override Guid AggregateId
{
get { return Id; }
}
} }
} }

31
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
public abstract class EnrichedEntityEvent : EnrichedEvent
{
public Guid Id { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public override void CalculatePartition()
{
Partition = Id.GetHashCode();
}
}
}

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs

@ -25,10 +25,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
public long Version { get; set; } public long Version { get; set; }
[IgnoreDataMember] public long Partition { get; set; }
public abstract Guid AggregateId { get; }
[IgnoreDataMember] [IgnoreDataMember]
public IUser User { get; set; } public IUser User { get; set; }
public abstract void CalculatePartition();
} }
} }

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{ {
public abstract class EnrichedSchemaEvent : EnrichedEvent public abstract class EnrichedSchemaEvent : EnrichedEntityEvent
{ {
public NamedId<Guid> SchemaId { get; set; } public NamedId<Guid> SchemaId { get; set; }
} }

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs

@ -6,9 +6,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules namespace Squidex.Domain.Apps.Core.HandleRules
{ {
@ -16,6 +15,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{ {
Type TriggerType { get; } Type TriggerType { get; }
bool Triggers(Envelope<AppEvent> @event, RuleTrigger trigger); bool Triggers(EnrichedEvent @event, RuleTrigger trigger);
} }
} }

79
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -12,6 +12,7 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -21,7 +22,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{ {
public class RuleEventFormatter public class RuleEventFormatter
{ {
private const string Undefined = "UNDEFINED"; private const string Fallback = "null";
private const string ScriptSuffix = ")";
private const string ScriptPrefix = "Script(";
private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray(); private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray();
private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray(); private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray();
private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled);
@ -29,13 +32,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules
private readonly List<(char[] Pattern, Func<EnrichedEvent, string> Replacer)> patterns = new List<(char[] Pattern, Func<EnrichedEvent, string> Replacer)>(); private readonly List<(char[] Pattern, Func<EnrichedEvent, string> Replacer)> patterns = new List<(char[] Pattern, Func<EnrichedEvent, string> Replacer)>();
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly IRuleUrlGenerator urlGenerator; private readonly IRuleUrlGenerator urlGenerator;
private readonly IScriptEngine scriptEngine;
public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator) public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine)
{ {
Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(urlGenerator, nameof(urlGenerator)); Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.jsonSerializer = jsonSerializer; this.jsonSerializer = jsonSerializer;
this.scriptEngine = scriptEngine;
this.urlGenerator = urlGenerator; this.urlGenerator = urlGenerator;
AddPattern("APP_ID", AppId); AddPattern("APP_ID", AppId);
@ -46,6 +52,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("SCHEMA_NAME", SchemaName); AddPattern("SCHEMA_NAME", SchemaName);
AddPattern("TIMESTAMP_DATETIME", TimestampTime); AddPattern("TIMESTAMP_DATETIME", TimestampTime);
AddPattern("TIMESTAMP_DATE", TimestampDate); AddPattern("TIMESTAMP_DATE", TimestampDate);
AddPattern("USER_ID", UserId);
AddPattern("USER_NAME", UserName); AddPattern("USER_NAME", UserName);
AddPattern("USER_EMAIL", UserEmail); AddPattern("USER_EMAIL", UserEmail);
} }
@ -72,6 +79,19 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return text; return text;
} }
if (text.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && text.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase))
{
var script = text.Substring(ScriptPrefix.Length, text.Length - ScriptPrefix.Length - ScriptSuffix.Length);
var customFunctions = new Dictionary<string, Func<string>>
{
["contentUrl"] = () => ContentUrl(@event),
["contentAction"] = () => ContentAction(@event)
};
return scriptEngine.Interpolate("event", @event, script, customFunctions);
}
var current = text.AsSpan(); var current = text.AsSpan();
var sb = new StringBuilder(); var sb = new StringBuilder();
@ -127,7 +147,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
} }
else else
{ {
sb.Append(Undefined); sb.Append(Fallback);
} }
current = current.Slice(match.Length + 1); current = current.Slice(match.Length + 1);
@ -169,7 +189,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return schemaEvent.SchemaId.Id.ToString(); return schemaEvent.SchemaId.Id.ToString();
} }
return Undefined; return Fallback;
} }
private static string SchemaName(EnrichedEvent @event) private static string SchemaName(EnrichedEvent @event)
@ -179,17 +199,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return schemaEvent.SchemaId.Name; return schemaEvent.SchemaId.Name;
} }
return Undefined; return Fallback;
} }
private static string ContentAction(EnrichedEvent @event) private static string ContentAction(EnrichedEvent @event)
{ {
if (@event is EnrichedContentEvent contentEvent) if (@event is EnrichedContentEvent contentEvent)
{ {
return contentEvent.Type.ToString().ToLowerInvariant(); return contentEvent.Type.ToString();
} }
return Undefined; return Fallback;
} }
private string ContentUrl(EnrichedEvent @event) private string ContentUrl(EnrichedEvent @event)
@ -199,43 +219,22 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return urlGenerator.GenerateContentUIUrl(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); return urlGenerator.GenerateContentUIUrl(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id);
} }
return Undefined; return Fallback;
} }
private static string UserName(EnrichedEvent @event) private static string UserName(EnrichedEvent @event)
{ {
if (@event.Actor != null) return @event.User?.DisplayName() ?? Fallback;
{ }
if (@event.Actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
return @event.Actor.ToString();
}
if (@event.User != null)
{
return @event.User.DisplayName();
}
}
return Undefined; private static string UserId(EnrichedEvent @event)
{
return @event.User?.Id ?? Fallback;
} }
private static string UserEmail(EnrichedEvent @event) private static string UserEmail(EnrichedEvent @event)
{ {
if (@event.Actor != null) return @event.User?.Email ?? Fallback;
{
if (@event.Actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
return @event.Actor.ToString();
}
if (@event.User != null)
{
return @event.User.Email;
}
}
return Undefined;
} }
private static string CalculateData(NamedContentData data, Match match) private static string CalculateData(NamedContentData data, Match match)
@ -251,12 +250,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
if (!data.TryGetValue(path[0], out var field)) if (!data.TryGetValue(path[0], out var field))
{ {
return Undefined; return Fallback;
} }
if (!field.TryGetValue(path[1], out var value)) if (!field.TryGetValue(path[1], out var value))
{ {
return Undefined; return Fallback;
} }
for (var j = 2; j < path.Length; j++) for (var j = 2; j < path.Length; j++)
@ -272,16 +271,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules
} }
else else
{ {
return Undefined; return Fallback;
} }
} }
if (value == null || value.Type == JsonValueType.Null) if (value == null || value.Type == JsonValueType.Null)
{ {
return Undefined; return Fallback;
} }
return value.ToString() ?? Undefined; return value.ToString() ?? Fallback;
} }
} }
} }

10
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -85,7 +85,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var appEventEnvelope = @event.To<AppEvent>(); var appEventEnvelope = @event.To<AppEvent>();
if (!triggerHandler.Triggers(appEventEnvelope, rule.Trigger)) var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope);
if (!triggerHandler.Triggers(enrichedEvent, rule.Trigger))
{ {
return null; return null;
} }
@ -104,22 +106,22 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null; return null;
} }
var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope);
var actionName = typeNameRegistry.GetName(actionType); var actionName = typeNameRegistry.GetName(actionType);
var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
var json = jsonSerializer.Serialize(actionData.Data); var json = jsonSerializer.Serialize(actionData.Data);
enrichedEvent.CalculatePartition();
var job = new RuleJob var job = new RuleJob
{ {
JobId = Guid.NewGuid(), JobId = Guid.NewGuid(),
ActionName = actionName, ActionName = actionName,
ActionData = json, ActionData = json,
AggregateId = enrichedEvent.AggregateId,
AppId = appEvent.AppId.Id, AppId = appEvent.AppId.Id,
Created = now, Created = now,
EventName = enrichedEvent.Name, EventName = enrichedEvent.Name,
ExecutionPartition = enrichedEvent.Partition,
Expires = expires, Expires = expires,
Description = actionData.Description Description = actionData.Description
}; };

7
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs

@ -6,9 +6,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules namespace Squidex.Domain.Apps.Core.HandleRules
{ {
@ -19,11 +18,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
get { return typeof(T); } get { return typeof(T); }
} }
bool IRuleTriggerHandler.Triggers(Envelope<AppEvent> @event, RuleTrigger trigger) bool IRuleTriggerHandler.Triggers(EnrichedEvent @event, RuleTrigger trigger)
{ {
return Triggers(@event, (T)trigger); return Triggers(@event, (T)trigger);
} }
protected abstract bool Triggers(Envelope<AppEvent> @event, T trigger); protected abstract bool Triggers(EnrichedEvent @event, T trigger);
} }
} }

29
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs

@ -5,27 +5,32 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules.Triggers namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
{ {
public sealed class AssetChangedTriggerHandler : RuleTriggerHandler<AssetChangedTrigger> public sealed class AssetChangedTriggerHandler : RuleTriggerHandler<AssetChangedTriggerV2>
{ {
protected override bool Triggers(Envelope<AppEvent> @event, AssetChangedTrigger trigger) private readonly IScriptEngine scriptEngine;
public AssetChangedTriggerHandler(IScriptEngine scriptEngine)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
this.scriptEngine = scriptEngine;
}
protected override bool Triggers(EnrichedEvent @event, AssetChangedTriggerV2 trigger)
{ {
return @event.Payload is AssetEvent assetEvent && MatchsType(trigger, assetEvent); return @event is EnrichedAssetEvent assetEvent && MatchsType(trigger, assetEvent);
} }
private static bool MatchsType(AssetChangedTrigger trigger, AssetEvent @event) private bool MatchsType(AssetChangedTriggerV2 trigger, EnrichedAssetEvent assetEvent)
{ {
return return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", assetEvent, trigger.Condition);
trigger.SendCreate && @event is AssetCreated ||
trigger.SendUpdate && @event is AssetUpdated ||
trigger.SendDelete && @event is AssetDeleted ||
trigger.SendRename && @event is AssetRenamed;
} }
} }
} }

78
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs

@ -5,32 +5,36 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules.Triggers namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
{ {
public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTrigger> public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2>
{ {
protected override bool Triggers(Envelope<AppEvent> @event, ContentChangedTrigger trigger) private readonly IScriptEngine scriptEngine;
public ContentChangedTriggerHandler(IScriptEngine scriptEngine)
{ {
if (trigger.HandleAll && Guard.NotNull(scriptEngine, nameof(scriptEngine));
@event.Payload is ContentEvent &&
!(@event.Payload is ContentChangesPublished) && this.scriptEngine = scriptEngine;
!(@event.Payload is ContentChangesDiscarded) && }
!(@event.Payload is ContentUpdateProposed))
protected override bool Triggers(EnrichedEvent @event, ContentChangedTriggerV2 trigger)
{
if (trigger.HandleAll)
{ {
return true; return true;
} }
if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent) if (trigger.Schemas != null && @event is EnrichedSchemaEvent schemaEvent)
{ {
foreach (var schema in trigger.Schemas) foreach (var schema in trigger.Schemas)
{ {
if (MatchsSchema(schema, schemaEvent) && MatchsType(schema, schemaEvent)) if (MatchsSchema(schema, schemaEvent) && MatchsCondition(schema, schemaEvent))
{ {
return true; return true;
} }
@ -40,56 +44,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
return false; return false;
} }
private static bool MatchsSchema(ContentChangedTriggerSchema schema, SchemaEvent @event) private bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event)
{ {
return @event.SchemaId.Id == schema.SchemaId; return @event.SchemaId.Id == schema.SchemaId;
} }
private static bool MatchsType(ContentChangedTriggerSchema schema, SchemaEvent @event) private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event)
{
return
IsArchived(schema, @event) ||
IsCreate(schema, @event) ||
IsDelete(schema, @event) ||
IsPublished(schema, @event) ||
IsRestored(schema, @event) ||
IsUpdate(schema, @event) ||
IsUnpublished(schema, @event);
}
private static bool IsPublished(ContentChangedTriggerSchema schema, SchemaEvent @event)
{
return schema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Change == StatusChange.Published;
}
private static bool IsRestored(ContentChangedTriggerSchema schema, SchemaEvent @event)
{
return schema.SendRestore && @event is ContentStatusChanged statusChanged && statusChanged.Change == StatusChange.Restored;
}
private static bool IsArchived(ContentChangedTriggerSchema schema, SchemaEvent @event)
{
return schema.SendArchived && @event is ContentStatusChanged statusChanged && statusChanged.Change == StatusChange.Archived;
}
private static bool IsUnpublished(ContentChangedTriggerSchema schema, SchemaEvent @event)
{
return schema.SendUnpublish && @event is ContentStatusChanged statusChanged && statusChanged.Change == StatusChange.Unpublished;
}
private static bool IsCreate(ContentChangedTriggerSchema schema, SchemaEvent @event)
{
return schema.SendCreate && @event is ContentCreated;
}
private static bool IsUpdate(ContentChangedTriggerSchema schema, SchemaEvent @event)
{
return schema.SendUpdate && @event is ContentUpdated || schema.SendUpdate && @event is ContentChangesPublished;
}
private static bool IsDelete(ContentChangedTriggerSchema schema, SchemaEvent @event)
{ {
return schema.SendDelete && @event is ContentDeleted; return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition);
} }
} }
} }

13
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
private readonly NamedContentData contentData; private readonly NamedContentData contentData;
private HashSet<string> fieldsToDelete; private HashSet<string> fieldsToDelete;
private Dictionary<string, ContentDataProperty> fieldProperties; private Dictionary<string, PropertyDescriptor> fieldProperties;
private bool isChanged; private bool isChanged;
public ContentDataObject(Engine engine, NamedContentData contentData) public ContentDataObject(Engine engine, NamedContentData contentData)
@ -55,7 +55,9 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
foreach (var kvp in fieldProperties) foreach (var kvp in fieldProperties)
{ {
if (kvp.Value.ContentField.TryUpdate(out var fieldData)) var value = (ContentDataProperty)kvp.Value;
if (value.ContentField.TryUpdate(out var fieldData))
{ {
contentData[kvp.Key] = fieldData; contentData[kvp.Key] = fieldData;
} }
@ -109,17 +111,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
EnsurePropertiesInitialized(); EnsurePropertiesInitialized();
foreach (var property in fieldProperties) return fieldProperties;
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
} }
private void EnsurePropertiesInitialized() private void EnsurePropertiesInitialized()
{ {
if (fieldProperties == null) if (fieldProperties == null)
{ {
fieldProperties = new Dictionary<string, ContentDataProperty>(contentData.Count); fieldProperties = new Dictionary<string, PropertyDescriptor>(contentData.Count);
foreach (var kvp in contentData) foreach (var kvp in contentData)
{ {

10
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs

@ -7,18 +7,17 @@
using Jint.Native; using Jint.Native;
using Jint.Runtime; using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
public sealed class ContentDataProperty : PropertyDescriptor public sealed class ContentDataProperty : CustomProperty
{ {
private readonly ContentDataObject contentData; private readonly ContentDataObject contentData;
private ContentFieldObject contentField; private ContentFieldObject contentField;
private JsValue value; private JsValue value;
public override JsValue Value protected override JsValue CustomValue
{ {
get get
{ {
@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
contentField.Put(kvp.Key, kvp.Value.Value, true); contentField.Put(kvp.Key, kvp.Value.Value, true);
} }
this.value = new JsValue(contentField); this.value = contentField;
} }
} }
} }
@ -53,14 +52,13 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
} }
public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null) public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null)
: base(null, true, true, true)
{ {
this.contentData = contentData; this.contentData = contentData;
this.contentField = contentField; this.contentField = contentField;
if (contentField != null) if (contentField != null)
{ {
value = new JsValue(contentField); value = contentField;
} }
} }
} }

15
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
private readonly ContentDataObject contentData; private readonly ContentDataObject contentData;
private readonly ContentFieldData fieldData; private readonly ContentFieldData fieldData;
private HashSet<string> valuesToDelete; private HashSet<string> valuesToDelete;
private Dictionary<string, ContentFieldProperty> valueProperties; private Dictionary<string, PropertyDescriptor> valueProperties;
private bool isChanged; private bool isChanged;
public ContentFieldData FieldData public ContentFieldData FieldData
@ -67,9 +67,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
foreach (var kvp in valueProperties) foreach (var kvp in valueProperties)
{ {
if (kvp.Value.IsChanged) var value = (ContentFieldProperty)kvp.Value;
if (value.IsChanged)
{ {
fieldData[kvp.Key] = kvp.Value.ContentValue; fieldData[kvp.Key] = value.ContentValue;
} }
} }
} }
@ -114,17 +116,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
EnsurePropertiesInitialized(); EnsurePropertiesInitialized();
foreach (var property in valueProperties) return valueProperties;
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
} }
private void EnsurePropertiesInitialized() private void EnsurePropertiesInitialized()
{ {
if (valueProperties == null) if (valueProperties == null)
{ {
valueProperties = new Dictionary<string, ContentFieldProperty>(FieldData.Count); valueProperties = new Dictionary<string, PropertyDescriptor>(FieldData.Count);
foreach (var kvp in FieldData) foreach (var kvp in FieldData)
{ {

6
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs

@ -6,19 +6,18 @@
// ========================================================================== // ==========================================================================
using Jint.Native; using Jint.Native;
using Jint.Runtime.Descriptors;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
public sealed class ContentFieldProperty : PropertyDescriptor public sealed class ContentFieldProperty : CustomProperty
{ {
private readonly ContentFieldObject contentField; private readonly ContentFieldObject contentField;
private IJsonValue contentValue; private IJsonValue contentValue;
private JsValue value; private JsValue value;
private bool isChanged; private bool isChanged;
public override JsValue Value protected override JsValue CustomValue
{ {
get get
{ {
@ -49,7 +48,6 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
} }
public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null) public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null)
: base(null, true, true, true)
{ {
this.contentField = contentField; this.contentField = contentField;
this.contentValue = contentValue; this.contentValue = contentValue;

24
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint.Runtime.Descriptors;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public abstract class CustomProperty : PropertyDescriptor
{
public CustomProperty()
: base(PropertyFlag.CustomJsValue)
{
Enumerable = true;
Writable = true;
Configurable = true;
}
}
}

6
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs

@ -27,11 +27,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
case JsonNull n: case JsonNull n:
return JsValue.Null; return JsValue.Null;
case JsonScalar<string> s: case JsonScalar<string> s:
return new JsValue(s.Value); return new JsString(s.Value);
case JsonScalar<bool> b: case JsonScalar<bool> b:
return new JsValue(b.Value); return new JsBoolean(b.Value);
case JsonScalar<double> b: case JsonScalar<double> b:
return new JsValue(b.Value); return new JsNumber(b.Value);
case JsonObject obj: case JsonObject obj:
return FromObject(obj, engine); return FromObject(obj, engine);
case JsonArray arr: case JsonArray arr:

6
src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
@ -16,5 +18,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
NamedContentData ExecuteAndTransform(ScriptContext context, string script); NamedContentData ExecuteAndTransform(ScriptContext context, string script);
NamedContentData Transform(ScriptContext context, string script); NamedContentData Transform(ScriptContext context, string script);
bool Evaluate(string name, object context, string script);
string Interpolate(string name, object context, string script, Dictionary<string, Func<string>> customFormatters = null);
} }
} }

149
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -6,15 +6,20 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Claims;
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Jint.Native.Date;
using Jint.Native.Object; using Jint.Native.Object;
using Jint.Parser;
using Jint.Runtime; using Jint.Runtime;
using Jint.Runtime.Interop; using Jint.Runtime.Interop;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
@ -22,6 +27,40 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
public sealed class Converter : IObjectConverter
{
public Engine Engine { get; set; }
public bool TryConvert(object value, out JsValue result)
{
result = null;
if (value is Enum)
{
result = value.ToString();
return true;
}
switch (value)
{
case IUser user:
result = JintUser.Create(Engine, user);
return true;
case ClaimsPrincipal principal:
result = JintUser.Create(Engine, principal);
return true;
case Instant instant:
result = JsValue.FromObject(Engine, instant.ToDateTimeUtc());
return true;
case NamedContentData content:
result = new ContentDataObject(Engine, content);
return true;
}
return false;
}
}
public void Execute(ScriptContext context, string script) public void Execute(ScriptContext context, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -115,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
engine.Execute(script); engine.Execute(script);
} }
catch (ParserException ex) catch (ArgumentException ex)
{ {
throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message));
} }
@ -127,7 +166,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
private Engine CreateScriptEngine(ScriptContext context) private Engine CreateScriptEngine(ScriptContext context)
{ {
var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict()); var engine = CreateScriptEngine();
var contextInstance = new ObjectInstance(engine); var contextInstance = new ObjectInstance(engine);
@ -143,7 +182,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
if (context.User != null) if (context.User != null)
{ {
contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false); contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false);
} }
if (!string.IsNullOrWhiteSpace(context.Operation)) if (!string.IsNullOrWhiteSpace(context.Operation))
@ -152,12 +191,57 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
engine.SetValue("ctx", contextInstance); engine.SetValue("ctx", contextInstance);
engine.SetValue("context", contextInstance);
return engine;
}
private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary<string, Func<string>> customFormatters = null)
{
var converter = new Converter();
var engine = new Engine(options =>
{
if (resolver != null)
{
options.SetReferencesResolver(resolver);
}
engine.SetValue("slugify", new ClrFunctionInstance(engine, Slugify)); options.TimeoutInterval(Timeout).Strict().AddObjectConverter(converter);
});
if (customFormatters != null)
{
foreach (var kvp in customFormatters)
{
engine.SetValue(kvp.Key, Safe(kvp.Value));
}
}
converter.Engine = engine;
engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify));
engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate));
engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate));
return engine; return engine;
} }
private static Func<string> Safe(Func<string> func)
{
return new Func<string>(() =>
{
try
{
return func();
}
catch
{
return "null";
}
});
}
private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) private static JsValue Slugify(JsValue thisObject, JsValue[] arguments)
{ {
try try
@ -178,6 +262,21 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments)
{
try
{
var dateValue = ((DateInstance)arguments.At(0)).ToDateTime();
var dateFormat = TypeConverter.ToString(arguments.At(1));
return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture);
}
catch
{
return JsValue.Undefined;
}
}
private static void EnableDisallow(Engine engine) private static void EnableDisallow(Engine engine)
{ {
engine.SetValue("disallow", new Action<string>(message => engine.SetValue("disallow", new Action<string>(message =>
@ -197,5 +296,45 @@ namespace Squidex.Domain.Apps.Core.Scripting
throw new ValidationException("Script rejected the operation.", errors); throw new ValidationException("Script rejected the operation.", errors);
})); }));
} }
public bool Evaluate(string name, object context, string script)
{
try
{
var result =
CreateScriptEngine(NullPropagation.Instance)
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
.ToObject();
return (bool)result;
}
catch
{
return false;
}
}
public string Interpolate(string name, object context, string script, Dictionary<string, Func<string>> customFormatters = null)
{
try
{
var result =
CreateScriptEngine(NullPropagation.Instance, customFormatters)
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
.ToObject();
var converted = result.ToString();
return converted == "undefined" ? "null" : converted;
}
catch (Exception ex)
{
return ex.Message;
}
}
} }
} }

44
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs

@ -5,21 +5,31 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Jint; using Jint;
using Jint.Native; using Jint.Runtime.Interop;
using Jint.Native.Object;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class JintUser : ObjectInstance public static class JintUser
{ {
private static readonly char[] ClaimSeparators = { '/', '.', ':' }; private static readonly char[] ClaimSeparators = { '/', '.', ':' };
public JintUser(Engine engine, ClaimsPrincipal principal) public static ObjectWrapper Create(Engine engine, IUser user)
: base(engine) {
var clientId = user.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value;
var isClient = !string.IsNullOrWhiteSpace(clientId);
return CreateUser(engine, user.Id, isClient, user.Email, user.DisplayName(), user.Claims);
}
public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal)
{ {
var id = principal.OpenIdSubject(); var id = principal.OpenIdSubject();
@ -30,22 +40,20 @@ namespace Squidex.Domain.Apps.Core.Scripting
id = principal.OpenIdClientId(); id = principal.OpenIdClientId();
} }
FastAddProperty("id", id, false, true, false); var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value;
FastAddProperty("isClient", isClient, false, true, false);
FastAddProperty("email", principal.OpenIdEmail(), false, true, false);
var claimsInstance = new ObjectInstance(engine); return CreateUser(engine, id, isClient, principal.OpenIdEmail(), name, principal.Claims);
}
foreach (var group in principal.Claims.GroupBy(x => x.Type))
{
var propertyName = group.Key.Split(ClaimSeparators).Last();
var propertyValue = engine.Array.Construct(group.Select(x => new JsValue(x.Value)).ToArray());
claimsInstance.FastAddProperty(propertyName, propertyValue, false, true, false); private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable<Claim> allClaims)
} {
var claims =
allClaims.GroupBy(x => x.Type)
.ToDictionary(
x => x.Key.Split(ClaimSeparators).Last(),
x => x.Select(y => y.Value).ToArray());
FastAddProperty("claims", claimsInstance, false, true, false); return new ObjectWrapper(engine, new { id, isClient, email, name, claims });
} }
} }
} }

43
src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs

@ -0,0 +1,43 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
using Jint.Runtime.References;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class NullPropagation : IReferenceResolver
{
public static readonly NullPropagation Instance = new NullPropagation();
public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value)
{
value = reference.GetBase();
return true;
}
public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value)
{
return value.IsNull() || value.IsUndefined();
}
public bool TryGetCallable(Engine engine, object reference, out JsValue value)
{
value = new ClrFunctionInstance(engine, "anonymous", (thisObj, values) => thisObj);
return true;
}
public bool CheckCoercible(JsValue value)
{
return true;
}
}
}

6
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -14,9 +14,9 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="2.11.58" /> <PackageReference Include="Jint" Version="3.0.0-beta-1427" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="NJsonSchema" Version="9.13.2" /> <PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

2
src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -15,7 +15,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" /> <ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" /> <PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

12
src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs

@ -170,14 +170,22 @@ namespace Squidex.Domain.Apps.Entities.Rules
{ {
x.AbsoluteExpirationRelativeToNow = UserCacheDuration; x.AbsoluteExpirationRelativeToNow = UserCacheDuration;
IUser user = null;
try try
{ {
return await userResolver.FindByIdOrEmailAsync(actor.Identifier); user = await userResolver.FindByIdOrEmailAsync(actor.Identifier);
} }
catch catch
{ {
return null; user = null;
} }
if (user == null && actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
user = new ClientUser(actor);
}
return user;
}); });
} }
} }

4
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs

@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
return action.Accept(visitor); return action.Accept(visitor);
} }
public Task<IEnumerable<ValidationError>> Visit(AssetChangedTrigger trigger) public Task<IEnumerable<ValidationError>> Visit(AssetChangedTriggerV2 trigger)
{ {
return Task.FromResult(Enumerable.Empty<ValidationError>()); return Task.FromResult(Enumerable.Empty<ValidationError>());
} }
public async Task<IEnumerable<ValidationError>> Visit(ContentChangedTrigger trigger) public async Task<IEnumerable<ValidationError>> Visit(ContentChangedTriggerV2 trigger)
{ {
if (trigger.Schemas != null) if (trigger.Schemas != null)
{ {

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

@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
this.log = log; this.log = log;
requestBlock = requestBlock =
new PartitionedActionBlock<IRuleEventEntity>(HandleAsync, x => x.Job.AggregateId.GetHashCode(), new PartitionedActionBlock<IRuleEventEntity>(HandleAsync, x => x.Job.ExecutionPartition,
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 });
} }

6
src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -15,12 +15,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GraphQL" Version="2.4.0" /> <PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.2"> <PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="NodaTime" Version="2.4.2" /> <PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

13
src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs

@ -6,15 +6,26 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Rules namespace Squidex.Domain.Apps.Events.Rules
{ {
[EventType(nameof(RuleCreated))] [EventType(nameof(RuleCreated))]
public sealed class RuleCreated : RuleEvent public sealed class RuleCreated : RuleEvent, IMigrated<IEvent>
{ {
public RuleTrigger Trigger { get; set; } public RuleTrigger Trigger { get; set; }
public RuleAction Action { get; set; } public RuleAction Action { get; set; }
public IEvent Migrate()
{
if (Trigger is IMigrated<RuleTrigger> migrated)
{
Trigger = migrated.Migrate();
}
return this;
}
} }
} }

13
src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs

@ -6,15 +6,26 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Rules namespace Squidex.Domain.Apps.Events.Rules
{ {
[EventType(nameof(RuleUpdated))] [EventType(nameof(RuleUpdated))]
public sealed class RuleUpdated : RuleEvent public sealed class RuleUpdated : RuleEvent, IMigrated<IEvent>
{ {
public RuleTrigger Trigger { get; set; } public RuleTrigger Trigger { get; set; }
public RuleAction Action { get; set; } public RuleAction Action { get; set; }
public IEvent Migrate()
{
if (Trigger is IMigrated<RuleTrigger> migrated)
{
Trigger = migrated.Migrate();
}
return this;
}
} }
} }

2
src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
<PackageReference Include="NodaTime" Version="2.4.2" /> <PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />

4
src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs

@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.EventSourcing
var payloadType = typeNameRegistry.GetType(eventData.Type); var payloadType = typeNameRegistry.GetType(eventData.Type);
var payloadObj = serializer.Deserialize<IEvent>(eventData.Payload, payloadType, stringConverter); var payloadObj = serializer.Deserialize<IEvent>(eventData.Payload, payloadType, stringConverter);
if (payloadObj is IMigratedEvent migratedEvent) if (payloadObj is IMigrated<IEvent> migratedEvent)
{ {
payloadObj = migratedEvent.Migrate(); payloadObj = migratedEvent.Migrate();
} }
@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
var eventPayload = envelope.Payload; var eventPayload = envelope.Payload;
if (migrate && eventPayload is IMigratedEvent migratedEvent) if (migrate && eventPayload is IMigrated<IEvent> migratedEvent)
{ {
eventPayload = migratedEvent.Migrate(); eventPayload = migratedEvent.Migrate();
} }

6
src/Squidex.Infrastructure/EventSourcing/IMigratedEvent.cs → src/Squidex.Infrastructure/IMigrated.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure
{ {
public interface IMigratedEvent public interface IMigrated<T>
{ {
IEvent Migrate(); T Migrate();
} }
} }

10
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -11,15 +11,15 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.2"> <PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NodaTime" Version="2.4.2" /> <PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0004" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0004" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

8
src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs

@ -23,22 +23,22 @@ namespace Squidex.Infrastructure.Tasks
get { return Task.WhenAll(workers.Select(x => x.Completion)); } get { return Task.WhenAll(workers.Select(x => x.Completion)); }
} }
public PartitionedActionBlock(Action<TInput> action, Func<TInput, int> partitioner) public PartitionedActionBlock(Action<TInput> action, Func<TInput, long> partitioner)
: this (action?.ToAsync(), partitioner, new ExecutionDataflowBlockOptions()) : this (action?.ToAsync(), partitioner, new ExecutionDataflowBlockOptions())
{ {
} }
public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, int> partitioner) public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, long> partitioner)
: this(action, partitioner, new ExecutionDataflowBlockOptions()) : this(action, partitioner, new ExecutionDataflowBlockOptions())
{ {
} }
public PartitionedActionBlock(Action<TInput> action, Func<TInput, int> partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) public PartitionedActionBlock(Action<TInput> action, Func<TInput, long> partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions)
: this(action?.ToAsync(), partitioner, dataflowBlockOptions) : this(action?.ToAsync(), partitioner, dataflowBlockOptions)
{ {
} }
public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, int> partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, long> partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions)
{ {
Guard.NotNull(action, nameof(action)); Guard.NotNull(action, nameof(action));
Guard.NotNull(partitioner, nameof(partitioner)); Guard.NotNull(partitioner, nameof(partitioner));

54
src/Squidex.Shared/Users/ClientUser.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
namespace Squidex.Shared.Users
{
public sealed class ClientUser : IUser
{
private readonly RefToken token;
private readonly List<Claim> claims;
public ClientUser(RefToken token)
{
Guard.NotNull(token, nameof(token));
this.token = token;
claims = new List<Claim>
{
new Claim(OpenIdClaims.ClientId, token.Identifier),
new Claim(SquidexClaimTypes.DisplayName, token.ToString())
};
}
public string Id
{
get { return token.Identifier; }
}
public string Email
{
get { return token.ToString(); }
}
public bool IsLocked
{
get { return false; }
}
public IReadOnlyList<Claim> Claims
{
get { return claims; }
}
}
}

4
src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs

@ -26,12 +26,12 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters
return properties.Accept(Instance); return properties.Accept(Instance);
} }
public RuleTriggerDto Visit(AssetChangedTrigger trigger) public RuleTriggerDto Visit(AssetChangedTriggerV2 trigger)
{ {
return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto());
} }
public RuleTriggerDto Visit(ContentChangedTrigger trigger) public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger)
{ {
var schemas = trigger.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedRuleTriggerSchemaDto())).ToArray(); var schemas = trigger.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedRuleTriggerSchemaDto())).ToArray();

21
src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs

@ -14,28 +14,13 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
public sealed class AssetChangedRuleTriggerDto : RuleTriggerDto public sealed class AssetChangedRuleTriggerDto : RuleTriggerDto
{ {
/// <summary> /// <summary>
/// Determines whether to handle the event when an asset is created. /// Javascript condition when to trigger.
/// </summary> /// </summary>
public bool SendCreate { get; set; } public string Condition { get; set; }
/// <summary>
/// Determines whether to handle the event when an asset is updated.
/// </summary>
public bool SendUpdate { get; set; }
/// <summary>
/// Determines whether to handle the event when an asset is renamed.
/// </summary>
public bool SendRename { get; set; }
/// <summary>
/// Determines whether to handle the event when an asset is deleted.
/// </summary>
public bool SendDelete { get; set; }
public override RuleTrigger ToTrigger() public override RuleTrigger ToTrigger()
{ {
return SimpleMapper.Map(this, new AssetChangedTrigger()); return SimpleMapper.Map(this, new AssetChangedTriggerV2());
} }
} }
} }

4
src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs

@ -29,9 +29,9 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
public override RuleTrigger ToTrigger() public override RuleTrigger ToTrigger()
{ {
var schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToReadOnlyCollection(); var schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchemaV2())).ToReadOnlyCollection();
return new ContentChangedTrigger { HandleAll = HandleAll, Schemas = schemas }; return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas };
} }
} }
} }

34
src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs

@ -17,38 +17,8 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
public Guid SchemaId { get; set; } public Guid SchemaId { get; set; }
/// <summary> /// <summary>
/// Determines whether to handle the event when a content is created. /// Javascript condition when to trigger.
/// </summary> /// </summary>
public bool SendCreate { get; set; } public string Condition { get; set; }
/// <summary>
/// Determines whether to handle the event when a content is updated.
/// </summary>
public bool SendUpdate { get; set; }
/// <summary>
/// Determines whether to handle the event when a content is deleted.
/// </summary>
public bool SendDelete { get; set; }
/// <summary>
/// Determines whether to handle the event when a content is published.
/// </summary>
public bool SendPublish { get; set; }
/// <summary>
/// Determines whether to handle the event when a content is unpublished.
/// </summary>
public bool SendUnpublish { get; set; }
/// <summary>
/// Determines whether to handle the event when a content is archived.
/// </summary>
public bool SendArchived { get; set; }
/// <summary>
/// Determines whether to handle the event when a content is restored.
/// </summary>
public bool SendRestore { get; set; }
} }
} }

16
src/Squidex/Squidex.csproj

@ -54,7 +54,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.2.1" /> <PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.0" /> <PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.0" />
<PackageReference Include="Ben.BlockingDetector" Version="0.0.3" /> <PackageReference Include="Ben.BlockingDetector" Version="0.0.3" />
<PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" /> <PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" />
@ -74,14 +74,14 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" /> <PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.Client" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.2" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" /> <PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="NJsonSchema" Version="9.13.2" /> <PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="NSwag.AspNetCore" Version="12.0.5" /> <PackageReference Include="NSwag.AspNetCore" Version="12.0.9" />
<PackageReference Include="OpenCover" Version="4.6.519" /> <PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" /> <PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" />
<PackageReference Include="OrleansDashboard" Version="2.1.3" /> <PackageReference Include="OrleansDashboard" Version="2.1.3" />

57
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html

@ -1,34 +1,33 @@
<div [formGroup]="triggerForm" class="form-horizontal"> <div [formGroup]="triggerForm" class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="form-check"> <label for="condition">Condition</label>
<input class="form-check-input" type="checkbox" id="sendCreate" formControlName="sendCreate" />
<label class="form-check-label" for="sendCreate"> <sqx-control-errors for="condition" [submitted]="triggerFormSubmitted"></sqx-control-errors>
created
</label> <textarea class="form-control code" id="condition" formControlName="condition"></textarea>
</div>
</div> </div>
<div class="form-group">
<div class="form-check"> <div class="help">
<input class="form-check-input" type="checkbox" id="sendUpdate" formControlName="sendUpdate" /> <h4>Conditions</h4>
<label class="form-check-label" for="sendUpdate">
updated <p>Conditions are javascript expressions that define when to trigger, for example:</p>
</label>
</div> <ul class="help-examples">
</div> <li class="help-example">
<div class="form-group"> Specific events:
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendRename" formControlName="sendRename" /> <pre>event.type == 'Created' || event.type == 'Updated'</pre>
<label class="form-check-label" for="sendRename"> </li>
renamed <li class="help-example">
</label> Large assets:
</div>
</div> <pre>event.fileSize > 100000000</pre>
<div class="form-group"> </li>
<div class="form-check"> <li class="help-example">
<input class="form-check-input" type="checkbox" id="sendDelete" formControlName="sendDelete" /> Images only:
<label class="form-check-label" for="sendDelete">
deleted <pre>event.isImage</pre>
</label> </li>
</div> </ul>
</div> </div>
</div> </div>

33
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.scss

@ -1,2 +1,33 @@
@import '_vars'; @import '_vars';
@import '_mixins'; @import '_mixins';
.code {
font-family: monospace;
}
textarea {
height: 100px;
}
pre {
background: $color-border;
border: 0;
padding: .25rem .5rem;
margin: 0;
}
.help {
& {
font-size: .9rem;
}
&-example {
margin-top: .375rem;
}
&-examples {
margin-top: 0;
margin-bottom: 0;
padding-left: 1.5rem;
}
}

13
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts

@ -24,16 +24,7 @@ export class AssetChangedTriggerComponent implements OnInit {
public triggerFormSubmitted = false; public triggerFormSubmitted = false;
public ngOnInit() { public ngOnInit() {
this.triggerForm.setControl('sendCreate', this.triggerForm.setControl('condition',
new FormControl(this.trigger.sendCreate || false)); new FormControl(this.trigger.condition || ''));
this.triggerForm.setControl('sendUpdate',
new FormControl(this.trigger.sendUpdate || false));
this.triggerForm.setControl('sendRename',
new FormControl(this.trigger.sendRename || false));
this.triggerForm.setControl('sendDelete',
new FormControl(this.trigger.sendDelete || false));
} }
} }

86
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -1,15 +1,8 @@
<ng-container *ngIf="!triggerForm.controls.handleAll.value"> <ng-container *ngIf="!triggerForm.controls.handleAll.value">
<table class="table table-middle table-sm table-fixed table-borderless"> <table class="table table-middle table-sm table-fixed table-borderless">
<colgroup> <colgroup>
<col style="width: 100%" /> <col style="width: 40%" />
<col style="width: 40px" /> <col style="width: 60%" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" />
<col style="width: 40px" /> <col style="width: 40px" />
</colgroup> </colgroup>
@ -17,29 +10,8 @@
<th> <th>
Schema Schema
</th> </th>
<th class="text-center"> <th>
<div class="rotated-label">All</div> Condition
</th>
<th class="text-center">
<div class="rotated-label">Created</div>
</th>
<th class="text-center">
<div class="rotated-label">Updated</div>
</th>
<th class="text-center">
<div class="rotated-label">Deleted</div>
</th>
<th class="text-center">
<div class="rotated-label">Published</div>
</th>
<th class="text-center">
<div class="rotated-label">Unpublished</div>
</th>
<th class="text-center">
<div class="rotated-label">Archived</div>
</th>
<th class="text-center">
<div class="rotated-label">Restored</div>
</th> </th>
<th></th> <th></th>
</tr> </tr>
@ -48,29 +20,8 @@
<td> <td>
<span class="truncate">{{schema.schema.displayName}}</span> <span class="truncate">{{schema.schema.displayName}}</span>
</td> </td>
<td class="text-center" title="Created"> <td class="text-center">
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" /> <input type="text" class="form-control code" [(ngModel)]="schema.condition" (blur)="updateValue()" />
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendCreate" (ngModelChange)="toggle(schema, 'sendCreate')" />
</td>
<td class="text-center" title="Updated">
<input type="checkbox" [ngModel]="schema.sendUpdate" (ngModelChange)="toggle(schema, 'sendUpdate')" />
</td>
<td class="text-center" title="Deleted">
<input type="checkbox" [ngModel]="schema.sendDelete" (ngModelChange)="toggle(schema, 'sendDelete')" />
</td>
<td class="text-center" title="Published">
<input type="checkbox" [ngModel]="schema.sendPublish" (ngModelChange)="toggle(schema, 'sendPublish')" />
</td>
<td class="text-center" title="Unpublished">
<input type="checkbox" [ngModel]="schema.sendUnpublish" (ngModelChange)="toggle(schema, 'sendUnpublish')" />
</td>
<td class="text-center" title="Archived">
<input type="checkbox" [ngModel]="schema.sendArchive" (ngModelChange)="toggle(schema, 'sendArchive')" />
</td>
<td class="text-center" title="Restored">
<input type="checkbox" [ngModel]="schema.sendRestore" (ngModelChange)="toggle(schema, 'sendRestore')" />
</td> </td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-link btn-secondary" (click)="removeSchema(schema)"> <button type="button" class="btn btn-link btn-secondary" (click)="removeSchema(schema)">
@ -91,6 +42,31 @@
<button type="submit" class="btn btn-success" [disabled]="!hasSchema">Add Schema</button> <button type="submit" class="btn btn-success" [disabled]="!hasSchema">Add Schema</button>
</form> </form>
</div> </div>
<div class="help">
<h4>Conditions</h4>
<p>Conditions are javascript expressions that define when to trigger, for example:</p>
<ul class="help-examples">
<li class="help-example">
Specific events:
<pre>event.type == 'Created' || event.type == 'Published'</pre>
</li>
<li class="help-example">
Content has value:
<pre>event.data.important.iv === true</pre>
</li>
<li class="help-example">
Updated by user:
<pre>user.email === 'user@squidex.io'</pre>
</li>
</ul>
</div>
</ng-container> </ng-container>
<div class="form-group" [formGroup]="triggerForm"> <div class="form-group" [formGroup]="triggerForm">

37
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss

@ -4,17 +4,40 @@
.section { .section {
border-top: 1px solid $color-border; border-top: 1px solid $color-border;
padding-top: 1rem; padding-top: 1rem;
padding-bottom: 0; padding-bottom: 1rem;
}
.table {
margin-top: 3.5rem;
} }
.form-check { .form-check {
margin-top: 1rem; margin-top: 1rem;
} }
.rotated-label { .code {
@include rotate(-45deg); font-family: monospace;
}
textarea {
height: 100px;
}
pre {
background: $color-border;
border: 0;
padding: .25rem .5rem;
margin: 0;
}
.help {
& {
font-size: .9rem;
}
&-example {
margin-top: .375rem;
}
&-examples {
margin-top: 0;
margin-bottom: 0;
padding-left: 1.5rem;
}
} }

134
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -16,14 +16,8 @@ import {
export interface TriggerSchemaForm { export interface TriggerSchemaForm {
schema: SchemaDto; schema: SchemaDto;
sendAll: boolean;
sendCreate: boolean; condition?: string;
sendUpdate: boolean;
sendDelete: boolean;
sendPublish: boolean;
sendUnpublish: boolean;
sendArchive: boolean;
sendRestore: boolean;
} }
@Component({ @Component({
@ -60,125 +54,45 @@ export class ContentChangedTriggerComponent implements OnInit {
this.triggerForm.setControl('handleAll', this.triggerForm.setControl('handleAll',
new FormControl(Types.isBoolean(this.trigger.handleAll) ? this.trigger.handleAll : false)); new FormControl(Types.isBoolean(this.trigger.handleAll) ? this.trigger.handleAll : false));
const triggerSchemas: any[] = (this.trigger.schemas = this.trigger.schemas || []); const schemas: TriggerSchemaForm[] = [];
this.triggerSchemas =
ImmutableArray.of(
triggerSchemas.map(triggerSchema => {
const schema = this.schemas.find(s => s.id === triggerSchema.schemaId);
if (schema) {
return this.updateSendAll({
schema: schema,
sendAll: false,
sendCreate: triggerSchema.sendCreate,
sendUpdate: triggerSchema.sendUpdate,
sendDelete: triggerSchema.sendDelete,
sendPublish: triggerSchema.sendPublish,
sendUnpublish: triggerSchema.sendUnpublish,
sendArchive: triggerSchema.sendArchive,
sendRestore: triggerSchema.sendRestore
});
} else {
return null;
}
}).filter(s => !!s).map(s => s!)).sortByStringAsc(s => s.schema.name);
this.schemasToAdd =
this.schemas.filter(schema =>
!triggerSchemas.find(s => s.schemaId === schema.id))
.sortByStringAsc(x => x.name);
this.schemaToAdd = this.schemasToAdd.at(0);
}
public removeSchema(schemaForm: TriggerSchemaForm) { if (this.trigger.schemas) {
this.triggerSchemas = this.triggerSchemas.remove(schemaForm); for (let triggerSchema of this.trigger.schemas) {
const schema = this.schemas.find(s => s.id === triggerSchema.schemaId);
this.updateValue();
this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name);
this.schemaToAdd = this.schemasToAdd.at(0);
}
public addSchema() { if (schema) {
this.triggerSchemas = schemas.push({ schema, condition: triggerSchema.condition });
this.triggerSchemas.push( }
this.updateSendAll({ }
schema: this.schemaToAdd, }
sendAll: false,
sendCreate: false,
sendUpdate: false,
sendDelete: false,
sendPublish: false,
sendUnpublish: false,
sendArchive: false,
sendRestore: false
})).sortByStringAsc(x => x.schema.name);
this.updateValue(); this.triggerSchemas = ImmutableArray.of(schemas).sortByStringAsc(s => s.schema.name);
this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); this.updateSchemaToAdd();
this.schemaToAdd = this.schemasToAdd.at(0);
} }
public toggle(schemaForm: TriggerSchemaForm, property: string) { public removeSchema(schemaForm: TriggerSchemaForm) {
const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); this.triggerSchemas = this.triggerSchemas.remove(schemaForm);
this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema);
this.updateValue(); this.updateValue();
this.updateSchemaToAdd();
} }
public toggleAll(schemaForm: TriggerSchemaForm) { public addSchema() {
const newSchema = this.updateAll(<any>{ schema: schemaForm.schema }, !schemaForm.sendAll); this.triggerSchemas = this.triggerSchemas.push({ schema: this.schemaToAdd }).sortByStringAsc(x => x.schema.name);
this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema);
this.updateValue(); this.updateValue();
this.updateSchemaToAdd();
} }
private updateValue() { public updateValue() {
const schemas = const schemas = this.triggerSchemas.values.map(s => ({ schemaId: s.schema.id, condition: s.condition }));
this.triggerSchemas.values.map(s => {
return {
schemaId: s.schema.id,
sendCreate: s.sendCreate,
sendUpdate: s.sendUpdate,
sendDelete: s.sendDelete,
sendPublish: s.sendPublish,
sendUnpublish: s.sendUnpublish,
sendArchive: s.sendArchive,
sendRestore: s.sendRestore
};
});
this.triggerForm.controls['schemas'].setValue(schemas); this.triggerForm.controls['schemas'].setValue(schemas);
} }
private updateAll(schemaForm: TriggerSchemaForm, value: boolean): TriggerSchemaForm { private updateSchemaToAdd() {
schemaForm.sendAll = value; this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortByStringAsc(x => x.name);
schemaForm.sendCreate = value; this.schemaToAdd = this.schemasToAdd.at(0);
schemaForm.sendUpdate = value;
schemaForm.sendDelete = value;
schemaForm.sendPublish = value;
schemaForm.sendUnpublish = value;
schemaForm.sendArchive = value;
schemaForm.sendRestore = value;
return schemaForm;
}
private updateSendAll(schemaForm: TriggerSchemaForm): TriggerSchemaForm {
schemaForm.sendAll =
schemaForm.sendCreate &&
schemaForm.sendUpdate &&
schemaForm.sendDelete &&
schemaForm.sendPublish &&
schemaForm.sendUnpublish &&
schemaForm.sendArchive &&
schemaForm.sendRestore;
return schemaForm;
} }
} }

6
tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs

@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Model.Rules
.Select(x => new[] { x }) .Select(x => new[] { x })
.ToList(); .ToList();
private readonly Rule rule_0 = new Rule(new ContentChangedTrigger(), new TestAction1()); private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1());
public sealed class OtherTrigger : RuleTrigger public sealed class OtherTrigger : RuleTrigger
{ {
@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.Model.Rules
[Fact] [Fact]
public void Should_create_with_trigger_and_action() public void Should_create_with_trigger_and_action()
{ {
var ruleTrigger = new ContentChangedTrigger(); var ruleTrigger = new ContentChangedTriggerV2();
var ruleAction = new TestAction1(); var ruleAction = new TestAction1();
var newRule = new Rule(ruleTrigger, ruleAction); var newRule = new Rule(ruleTrigger, ruleAction);
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Model.Rules
[Fact] [Fact]
public void Should_replace_trigger_when_updating() public void Should_replace_trigger_when_updating()
{ {
var newTrigger = new ContentChangedTrigger(); var newTrigger = new ContentChangedTriggerV2();
var rule_1 = rule_0.Update(newTrigger); var rule_1 = rule_0.Update(newTrigger);

233
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -13,6 +13,7 @@ using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -32,13 +33,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
public RuleEventFormatterTests() public RuleEventFormatterTests()
{ {
A.CallTo(() => user.Id)
.Returns("123");
A.CallTo(() => user.Email) A.CallTo(() => user.Email)
.Returns("me@email.com"); .Returns("me@email.com");
A.CallTo(() => user.Claims) A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") }); .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") });
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator); A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId))
.Returns("content-url");
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine());
} }
[Fact] [Fact]
@ -69,91 +76,120 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Contains("MyEventName", result); Assert.Contains("MyEventName", result);
} }
[Fact] [Theory]
public void Should_replace_app_information_from_event() [InlineData("Name $APP_NAME has id $APP_ID")]
[InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")]
public void Should_replace_app_information_from_event(string script)
{ {
var @event = new EnrichedContentEvent { AppId = appId }; var @event = new EnrichedContentEvent { AppId = appId };
var result = sut.Format("Name $APP_NAME has id $APP_ID", @event); var result = sut.Format(script, @event);
Assert.Equal($"Name my-app has id {appId.Id}", result); Assert.Equal($"Name my-app has id {appId.Id}", result);
} }
[Fact] [Theory]
public void Should_replace_schema_information_from_event() [InlineData("Name $SCHEMA_NAME has id $SCHEMA_ID")]
[InlineData("Script(`Name ${event.schemaId.name} has id ${event.schemaId.id}`)")]
public void Should_replace_schema_information_from_event(string script)
{ {
var @event = new EnrichedContentEvent { SchemaId = schemaId }; var @event = new EnrichedContentEvent { SchemaId = schemaId };
var result = sut.Format("Name $SCHEMA_NAME has id $SCHEMA_ID", @event); var result = sut.Format(script, @event);
Assert.Equal($"Name my-schema has id {schemaId.Id}", result); Assert.Equal($"Name my-schema has id {schemaId.Id}", result);
} }
[Fact] [Theory]
public void Should_replace_timestamp_information_from_event() [InlineData("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME")]
[InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")]
public void Should_replace_timestamp_information_from_event(string script)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var envelope = new EnrichedContentEvent { Timestamp = Instant.FromDateTimeUtc(now) }; var envelope = new EnrichedContentEvent { Timestamp = Instant.FromDateTimeUtc(now) };
var result = sut.Format("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope); var result = sut.Format(script, envelope);
Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result); Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result);
} }
[Fact] [Theory]
public void Should_format_email_and_display_name_from_user() [InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]
public void Should_format_email_and_display_name_from_user(string script)
{ {
var @event = new EnrichedContentEvent { User = user, Actor = new RefToken(RefTokenType.Subject, "123") }; var @event = new EnrichedContentEvent { User = user };
var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event); var result = sut.Format(script, @event);
Assert.Equal("From me (me@email.com)", result); Assert.Equal("From me (me@email.com, 123)", result);
} }
[Fact] [Theory]
public void Should_return_undefined_if_user_is_not_found() [InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]
public void Should_return_null_if_user_is_not_found(string script)
{ {
var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Subject, "123") }; var @event = new EnrichedContentEvent();
var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event); var result = sut.Format(script, @event);
Assert.Equal("From UNDEFINED (UNDEFINED)", result); Assert.Equal("From null (null, null)", result);
} }
[Fact] [Theory]
public void Should_format_email_and_display_name_from_client() [InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]
public void Should_format_email_and_display_name_from_client(string script)
{ {
var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") }; var @event = new EnrichedContentEvent { User = new ClientUser(new RefToken(RefTokenType.Client, "android")) };
var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event); var result = sut.Format(script, @event);
Assert.Equal("From client:android (client:android)", result); Assert.Equal("From client:android (client:android, android)", result);
} }
[Fact] [Theory]
public void Should_replace_content_url_from_event() [InlineData("Go to $CONTENT_URL")]
[InlineData("Script(`Go to ${contentUrl()}`)")]
public void Should_replace_content_url_from_event(string script)
{ {
var url = "http://content"; var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId };
A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId)) var result = sut.Format(script, @event);
.Returns(url);
var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; Assert.Equal("Go to content-url", result);
}
var result = sut.Format("Go to $CONTENT_URL", @event); [Theory]
[InlineData("Go to $CONTENT_URL")]
[InlineData("Script(`Go to ${contentUrl()}`)")]
public void Should_format_content_url_when_not_found(string script)
{
Assert.Equal("Go to null", sut.Format(script, new EnrichedAssetEvent()));
}
Assert.Equal($"Go to {url}", result); [Theory]
[InlineData("$CONTENT_ACTION")]
[InlineData("Script(`${event.type}`)")]
public void Should_format_content_actions_when_found(string script)
{
Assert.Equal("Created", sut.Format(script, new EnrichedContentEvent { Type = EnrichedContentEventType.Created }));
} }
[Fact] [Theory]
public void Should_format_content_url_when_not_found() [InlineData("$CONTENT_ACTION")]
[InlineData("Script(contentAction())")]
public void Should_null_when_content_action_not_found(string script)
{ {
Assert.Equal("UNDEFINED", sut.Format("$CONTENT_URL", new EnrichedAssetEvent())); Assert.Equal("null", sut.Format(script, new EnrichedAssetEvent()));
} }
[Fact] [Theory]
public void Should_return_undefined_when_field_not_found() [InlineData("$CONTENT_DATA.country.iv")]
[InlineData("Script(`${event.data.country.iv}`)")]
public void Should_return_null_when_field_not_found(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -164,13 +200,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin")) .AddValue("iv", "Berlin"))
}; };
var result = sut.Format("$CONTENT_DATA.country.iv", @event); var result = sut.Format(script, @event);
Assert.Equal("UNDEFINED", result); Assert.Equal("null", result);
} }
[Fact] [Theory]
public void Should_return_undefined_when_partition_not_found() [InlineData("$CONTENT_DATA.city.de")]
[InlineData("Script(`${event.data.country.iv}`)")]
public void Should_return_null_when_partition_not_found(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -181,13 +219,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin")) .AddValue("iv", "Berlin"))
}; };
var result = sut.Format("$CONTENT_DATA.city.de", @event); var result = sut.Format(script, @event);
Assert.Equal("UNDEFINED", result); Assert.Equal("null", result);
} }
[Fact] [Theory]
public void Should_return_undefined_when_array_item_not_found() [InlineData("$CONTENT_DATA.city.iv.10")]
[InlineData("Script(`${event.data.country.de[10]}`)")]
public void Should_return_null_when_array_item_not_found(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -198,13 +238,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Array())) .AddValue("iv", JsonValue.Array()))
}; };
var result = sut.Format("$CONTENT_DATA.city.de.10", @event); var result = sut.Format(script, @event);
Assert.Equal("UNDEFINED", result); Assert.Equal("null", result);
} }
[Fact] [Theory]
public void Should_return_undefined_when_property_not_found() [InlineData("$CONTENT_DATA.city.de.Name")]
[InlineData("Script(`${event.data.city.de.Location}`)")]
public void Should_return_null_when_property_not_found(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -215,13 +257,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) .AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
}; };
var result = sut.Format("$CONTENT_DATA.city.de.Name", @event); var result = sut.Format(script, @event);
Assert.Equal("UNDEFINED", result); Assert.Equal("null", result);
} }
[Fact] [Theory]
public void Should_return_plain_value_when_found() [InlineData("$CONTENT_DATA.city.iv")]
[InlineData("Script(`${event.data.city.iv}`)")]
public void Should_return_plain_value_when_found(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -232,13 +276,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin")) .AddValue("iv", "Berlin"))
}; };
var result = sut.Format("$CONTENT_DATA.city.iv", @event); var result = sut.Format(script, @event);
Assert.Equal("Berlin", result); Assert.Equal("Berlin", result);
} }
[Fact] [Theory]
public void Should_return_plain_value_when_found_from_update_event() [InlineData("$CONTENT_DATA.city.iv.0")]
[InlineData("Script(`${event.data.city.iv[0]}`)")]
public void Should_return_plain_value_from_array_when_found(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -246,33 +292,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData() new NamedContentData()
.AddField("city", .AddField("city",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", "Berlin")) .AddValue("iv", JsonValue.Array(
"Berlin")))
}; };
var result = sut.Format("$CONTENT_DATA.city.iv", @event); var result = sut.Format(script, @event);
Assert.Equal("Berlin", result); Assert.Equal("Berlin", result);
} }
[Fact] [Theory]
public void Should_return_undefined_when_null() [InlineData("$CONTENT_DATA.city.iv.name")]
{ [InlineData("Script(`${event.data.city.iv.name}`)")]
var @event = new EnrichedContentEvent public void Should_return_plain_value_from_object_when_found(string script)
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Null))
};
var result = sut.Format("$CONTENT_DATA.city.iv", @event);
Assert.Equal("UNDEFINED", result);
}
[Fact]
public void Should_return_string_when_object()
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -283,31 +315,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) .AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
}; };
var result = sut.Format("$CONTENT_DATA.city.iv", @event); var result = sut.Format(script, @event);
Assert.Equal("{\"name\":\"Berlin\"}", result);
}
[Fact]
public void Should_return_plain_value_from_array_when_found()
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
"Berlin")))
};
var result = sut.Format("$CONTENT_DATA.city.iv.0", @event);
Assert.Equal("Berlin", result); Assert.Equal("Berlin", result);
} }
[Fact] [Theory]
public void Should_return_plain_value_from_object_when_found() [InlineData("$CONTENT_DATA.city.iv")]
[InlineData("Script(`${JSON.stringify(event.data.city.iv)}`)")]
public void Should_return_json_string_when_object(string script)
{ {
var @event = new EnrichedContentEvent var @event = new EnrichedContentEvent
{ {
@ -318,21 +334,20 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Object().Add("name", "Berlin"))) .AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
}; };
var result = sut.Format("$CONTENT_DATA.city.iv.name", @event); var result = sut.Format(script, @event);
Assert.Equal("Berlin", result); Assert.Equal("{\"name\":\"Berlin\"}", result);
} }
[Fact] [Theory]
public void Should_format_content_actions_when_found() [InlineData("Script(`From ${event.actor}`)")]
public void Should_format_actor(string script)
{ {
Assert.Equal("created", sut.Format("$CONTENT_ACTION", new EnrichedContentEvent { Type = EnrichedContentEventType.Created })); var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") };
}
[Fact] var result = sut.Format(script, @event);
public void Should_format_content_actions_when_not_found()
{ Assert.Equal("From client:android", result);
Assert.Equal("UNDEFINED", sut.Format("$CONTENT_ACTION", new EnrichedAssetEvent()));
} }
} }
} }

12
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.Returns(typeof(ValidData)); .Returns(typeof(ValidData));
A.CallTo(() => ruleTriggerHandler.TriggerType) A.CallTo(() => ruleTriggerHandler.TriggerType)
.Returns(typeof(ContentChangedTrigger)); .Returns(typeof(ContentChangedTriggerV2));
sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, eventEnricher, TestUtils.DefaultSerializer, clock, typeNameRegistry); sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, eventEnricher, TestUtils.DefaultSerializer, clock, typeNameRegistry);
} }
@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Fact] [Fact]
public async Task Should_not_create_job_if_no_action_handler_registered() public async Task Should_not_create_job_if_no_action_handler_registered()
{ {
var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction()); var ruleConfig = new Rule(new ContentChangedTriggerV2(), new InvalidAction());
var ruleEnvelope = Envelope.Create(new ContentCreated()); var ruleEnvelope = Envelope.Create(new ContentCreated());
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
@ -132,7 +132,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
var ruleConfig = ValidRule(); var ruleConfig = ValidRule();
var ruleEnvelope = Envelope.Create(new ContentCreated()); var ruleEnvelope = Envelope.Create(new ContentCreated());
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger)) A.CallTo(() => ruleTriggerHandler.Triggers(A<EnrichedEvent>.Ignored, ruleConfig.Trigger))
.Returns(false); .Returns(false);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => clock.GetCurrentInstant()) A.CallTo(() => clock.GetCurrentInstant())
.Returns(now); .Returns(now);
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger)) A.CallTo(() => ruleTriggerHandler.Triggers(A<EnrichedEvent>.Ignored, ruleConfig.Trigger))
.Returns(true); .Returns(true);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, ruleConfig.Action)) A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, ruleConfig.Action))
@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => clock.GetCurrentInstant()) A.CallTo(() => clock.GetCurrentInstant())
.Returns(now); .Returns(now);
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger)) A.CallTo(() => ruleTriggerHandler.Triggers(A<EnrichedEvent>.Ignored, ruleConfig.Trigger))
.Returns(true); .Returns(true);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, ruleConfig.Action)) A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, ruleConfig.Action))
@ -260,7 +260,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
private static Rule ValidRule() private static Rule ValidRule()
{ {
return new Rule(new ContentChangedTrigger(), new ValidAction()); return new Rule(new ContentChangedTriggerV2(), new ValidAction());
} }
} }
} }

92
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs

@ -5,56 +5,80 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using FakeItEasy;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure.EventSourcing;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
{ {
public class AssetChangedTriggerTests public class AssetChangedTriggerTests
{ {
private readonly IRuleTriggerHandler sut = new AssetChangedTriggerHandler(); private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IRuleTriggerHandler sut;
public static IEnumerable<object[]> TestData public AssetChangedTriggerTests()
{ {
get sut = new AssetChangedTriggerHandler(scriptEngine);
{
return new[]
{
new object[] { 0, 1, 1, 1, 1, new RuleCreated() },
new object[] { 1, 1, 0, 0, 0, new AssetCreated() },
new object[] { 0, 0, 0, 0, 0, new AssetCreated() },
new object[] { 1, 0, 1, 0, 0, new AssetUpdated() },
new object[] { 0, 0, 0, 0, 0, new AssetUpdated() },
new object[] { 1, 0, 0, 1, 0, new AssetRenamed() },
new object[] { 0, 0, 0, 0, 0, new AssetRenamed() },
new object[] { 1, 0, 0, 0, 1, new AssetDeleted() },
new object[] { 0, 0, 0, 0, 0, new AssetDeleted() }
};
}
} }
[Theory] [Fact]
[MemberData(nameof(TestData))] public void Should_trigger_when_condition_is_null()
public void Should_return_result_depending_on_event(int expected, int sendCreate, int sendUpdate, int sendRename, int sendDelete, AppEvent @event)
{ {
var trigger = new AssetChangedTrigger var trigger = new AssetChangedTriggerV2();
{
SendCreate = sendCreate == 1,
SendUpdate = sendUpdate == 1,
SendRename = sendRename == 1,
SendDelete = sendDelete == 1
};
var result = sut.Triggers(new Envelope<AppEvent>(@event), trigger); var result = sut.Triggers(new EnrichedAssetEvent(), trigger);
Assert.Equal(expected == 1, result); Assert.True(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public void Should_trigger_when_condition_is_empty()
{
var trigger = new AssetChangedTriggerV2 { Condition = string.Empty };
var result = sut.Triggers(new EnrichedAssetEvent(), trigger);
Assert.True(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public void Should_trigger_when_condition_matchs()
{
var trigger = new AssetChangedTriggerV2 { Condition = "true" };
var @event = new EnrichedAssetEvent();
A.CallTo(() => scriptEngine.Evaluate("event", @event, trigger.Condition))
.Returns(true);
var result = sut.Triggers(@event, trigger);
Assert.True(result);
}
[Fact]
public void Should_not_trigger_when_condition_does_not_matchs()
{
var trigger = new AssetChangedTriggerV2 { Condition = "false" };
var @event = new EnrichedAssetEvent();
A.CallTo(() => scriptEngine.Evaluate("event", @event, trigger.Condition))
.Returns(false);
var result = sut.Triggers(@event, trigger);
Assert.False(result);
} }
} }
} }

193
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs

@ -7,17 +7,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents; using System.Collections.ObjectModel;
using FakeItEasy;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.EventSourcing;
using Xunit; using Xunit;
#pragma warning disable SA1401 // Fields must be private #pragma warning disable SA1401 // Fields must be private
@ -26,82 +23,164 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
{ {
public class ContentChangedTriggerTests public class ContentChangedTriggerTests
{ {
private readonly IRuleTriggerHandler sut = new ContentChangedTriggerHandler(); private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IRuleTriggerHandler sut;
private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1");
private static readonly NamedId<Guid> SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); private static readonly NamedId<Guid> SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2");
public static IEnumerable<object[]> TestData = new[] public ContentChangedTriggerTests()
{ {
new object[] { 0, 1, 1, 1, 1, 0, 0, 0, new RuleCreated() }, sut = new ContentChangedTriggerHandler(scriptEngine);
new object[] { 0, 1, 1, 1, 1, 0, 0, 0, new ContentCreated { SchemaId = SchemaNonMatch } }, }
new object[] { 1, 1, 0, 0, 0, 0, 0, 0, new ContentCreated { SchemaId = SchemaMatch } },
new object[] { 0, 0, 0, 0, 0, 0, 0, 0, new ContentCreated { SchemaId = SchemaMatch } },
new object[] { 1, 0, 1, 0, 0, 0, 0, 0, new ContentUpdated { SchemaId = SchemaMatch } },
new object[] { 0, 0, 0, 0, 0, 0, 0, 0, new ContentUpdated { SchemaId = SchemaMatch } },
new object[] { 1, 0, 0, 1, 0, 0, 0, 0, new ContentDeleted { SchemaId = SchemaMatch } },
new object[] { 0, 0, 0, 0, 0, 0, 0, 0, new ContentDeleted { SchemaId = SchemaMatch } },
new object[] { 1, 1, 1, 1, 0, 0, 1, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Archived } },
new object[] { 0, 1, 1, 1, 0, 0, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Archived } },
new object[] { 1, 0, 0, 0, 0, 0, 0, 1, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Restored } },
new object[] { 0, 0, 0, 0, 0, 0, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Restored } },
new object[] { 1, 0, 0, 0, 1, 0, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Published } },
new object[] { 0, 0, 0, 0, 0, 0, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Published } },
new object[] { 1, 1, 1, 1, 0, 1, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Unpublished } },
new object[] { 0, 1, 1, 1, 0, 0, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Change = StatusChange.Unpublished } },
new object[] { 0, 1, 1, 1, 1, 0, 0, 0, new SchemaCreated { SchemaId = SchemaNonMatch } }
};
[Fact] [Fact]
public void Should_return_false_when_trigger_contains_no_schemas() public void Should_not_trigger_when_o_contains_no_schemas()
{ {
var trigger = new ContentChangedTrigger(); var trigger = new ContentChangedTriggerV2();
var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
var result = sut.Triggers(new Envelope<AppEvent>(new ContentCreated()), trigger); var result = sut.Triggers(@event, trigger);
Assert.False(result); Assert.False(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public void Should_return_true_when_cathing_all_events() public void Should_trigger_when_cathing_all_events()
{ {
var trigger = new ContentChangedTrigger { HandleAll = true }; var trigger = new ContentChangedTriggerV2 { HandleAll = true };
var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
var result = sut.Triggers(new Envelope<AppEvent>(new ContentCreated()), trigger); var result = sut.Triggers(@event, trigger);
Assert.True(result); Assert.True(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
} }
[Theory] [Fact]
[MemberData(nameof(TestData))] public void Should_trigger_when_condition_is_null()
public void Should_return_result_depending_on_event(int expected,
int sendCreate,
int sendUpdate,
int sendDelete,
int sendPublish,
int sendUnpublish,
int sendArchive,
int sendRestore,
AppEvent @event)
{ {
var trigger = new ContentChangedTrigger var trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Create( Schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(new List<ContentChangedTriggerSchemaV2>
new ContentChangedTriggerSchema {
new ContentChangedTriggerSchemaV2
{ {
SendCreate = sendCreate == 1,
SendUpdate = sendUpdate == 1,
SendDelete = sendDelete == 1,
SendPublish = sendPublish == 1,
SendUnpublish = sendUnpublish == 1,
SendArchived = sendArchive == 1,
SendRestore = sendRestore == 1,
SchemaId = SchemaMatch.Id SchemaId = SchemaMatch.Id
}) }
})
}; };
var result = sut.Triggers(new Envelope<AppEvent>(@event), trigger); var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
var result = sut.Triggers(@event, trigger);
Assert.True(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public void Should_not_trigger_when_schema_id_does_not_match()
{
var trigger = new ContentChangedTriggerV2
{
Schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(new List<ContentChangedTriggerSchemaV2>
{
new ContentChangedTriggerSchemaV2
{
SchemaId = SchemaNonMatch.Id
}
})
};
var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
var result = sut.Triggers(@event, trigger);
Assert.False(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public void Should_trigger_when_condition_is_empty()
{
var trigger = new ContentChangedTriggerV2
{
Schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(new List<ContentChangedTriggerSchemaV2>
{
new ContentChangedTriggerSchemaV2
{
SchemaId = SchemaMatch.Id, Condition = string.Empty
}
})
};
Assert.Equal(expected == 1, result); var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
var result = sut.Triggers(@event, trigger);
Assert.True(result);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public void Should_trigger_when_condition_matchs()
{
var trigger = new ContentChangedTriggerV2
{
Schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(new List<ContentChangedTriggerSchemaV2>
{
new ContentChangedTriggerSchemaV2
{
SchemaId = SchemaMatch.Id, Condition = "true"
}
})
};
var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
A.CallTo(() => scriptEngine.Evaluate("event", @event, "true"))
.Returns(true);
var result = sut.Triggers(@event, trigger);
Assert.True(result);
}
[Fact]
public void Should_not_trigger_when_condition_does_not_matchs()
{
var trigger = new ContentChangedTriggerV2
{
Schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(new List<ContentChangedTriggerSchemaV2>
{
new ContentChangedTriggerSchemaV2
{
SchemaId = SchemaMatch.Id, Condition = "false"
}
})
};
var @event = new EnrichedContentEvent { SchemaId = SchemaMatch };
A.CallTo(() => scriptEngine.Evaluate("event", @event, "false"))
.Returns(false);
var result = sut.Triggers(@event, trigger);
Assert.False(result);
} }
} }
} }

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs

@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{ {
var engine = new Engine(); var engine = new Engine();
engine.SetValue("user", new JintUser(engine, new ClaimsPrincipal(new[] { identity }))); engine.SetValue("user", JintUser.Create(engine, new ClaimsPrincipal(new[] { identity })));
return engine.Execute(script).GetCompletionValue().ToObject(); return engine.Execute(script).GetCompletionValue().ToObject();
} }

2
tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -11,7 +11,7 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" /> <PackageReference Include="FakeItEasy" Version="4.9.2" />
<PackageReference Include="FluentAssertions" Version="5.5.3" /> <PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

14
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
public class GuardRuleTests public class GuardRuleTests
{ {
private readonly Uri validUrl = new Uri("https://squidex.io"); private readonly Uri validUrl = new Uri("https://squidex.io");
private readonly Rule rule_0 = new Rule(new ContentChangedTrigger(), new TestAction()); private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction());
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
@ -60,9 +60,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{ {
var command = CreateCommand(new CreateRule var command = CreateCommand(new CreateRule
{ {
Trigger = new ContentChangedTrigger Trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>() Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
}, },
Action = null Action = null
}); });
@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{ {
var command = CreateCommand(new CreateRule var command = CreateCommand(new CreateRule
{ {
Trigger = new ContentChangedTrigger Trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>() Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
}, },
Action = new TestAction Action = new TestAction
{ {
@ -103,9 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{ {
var command = new UpdateRule var command = new UpdateRule
{ {
Trigger = new ContentChangedTrigger Trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>() Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
}, },
Action = new TestAction Action = new TestAction
{ {

14
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs

@ -26,10 +26,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false))
.Returns(Task.FromResult<ISchemaEntity>(null)); .Returns(Task.FromResult<ISchemaEntity>(null));
var trigger = new ContentChangedTrigger var trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Create( Schemas = ReadOnlyCollection.Create(
new ContentChangedTriggerSchema() new ContentChangedTriggerSchemaV2()
) )
}; };
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
[Fact] [Fact]
public async Task Should_not_add_error_if_schemas_is_null() public async Task Should_not_add_error_if_schemas_is_null()
{ {
var trigger = new ContentChangedTrigger(); var trigger = new ContentChangedTriggerV2();
var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider);
@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
[Fact] [Fact]
public async Task Should_not_add_error_if_schemas_is_empty() public async Task Should_not_add_error_if_schemas_is_empty()
{ {
var trigger = new ContentChangedTrigger var trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>() Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
}; };
var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider);
@ -67,10 +67,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>()); .Returns(A.Fake<ISchemaEntity>());
var trigger = new ContentChangedTrigger var trigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Create( Schemas = ReadOnlyCollection.Create(
new ContentChangedTriggerSchema() new ContentChangedTriggerSchemaV2()
) )
}; };

6
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -70,9 +70,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
{ {
var @event = Envelope.Create(new ContentCreated { AppId = appId }); var @event = Envelope.Create(new ContentCreated { AppId = appId });
var rule1 = new Rule(new ContentChangedTrigger(), new TestAction { Url = new Uri("https://squidex.io") }); var rule1 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });
var rule2 = new Rule(new ContentChangedTrigger(), new TestAction { Url = new Uri("https://squidex.io") }); var rule2 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });
var rule3 = new Rule(new ContentChangedTrigger(), new TestAction { Url = new Uri("https://squidex.io") }); var rule3 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") });
var job1 = new RuleJob { Created = now }; var job1 = new RuleJob { Created = now };
var job2 = new RuleJob { Created = now }; var job2 = new RuleJob { Created = now };

8
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs

@ -189,9 +189,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
private static CreateRule MakeCreateCommand() private static CreateRule MakeCreateCommand()
{ {
var newTrigger = new ContentChangedTrigger var newTrigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>() Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
}; };
var newAction = new TestAction var newAction = new TestAction
@ -204,9 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
private static UpdateRule MakeUpdateCommand() private static UpdateRule MakeUpdateCommand()
{ {
var newTrigger = new ContentChangedTrigger var newTrigger = new ContentChangedTriggerV2
{ {
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>() Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
}; };
var newAction = new TestAction var newAction = new TestAction

2
tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -16,7 +16,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj" /> <ProjectReference Include="..\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" /> <PackageReference Include="FakeItEasy" Version="4.9.2" />
<PackageReference Include="FluentAssertions" Version="5.5.3" /> <PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="GraphQL" Version="2.4.0" /> <PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />

2
tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -12,7 +12,7 @@
<ProjectReference Include="..\..\src\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\..\src\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" /> <PackageReference Include="FakeItEasy" Version="4.9.2" />
<PackageReference Include="FluentAssertions" Version="5.5.3" /> <PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

2
tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs

@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
public class DefaultEventDataFormatterTests public class DefaultEventDataFormatterTests
{ {
public sealed class MyOldEvent : IEvent, IMigratedEvent public sealed class MyOldEvent : IEvent, IMigrated<IEvent>
{ {
public string MyProperty { get; set; } public string MyProperty { get; set; }

2
tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -12,7 +12,7 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" /> <PackageReference Include="FakeItEasy" Version="4.9.2" />
<PackageReference Include="FluentAssertions" Version="5.5.3" /> <PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="Google.Cloud.Storage.V1" Version="2.2.1" /> <PackageReference Include="Google.Cloud.Storage.V1" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />

4
tests/Squidex.Tests/Squidex.Tests.csproj

@ -11,11 +11,11 @@
<ProjectReference Include="..\..\src\Squidex\Squidex.csproj" /> <ProjectReference Include="..\..\src\Squidex\Squidex.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" /> <PackageReference Include="FakeItEasy" Version="4.9.2" />
<PackageReference Include="IdentityServer4" Version="2.3.2" /> <PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.3.0" /> <PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NJsonSchema" Version="9.13.2" /> <PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />

3
tools/Migrate_01/OldEvents/AppClientChanged.cs

@ -7,6 +7,7 @@
using System; using System;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -14,7 +15,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(AppClientChanged))] [EventType(nameof(AppClientChanged))]
[Obsolete] [Obsolete]
public sealed class AppClientChanged : AppEvent, IMigratedEvent public sealed class AppClientChanged : AppEvent, IMigrated<IEvent>
{ {
public string Id { get; set; } public string Id { get; set; }

3
tools/Migrate_01/OldEvents/AppClientUpdated.cs

@ -8,6 +8,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated; using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated;
@ -16,7 +17,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(AppClientUpdated))] [EventType(nameof(AppClientUpdated))]
[Obsolete] [Obsolete]
public sealed class AppClientUpdated : AppEvent, IMigratedEvent public sealed class AppClientUpdated : AppEvent, IMigrated<IEvent>
{ {
public string Id { get; set; } public string Id { get; set; }

3
tools/Migrate_01/OldEvents/AppContributorAssigned.cs

@ -8,6 +8,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using AppContributorAssignedV2 = Squidex.Domain.Apps.Events.Apps.AppContributorAssigned; using AppContributorAssignedV2 = Squidex.Domain.Apps.Events.Apps.AppContributorAssigned;
@ -16,7 +17,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(AppContributorAssigned))] [EventType(nameof(AppContributorAssigned))]
[Obsolete] [Obsolete]
public sealed class AppContributorAssigned : AppEvent, IMigratedEvent public sealed class AppContributorAssigned : AppEvent, IMigrated<IEvent>
{ {
public string ContributorId { get; set; } public string ContributorId { get; set; }

3
tools/Migrate_01/OldEvents/ContentArchived.cs

@ -8,6 +8,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -15,7 +16,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(ContentArchived))] [EventType(nameof(ContentArchived))]
[Obsolete] [Obsolete]
public sealed class ContentArchived : ContentEvent, IMigratedEvent public sealed class ContentArchived : ContentEvent, IMigrated<IEvent>
{ {
public IEvent Migrate() public IEvent Migrate()
{ {

3
tools/Migrate_01/OldEvents/ContentPublished.cs

@ -8,6 +8,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -15,7 +16,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(ContentPublished))] [EventType(nameof(ContentPublished))]
[Obsolete] [Obsolete]
public sealed class ContentPublished : ContentEvent, IMigratedEvent public sealed class ContentPublished : ContentEvent, IMigrated<IEvent>
{ {
public IEvent Migrate() public IEvent Migrate()
{ {

3
tools/Migrate_01/OldEvents/ContentRestored.cs

@ -8,6 +8,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -15,7 +16,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(ContentRestored))] [EventType(nameof(ContentRestored))]
[Obsolete] [Obsolete]
public sealed class ContentRestored : ContentEvent, IMigratedEvent public sealed class ContentRestored : ContentEvent, IMigrated<IEvent>
{ {
public IEvent Migrate() public IEvent Migrate()
{ {

3
tools/Migrate_01/OldEvents/ContentUnpublished.cs

@ -8,6 +8,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -15,7 +16,7 @@ namespace Migrate_01.OldEvents
{ {
[EventType(nameof(ContentUnpublished))] [EventType(nameof(ContentUnpublished))]
[Obsolete] [Obsolete]
public sealed class ContentUnpublished : ContentEvent, IMigratedEvent public sealed class ContentUnpublished : ContentEvent, IMigrated<IEvent>
{ {
public IEvent Migrate() public IEvent Migrate()
{ {

71
tools/Migrate_01/OldTriggers/AssetChangedTrigger.cs

@ -0,0 +1,71 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure;
namespace Migrate_01.OldTriggers
{
[TypeName(nameof(AssetChangedTrigger))]
public sealed class AssetChangedTrigger : RuleTrigger, IMigrated<RuleTrigger>
{
public bool SendCreate { get; set; }
public bool SendUpdate { get; set; }
public bool SendRename { get; set; }
public bool SendDelete { get; set; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{
throw new NotSupportedException();
}
public RuleTrigger Migrate()
{
var conditions = new List<string>();
if (SendCreate)
{
conditions.Add($"event.type == '{EnrichedAssetEventType.Created}'");
}
if (SendUpdate)
{
conditions.Add($"event.type == '{EnrichedAssetEventType.Updated}'");
}
if (SendRename)
{
conditions.Add($"event.type == '{EnrichedAssetEventType.Renamed}'");
}
if (SendDelete)
{
conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'");
}
var condition = string.Empty;
if (conditions.Count == 0)
{
condition = "false";
}
else if (condition.Length < 4)
{
condition = string.Join(" || ", conditions);
}
return new AssetChangedTriggerV2 { Condition = condition };
}
}
}

49
tools/Migrate_01/OldTriggers/ContentChangedTrigger.cs

@ -0,0 +1,49 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure;
namespace Migrate_01.OldTriggers
{
[TypeName(nameof(ContentChangedTrigger))]
public sealed class ContentChangedTrigger : RuleTrigger, IMigrated<RuleTrigger>
{
public ReadOnlyCollection<ContentChangedTriggerSchema> Schemas { get; set; }
public bool HandleAll { get; set; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{
throw new NotSupportedException();
}
public override void Freeze()
{
base.Freeze();
if (Schemas != null)
{
foreach (var schema in Schemas)
{
schema.Freeze();
}
}
}
public RuleTrigger Migrate()
{
var schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(Schemas.Select(x => x.Migrate()).ToList());
return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas };
}
}
}

87
tools/Migrate_01/OldTriggers/ContentChangedTriggerSchema.cs

@ -0,0 +1,87 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
namespace Migrate_01.OldTriggers
{
public sealed class ContentChangedTriggerSchema : Freezable
{
public Guid SchemaId { get; set; }
public bool SendCreate { get; set; }
public bool SendUpdate { get; set; }
public bool SendDelete { get; set; }
public bool SendPublish { get; set; }
public bool SendUnpublish { get; set; }
public bool SendArchived { get; set; }
public bool SendRestore { get; set; }
public ContentChangedTriggerSchemaV2 Migrate()
{
var conditions = new List<string>();
if (SendCreate)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Created}'");
}
if (SendUpdate)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Updated}'");
}
if (SendPublish)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Published}'");
}
if (SendUnpublish)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Unpublished}'");
}
if (SendArchived)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Archived}'");
}
if (SendRestore)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Restored}'");
}
if (SendDelete)
{
conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'");
}
var condition = string.Empty;
if (conditions.Count == 0 && condition.Length < 7)
{
condition = "false";
}
else if (condition.Length < 7)
{
condition = string.Join(" || ", conditions);
}
return new ContentChangedTriggerSchemaV2 { SchemaId = SchemaId, Condition = condition };
}
}
}
Loading…
Cancel
Save