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"?>
<configuration>
<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" />
</packageSources>
</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)
{
var id = @event is EnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty;
var ruleJob = new FastlyJob
{
Key = @event.AggregateId.ToString(),
Key = id,
FastlyApiKey = action.ApiKey,
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" />
</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="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.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="NodaTime" Version="2.4.2" />
<PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<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>
{
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 Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Json
@ -32,7 +33,14 @@ namespace Squidex.Domain.Apps.Core.Rules.Json
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)
{

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 AggregateId { get; set; }
public string EventName { get; set; }
public string ActionName { get; set; }
@ -26,6 +24,8 @@ namespace Squidex.Domain.Apps.Core.Rules
public string Description { get; set; }
public long ExecutionPartition { get; set; }
public Instant Created { 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
{
[TypeName(nameof(AssetChangedTrigger))]
public sealed class AssetChangedTrigger : RuleTrigger
[TypeName(nameof(AssetChangedTriggerV2))]
public sealed class AssetChangedTriggerV2 : RuleTrigger
{
public bool SendCreate { get; set; }
public bool SendUpdate { get; set; }
public bool SendRename { get; set; }
public bool SendDelete { get; set; }
public string Condition { get; set; }
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
{
public sealed class ContentChangedTriggerSchema : Freezable
public sealed class ContentChangedTriggerSchemaV2 : 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 string Condition { 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
{
[TypeName(nameof(ContentChangedTrigger))]
public sealed class ContentChangedTrigger : RuleTrigger
[TypeName(nameof(ContentChangedTriggerV2))]
public sealed class ContentChangedTriggerV2 : RuleTrigger
{
public ReadOnlyCollection<ContentChangedTriggerSchema> Schemas { get; set; }
public ReadOnlyCollection<ContentChangedTriggerSchemaV2> Schemas { 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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="3.3.3">
<PackageReference Include="Fody" Version="3.3.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

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

@ -5,26 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
public sealed class EnrichedAssetEvent : EnrichedEvent
public sealed class EnrichedAssetEvent : EnrichedEntityEvent
{
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 FileName { get; set; }
@ -38,10 +24,5 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
public int? PixelWidth { 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.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
@ -16,23 +13,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
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 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; }
[IgnoreDataMember]
public abstract Guid AggregateId { get; }
public long Partition { get; set; }
[IgnoreDataMember]
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
{
public abstract class EnrichedSchemaEvent : EnrichedEvent
public abstract class EnrichedSchemaEvent : EnrichedEntityEvent
{
public NamedId<Guid> SchemaId { get; set; }
}

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

@ -6,9 +6,8 @@
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules
{
@ -16,6 +15,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
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 Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects;
@ -21,7 +22,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
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[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray();
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 IJsonSerializer jsonSerializer;
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(scriptEngine, nameof(scriptEngine));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.jsonSerializer = jsonSerializer;
this.scriptEngine = scriptEngine;
this.urlGenerator = urlGenerator;
AddPattern("APP_ID", AppId);
@ -46,6 +52,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("SCHEMA_NAME", SchemaName);
AddPattern("TIMESTAMP_DATETIME", TimestampTime);
AddPattern("TIMESTAMP_DATE", TimestampDate);
AddPattern("USER_ID", UserId);
AddPattern("USER_NAME", UserName);
AddPattern("USER_EMAIL", UserEmail);
}
@ -72,6 +79,19 @@ namespace Squidex.Domain.Apps.Core.HandleRules
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 sb = new StringBuilder();
@ -127,7 +147,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
}
else
{
sb.Append(Undefined);
sb.Append(Fallback);
}
current = current.Slice(match.Length + 1);
@ -169,7 +189,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return schemaEvent.SchemaId.Id.ToString();
}
return Undefined;
return Fallback;
}
private static string SchemaName(EnrichedEvent @event)
@ -179,17 +199,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return schemaEvent.SchemaId.Name;
}
return Undefined;
return Fallback;
}
private static string ContentAction(EnrichedEvent @event)
{
if (@event is EnrichedContentEvent contentEvent)
{
return contentEvent.Type.ToString().ToLowerInvariant();
return contentEvent.Type.ToString();
}
return Undefined;
return Fallback;
}
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 Undefined;
return Fallback;
}
private static string UserName(EnrichedEvent @event)
{
if (@event.Actor != null)
{
if (@event.Actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
return @event.Actor.ToString();
}
if (@event.User != null)
{
return @event.User.DisplayName();
}
}
return @event.User?.DisplayName() ?? Fallback;
}
return Undefined;
private static string UserId(EnrichedEvent @event)
{
return @event.User?.Id ?? Fallback;
}
private static string UserEmail(EnrichedEvent @event)
{
if (@event.Actor != null)
{
if (@event.Actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase))
{
return @event.Actor.ToString();
}
if (@event.User != null)
{
return @event.User.Email;
}
}
return Undefined;
return @event.User?.Email ?? Fallback;
}
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))
{
return Undefined;
return Fallback;
}
if (!field.TryGetValue(path[1], out var value))
{
return Undefined;
return Fallback;
}
for (var j = 2; j < path.Length; j++)
@ -272,16 +271,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules
}
else
{
return Undefined;
return Fallback;
}
}
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>();
if (!triggerHandler.Triggers(appEventEnvelope, rule.Trigger))
var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope);
if (!triggerHandler.Triggers(enrichedEvent, rule.Trigger))
{
return null;
}
@ -104,22 +106,22 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null;
}
var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope);
var actionName = typeNameRegistry.GetName(actionType);
var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
var json = jsonSerializer.Serialize(actionData.Data);
enrichedEvent.CalculatePartition();
var job = new RuleJob
{
JobId = Guid.NewGuid(),
ActionName = actionName,
ActionData = json,
AggregateId = enrichedEvent.AggregateId,
AppId = appEvent.AppId.Id,
Created = now,
EventName = enrichedEvent.Name,
ExecutionPartition = enrichedEvent.Partition,
Expires = expires,
Description = actionData.Description
};

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

@ -6,9 +6,8 @@
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules
{
@ -19,11 +18,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
get { return typeof(T); }
}
bool IRuleTriggerHandler.Triggers(Envelope<AppEvent> @event, RuleTrigger trigger)
bool IRuleTriggerHandler.Triggers(EnrichedEvent @event, RuleTrigger 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.
// ==========================================================================
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
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
trigger.SendCreate && @event is AssetCreated ||
trigger.SendUpdate && @event is AssetUpdated ||
trigger.SendDelete && @event is AssetDeleted ||
trigger.SendRename && @event is AssetRenamed;
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", assetEvent, trigger.Condition);
}
}
}

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

@ -5,32 +5,36 @@
// 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.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
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 &&
@event.Payload is ContentEvent &&
!(@event.Payload is ContentChangesPublished) &&
!(@event.Payload is ContentChangesDiscarded) &&
!(@event.Payload is ContentUpdateProposed))
Guard.NotNull(scriptEngine, nameof(scriptEngine));
this.scriptEngine = scriptEngine;
}
protected override bool Triggers(EnrichedEvent @event, ContentChangedTriggerV2 trigger)
{
if (trigger.HandleAll)
{
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)
{
if (MatchsSchema(schema, schemaEvent) && MatchsType(schema, schemaEvent))
if (MatchsSchema(schema, schemaEvent) && MatchsCondition(schema, schemaEvent))
{
return true;
}
@ -40,56 +44,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
return false;
}
private static bool MatchsSchema(ContentChangedTriggerSchema schema, SchemaEvent @event)
private bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event)
{
return @event.SchemaId.Id == schema.SchemaId;
}
private static bool MatchsType(ContentChangedTriggerSchema schema, SchemaEvent @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)
private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @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 HashSet<string> fieldsToDelete;
private Dictionary<string, ContentDataProperty> fieldProperties;
private Dictionary<string, PropertyDescriptor> fieldProperties;
private bool isChanged;
public ContentDataObject(Engine engine, NamedContentData contentData)
@ -55,7 +55,9 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
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;
}
@ -109,17 +111,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
EnsurePropertiesInitialized();
foreach (var property in fieldProperties)
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
return fieldProperties;
}
private void EnsurePropertiesInitialized()
{
if (fieldProperties == null)
{
fieldProperties = new Dictionary<string, ContentDataProperty>(contentData.Count);
fieldProperties = new Dictionary<string, PropertyDescriptor>(contentData.Count);
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.Runtime;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentDataProperty : PropertyDescriptor
public sealed class ContentDataProperty : CustomProperty
{
private readonly ContentDataObject contentData;
private ContentFieldObject contentField;
private JsValue value;
public override JsValue Value
protected override JsValue CustomValue
{
get
{
@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
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)
: base(null, true, true, true)
{
this.contentData = contentData;
this.contentField = contentField;
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 ContentFieldData fieldData;
private HashSet<string> valuesToDelete;
private Dictionary<string, ContentFieldProperty> valueProperties;
private Dictionary<string, PropertyDescriptor> valueProperties;
private bool isChanged;
public ContentFieldData FieldData
@ -67,9 +67,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
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();
foreach (var property in valueProperties)
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
return valueProperties;
}
private void EnsurePropertiesInitialized()
{
if (valueProperties == null)
{
valueProperties = new Dictionary<string, ContentFieldProperty>(FieldData.Count);
valueProperties = new Dictionary<string, PropertyDescriptor>(FieldData.Count);
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.Runtime.Descriptors;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentFieldProperty : PropertyDescriptor
public sealed class ContentFieldProperty : CustomProperty
{
private readonly ContentFieldObject contentField;
private IJsonValue contentValue;
private JsValue value;
private bool isChanged;
public override JsValue Value
protected override JsValue CustomValue
{
get
{
@ -49,7 +48,6 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
}
public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null)
: base(null, true, true, true)
{
this.contentField = contentField;
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:
return JsValue.Null;
case JsonScalar<string> s:
return new JsValue(s.Value);
return new JsString(s.Value);
case JsonScalar<bool> b:
return new JsValue(b.Value);
return new JsBoolean(b.Value);
case JsonScalar<double> b:
return new JsValue(b.Value);
return new JsNumber(b.Value);
case JsonObject obj:
return FromObject(obj, engine);
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.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
@ -16,5 +18,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
NamedContentData ExecuteAndTransform(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.Collections.Generic;
using System.Globalization;
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Native.Date;
using Jint.Native.Object;
using Jint.Parser;
using Jint.Runtime;
using Jint.Runtime.Interop;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure;
using Squidex.Shared.Users;
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 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)
{
Guard.NotNull(context, nameof(context));
@ -115,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
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));
}
@ -127,7 +166,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
private Engine CreateScriptEngine(ScriptContext context)
{
var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict());
var engine = CreateScriptEngine();
var contextInstance = new ObjectInstance(engine);
@ -143,7 +182,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
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))
@ -152,12 +191,57 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
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;
}
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)
{
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)
{
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);
}));
}
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.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Interop;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class JintUser : ObjectInstance
public static class JintUser
{
private static readonly char[] ClaimSeparators = { '/', '.', ':' };
public JintUser(Engine engine, ClaimsPrincipal principal)
: base(engine)
public static ObjectWrapper Create(Engine engine, IUser user)
{
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();
@ -30,22 +40,20 @@ namespace Squidex.Domain.Apps.Core.Scripting
id = principal.OpenIdClientId();
}
FastAddProperty("id", id, false, true, false);
FastAddProperty("isClient", isClient, false, true, false);
FastAddProperty("email", principal.OpenIdEmail(), false, true, false);
var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value;
var claimsInstance = new ObjectInstance(engine);
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());
return CreateUser(engine, id, isClient, principal.OpenIdEmail(), name, principal.Claims);
}
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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="NJsonSchema" Version="9.13.2" />
<PackageReference Include="Jint" Version="3.0.0-beta-1427" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<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" />
</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="RefactoringEssentials" Version="5.6.0" 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;
IUser user = null;
try
{
return await userResolver.FindByIdOrEmailAsync(actor.Identifier);
user = await userResolver.FindByIdOrEmailAsync(actor.Identifier);
}
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);
}
public Task<IEnumerable<ValidationError>> Visit(AssetChangedTrigger trigger)
public Task<IEnumerable<ValidationError>> Visit(AssetChangedTriggerV2 trigger)
{
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)
{

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

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

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

@ -15,12 +15,12 @@
</ItemGroup>
<ItemGroup>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" />
<PackageReference Include="NodaTime" Version="2.4.2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<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.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleCreated))]
public sealed class RuleCreated : RuleEvent
public sealed class RuleCreated : RuleEvent, IMigrated<IEvent>
{
public RuleTrigger Trigger { 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.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleUpdated))]
public sealed class RuleUpdated : RuleEvent
public sealed class RuleUpdated : RuleEvent, IMigrated<IEvent>
{
public RuleTrigger Trigger { 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>
<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="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<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 payloadObj = serializer.Deserialize<IEvent>(eventData.Payload, payloadType, stringConverter);
if (payloadObj is IMigratedEvent migratedEvent)
if (payloadObj is IMigrated<IEvent> migratedEvent)
{
payloadObj = migratedEvent.Migrate();
}
@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var eventPayload = envelope.Payload;
if (migrate && eventPayload is IMigratedEvent migratedEvent)
if (migrate && eventPayload is IMigrated<IEvent> migratedEvent)
{
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.
// ==========================================================================
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.Diagnostics.HealthChecks.Abstractions" 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.Orleans.CodeGenerator.MSBuild" Version="2.1.2">
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.0" />
<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="SixLabors.ImageSharp" Version="1.0.0-beta0004" />
<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)); }
}
public PartitionedActionBlock(Action<TInput> action, Func<TInput, int> partitioner)
public PartitionedActionBlock(Action<TInput> action, Func<TInput, long> partitioner)
: 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())
{
}
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)
{
}
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(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);
}
public RuleTriggerDto Visit(AssetChangedTrigger trigger)
public RuleTriggerDto Visit(AssetChangedTriggerV2 trigger)
{
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();

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
{
/// <summary>
/// Determines whether to handle the event when an asset is created.
/// Javascript condition when to trigger.
/// </summary>
public bool SendCreate { 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 string Condition { get; set; }
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()
{
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; }
/// <summary>
/// Determines whether to handle the event when a content is created.
/// Javascript condition when to trigger.
/// </summary>
public bool SendCreate { 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; }
public string Condition { get; set; }
}
}

16
src/Squidex/Squidex.csproj

@ -54,7 +54,7 @@
</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="Ben.BlockingDetector" Version="0.0.3" />
<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.Options.ConfigurationExtensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="NJsonSchema" Version="9.13.2" />
<PackageReference Include="NSwag.AspNetCore" Version="12.0.5" />
<PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="NSwag.AspNetCore" Version="12.0.9" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" />
<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 class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendCreate" formControlName="sendCreate" />
<label class="form-check-label" for="sendCreate">
created
</label>
</div>
<label for="condition">Condition</label>
<sqx-control-errors for="condition" [submitted]="triggerFormSubmitted"></sqx-control-errors>
<textarea class="form-control code" id="condition" formControlName="condition"></textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendUpdate" formControlName="sendUpdate" />
<label class="form-check-label" for="sendUpdate">
updated
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendRename" formControlName="sendRename" />
<label class="form-check-label" for="sendRename">
renamed
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendDelete" formControlName="sendDelete" />
<label class="form-check-label" for="sendDelete">
deleted
</label>
</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 == 'Updated'</pre>
</li>
<li class="help-example">
Large assets:
<pre>event.fileSize > 100000000</pre>
</li>
<li class="help-example">
Images only:
<pre>event.isImage</pre>
</li>
</ul>
</div>
</div>

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

@ -1,2 +1,33 @@
@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 ngOnInit() {
this.triggerForm.setControl('sendCreate',
new FormControl(this.trigger.sendCreate || false));
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));
this.triggerForm.setControl('condition',
new FormControl(this.trigger.condition || ''));
}
}

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">
<table class="table table-middle table-sm table-fixed table-borderless">
<colgroup>
<col style="width: 100%" />
<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: 40%" />
<col style="width: 60%" />
<col style="width: 40px" />
</colgroup>
@ -17,29 +10,8 @@
<th>
Schema
</th>
<th class="text-center">
<div class="rotated-label">All</div>
</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>
Condition
</th>
<th></th>
</tr>
@ -48,29 +20,8 @@
<td>
<span class="truncate">{{schema.schema.displayName}}</span>
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" />
</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 class="text-center">
<input type="text" class="form-control code" [(ngModel)]="schema.condition" (blur)="updateValue()" />
</td>
<td class="text-center">
<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>
</form>
</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>
<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 {
border-top: 1px solid $color-border;
padding-top: 1rem;
padding-bottom: 0;
}
.table {
margin-top: 3.5rem;
padding-bottom: 1rem;
}
.form-check {
margin-top: 1rem;
}
.rotated-label {
@include rotate(-45deg);
.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;
}
}

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

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

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 })
.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
{
@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.Model.Rules
[Fact]
public void Should_create_with_trigger_and_action()
{
var ruleTrigger = new ContentChangedTrigger();
var ruleTrigger = new ContentChangedTriggerV2();
var ruleAction = new TestAction1();
var newRule = new Rule(ruleTrigger, ruleAction);
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Model.Rules
[Fact]
public void Should_replace_trigger_when_updating()
{
var newTrigger = new ContentChangedTrigger();
var newTrigger = new ContentChangedTriggerV2();
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.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity;
@ -32,13 +33,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
public RuleEventFormatterTests()
{
A.CallTo(() => user.Id)
.Returns("123");
A.CallTo(() => user.Email)
.Returns("me@email.com");
A.CallTo(() => user.Claims)
.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]
@ -69,91 +76,120 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Contains("MyEventName", result);
}
[Fact]
public void Should_replace_app_information_from_event()
[Theory]
[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 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);
}
[Fact]
public void Should_replace_schema_information_from_event()
[Theory]
[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 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);
}
[Fact]
public void Should_replace_timestamp_information_from_event()
[Theory]
[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 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);
}
[Fact]
public void Should_format_email_and_display_name_from_user()
[Theory]
[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]
public void Should_return_undefined_if_user_is_not_found()
[Theory]
[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]
public void Should_format_email_and_display_name_from_client()
[Theory]
[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]
public void Should_replace_content_url_from_event()
[Theory]
[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))
.Returns(url);
var result = sut.Format(script, @event);
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]
public void Should_format_content_url_when_not_found()
[Theory]
[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]
public void Should_return_undefined_when_field_not_found()
[Theory]
[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
{
@ -164,13 +200,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.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]
public void Should_return_undefined_when_partition_not_found()
[Theory]
[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
{
@ -181,13 +219,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.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]
public void Should_return_undefined_when_array_item_not_found()
[Theory]
[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
{
@ -198,13 +238,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.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]
public void Should_return_undefined_when_property_not_found()
[Theory]
[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
{
@ -215,13 +257,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.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]
public void Should_return_plain_value_when_found()
[Theory]
[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
{
@ -232,13 +276,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
var result = sut.Format("$CONTENT_DATA.city.iv", @event);
var result = sut.Format(script, @event);
Assert.Equal("Berlin", result);
}
[Fact]
public void Should_return_plain_value_when_found_from_update_event()
[Theory]
[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
{
@ -246,33 +292,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
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);
}
[Fact]
public void Should_return_undefined_when_null()
{
var @event = new EnrichedContentEvent
{
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()
[Theory]
[InlineData("$CONTENT_DATA.city.iv.name")]
[InlineData("Script(`${event.data.city.iv.name}`)")]
public void Should_return_plain_value_from_object_when_found(string script)
{
var @event = new EnrichedContentEvent
{
@ -283,31 +315,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
};
var result = sut.Format("$CONTENT_DATA.city.iv", @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);
var result = sut.Format(script, @event);
Assert.Equal("Berlin", result);
}
[Fact]
public void Should_return_plain_value_from_object_when_found()
[Theory]
[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
{
@ -318,21 +334,20 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.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]
public void Should_format_content_actions_when_found()
[Theory]
[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]
public void Should_format_content_actions_when_not_found()
{
Assert.Equal("UNDEFINED", sut.Format("$CONTENT_ACTION", new EnrichedAssetEvent()));
var result = sut.Format(script, @event);
Assert.Equal("From client:android", result);
}
}
}

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));
A.CallTo(() => ruleTriggerHandler.TriggerType)
.Returns(typeof(ContentChangedTrigger));
.Returns(typeof(ContentChangedTriggerV2));
sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, eventEnricher, TestUtils.DefaultSerializer, clock, typeNameRegistry);
}
@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Fact]
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 job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
@ -132,7 +132,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
var ruleConfig = ValidRule();
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);
var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope);
@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => clock.GetCurrentInstant())
.Returns(now);
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger))
A.CallTo(() => ruleTriggerHandler.Triggers(A<EnrichedEvent>.Ignored, ruleConfig.Trigger))
.Returns(true);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, ruleConfig.Action))
@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => clock.GetCurrentInstant())
.Returns(now);
A.CallTo(() => ruleTriggerHandler.Triggers(A<Envelope<AppEvent>>.Ignored, ruleConfig.Trigger))
A.CallTo(() => ruleTriggerHandler.Triggers(A<EnrichedEvent>.Ignored, ruleConfig.Trigger))
.Returns(true);
A.CallTo(() => ruleActionHandler.CreateJobAsync(A<EnrichedEvent>.Ignored, ruleConfig.Action))
@ -260,7 +260,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
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.
// ==========================================================================
using System.Collections.Generic;
using FakeItEasy;
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.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Domain.Apps.Core.Scripting;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
{
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
{
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() }
};
}
sut = new AssetChangedTriggerHandler(scriptEngine);
}
[Theory]
[MemberData(nameof(TestData))]
public void Should_return_result_depending_on_event(int expected, int sendCreate, int sendUpdate, int sendRename, int sendDelete, AppEvent @event)
[Fact]
public void Should_trigger_when_condition_is_null()
{
var trigger = new AssetChangedTrigger
{
SendCreate = sendCreate == 1,
SendUpdate = sendUpdate == 1,
SendRename = sendRename == 1,
SendDelete = sendDelete == 1
};
var trigger = new AssetChangedTriggerV2();
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.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.EnrichedEvents;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
#pragma warning disable SA1401 // Fields must be private
@ -26,82 +23,164 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers
{
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> 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() },
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 } }
};
sut = new ContentChangedTriggerHandler(scriptEngine);
}
[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);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[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);
A.CallTo(() => scriptEngine.Evaluate(A<string>.Ignored, A<object>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Theory]
[MemberData(nameof(TestData))]
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)
[Fact]
public void Should_trigger_when_condition_is_null()
{
var trigger = new ContentChangedTrigger
var trigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Create(
new ContentChangedTriggerSchema
Schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(new List<ContentChangedTriggerSchemaV2>
{
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
})
}
})
};
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();
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();
}

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" />
</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="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<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
{
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 IAppProvider appProvider = A.Fake<IAppProvider>();
@ -60,9 +60,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{
var command = CreateCommand(new CreateRule
{
Trigger = new ContentChangedTrigger
Trigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>()
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
},
Action = null
});
@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{
var command = CreateCommand(new CreateRule
{
Trigger = new ContentChangedTrigger
Trigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>()
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
},
Action = new TestAction
{
@ -103,9 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{
var command = new UpdateRule
{
Trigger = new ContentChangedTrigger
Trigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>()
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
},
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))
.Returns(Task.FromResult<ISchemaEntity>(null));
var trigger = new ContentChangedTrigger
var trigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Create(
new ContentChangedTriggerSchema()
new ContentChangedTriggerSchemaV2()
)
};
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
[Fact]
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);
@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
[Fact]
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);
@ -67,10 +67,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers
A.CallTo(() => appProvider.GetSchemaAsync(appId, A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
var trigger = new ContentChangedTrigger
var trigger = new ContentChangedTriggerV2
{
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 rule1 = new Rule(new ContentChangedTrigger(), new TestAction { Url = new Uri("https://squidex.io") });
var rule2 = new Rule(new ContentChangedTrigger(), new TestAction { Url = new Uri("https://squidex.io") });
var rule3 = 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 ContentChangedTriggerV2(), 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 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()
{
var newTrigger = new ContentChangedTrigger
var newTrigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>()
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
};
var newAction = new TestAction
@ -204,9 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
private static UpdateRule MakeUpdateCommand()
{
var newTrigger = new ContentChangedTrigger
var newTrigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchema>()
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
};
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" />
</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="GraphQL" Version="2.4.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" />
</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="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<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 sealed class MyOldEvent : IEvent, IMigratedEvent
public sealed class MyOldEvent : IEvent, IMigrated<IEvent>
{
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" />
</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="Google.Cloud.Storage.V1" Version="2.2.1" />
<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" />
</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.AspNetIdentity" Version="2.3.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="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />

3
tools/Migrate_01/OldEvents/AppClientChanged.cs

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

3
tools/Migrate_01/OldEvents/AppClientUpdated.cs

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

3
tools/Migrate_01/OldEvents/AppContributorAssigned.cs

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

3
tools/Migrate_01/OldEvents/ContentArchived.cs

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

3
tools/Migrate_01/OldEvents/ContentPublished.cs

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

3
tools/Migrate_01/OldEvents/ContentRestored.cs

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

3
tools/Migrate_01/OldEvents/ContentUnpublished.cs

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