Browse Source

Feature/liquid (#531)

* Started with liquid support.

* First working jint extension.

* Finalized liquid support.
pull/532/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
a866626415
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  2. 67
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  4. 42
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  5. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  6. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs
  7. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  8. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs
  10. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
  11. 64
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  12. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  13. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  14. 33
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs
  15. 79
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs
  16. 106
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/JsonArrayFluidValue.cs
  17. 51
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs
  18. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs
  19. 75
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidValue.cs
  20. 93
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/FluidTemplateEngine.cs
  21. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/IFluidExtension.cs
  22. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/ITemplateEngine.cs
  23. 57
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateParseException.cs
  24. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateVars.cs
  25. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
  28. 45
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  29. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  30. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  31. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  33. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  34. 99
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs
  35. 16
      backend/src/Squidex.Domain.Apps.Entities/Context.cs
  36. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs
  37. 17
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  38. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
  39. 30
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  40. 15
      backend/src/Squidex/Config/Domain/RuleServices.cs
  41. 60
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs
  42. 793
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs
  43. 449
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  44. 36
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  45. 44
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  46. 129
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs
  47. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  48. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs
  49. 112
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  50. 26
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  51. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs
  52. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs
  53. 104
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs
  54. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs
  55. 8
      frontend/app/features/rules/pages/events/rule-events-page.component.scss

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Newtonsoft.Json;
using NodaTime; using NodaTime;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
@ -31,5 +32,8 @@ namespace Squidex.Domain.Apps.Core.Rules
public Instant Created { get; set; } public Instant Created { get; set; }
public Instant Expires { get; set; } public Instant Expires { get; set; }
[JsonIgnore]
public Exception? Exception { get; set; }
} }
} }

67
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Fluid;
using Fluid.Values;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{
public sealed class EventFluidExtensions : IFluidExtension
{
private readonly IUrlGenerator urlGenerator;
public EventFluidExtensions(IUrlGenerator urlGenerator)
{
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.urlGenerator = urlGenerator;
}
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl);
TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl);
}
private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context)
{
if (input is ObjectValue objectValue)
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent)
{
if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty)
{
var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, guid);
return new StringValue(result);
}
}
}
return NilValue.Empty;
}
private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context)
{
if (input is ObjectValue objectValue)
{
if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty)
{
var result = urlGenerator.AssetContent(guid);
return new StringValue(result);
}
}
return NilValue.Empty;
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs → backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs

@ -10,14 +10,14 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.Scripting namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{ {
public sealed class EventScriptExtension : IScriptExtension public sealed class EventJintExtension : IJintExtension
{ {
private delegate JsValue EventDelegate(); private delegate JsValue EventDelegate();
private readonly IUrlGenerator urlGenerator; private readonly IUrlGenerator urlGenerator;
public EventScriptExtension(IUrlGenerator urlGenerator) public EventJintExtension(IUrlGenerator urlGenerator)
{ {
Guard.NotNull(urlGenerator, nameof(urlGenerator)); Guard.NotNull(urlGenerator, nameof(urlGenerator));

42
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -15,6 +15,7 @@ using Newtonsoft.Json;
using NodaTime.Text; using NodaTime.Text;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using ValueTaskSupplement; using ValueTaskSupplement;
@ -28,6 +29,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\s]+))?[\s]*\}", RegexOptions.Compiled); private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\s]+))?[\s]*\}", RegexOptions.Compiled);
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly IEnumerable<IRuleEventFormatter> formatters; private readonly IEnumerable<IRuleEventFormatter> formatters;
private readonly ITemplateEngine templateEngine;
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private struct TextPart private struct TextPart
@ -67,14 +69,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules
} }
} }
public RuleEventFormatter(IJsonSerializer jsonSerializer, IEnumerable<IRuleEventFormatter> formatters, IScriptEngine scriptEngine) public RuleEventFormatter(IJsonSerializer jsonSerializer, IEnumerable<IRuleEventFormatter> formatters, ITemplateEngine templateEngine, IScriptEngine scriptEngine)
{ {
Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(templateEngine, nameof(templateEngine));
Guard.NotNull(formatters, nameof(formatters)); Guard.NotNull(formatters, nameof(formatters));
this.jsonSerializer = jsonSerializer; this.jsonSerializer = jsonSerializer;
this.formatters = formatters; this.formatters = formatters;
this.templateEngine = templateEngine;
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
} }
@ -95,14 +99,24 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return text; return text;
} }
if (TryGetTemplate(text.Trim(), out var template))
{
var vars = new TemplateVars
{
["event"] = @event
};
return await templateEngine.RenderAsync(template, vars);
}
if (TryGetScript(text.Trim(), out var script)) if (TryGetScript(text.Trim(), out var script))
{ {
var context = new ScriptContext var vars = new ScriptVars
{ {
["event"] = @event ["event"] = @event
}; };
return scriptEngine.Interpolate(context, script); return scriptEngine.Interpolate(vars, script);
} }
var parts = BuildParts(text, @event); var parts = BuildParts(text, @event);
@ -263,7 +277,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
case "trim": case "trim":
text = text.Trim(); text = text.Trim();
break; break;
case "timestamp_ms": case "timestamp":
{ {
var instant = InstantPattern.General.Parse(text); var instant = InstantPattern.General.Parse(text);
@ -275,7 +289,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
break; break;
} }
case "timestamp_seconds": case "timestamp_sec":
{ {
var instant = InstantPattern.General.Parse(text); var instant = InstantPattern.General.Parse(text);
@ -336,5 +350,23 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return false; return false;
} }
private static bool TryGetTemplate(string text, out string script)
{
const string TemplateSuffix = ")";
const string TemplatePrefix = "Liquid(";
script = null!;
var comparer = StringComparison.OrdinalIgnoreCase;
if (text.StartsWith(TemplatePrefix, comparer) && text.EndsWith(TemplateSuffix, comparer))
{
script = text.Substring(TemplatePrefix.Length, text.Length - TemplatePrefix.Length - TemplateSuffix.Length);
return true;
}
return false;
}
} }
} }

21
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -135,24 +135,35 @@ namespace Squidex.Domain.Apps.Core.HandleRules
} }
var actionName = typeNameRegistry.GetName(actionType); var actionName = typeNameRegistry.GetName(actionType);
var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
var json = jsonSerializer.Serialize(actionData.Data);
var job = new RuleJob var job = new RuleJob
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
ActionData = json, ActionData = string.Empty,
ActionName = actionName, ActionName = actionName,
AppId = enrichedEvent.AppId.Id, AppId = enrichedEvent.AppId.Id,
Created = now, Created = now,
Description = actionData.Description,
EventName = enrichedEvent.Name, EventName = enrichedEvent.Name,
ExecutionPartition = enrichedEvent.Partition, ExecutionPartition = enrichedEvent.Partition,
Expires = expires, Expires = expires,
RuleId = ruleId RuleId = ruleId
}; };
try
{
var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action);
var json = jsonSerializer.Serialize(actionData.Data);
job.ActionData = json;
job.Description = actionData.Description;
}
catch (Exception ex)
{
job.Description = "Failed to create job";
job.Exception = ex;
}
result.Add(job); result.Add(job);
} }
catch (Exception ex) catch (Exception ex)

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs

@ -12,11 +12,11 @@ using Jint.Native;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class DateTimeScriptExtension : IScriptExtension public sealed class DateTimeJintExtension : IJintExtension
{ {
private readonly Func<DateTime, string, JsValue> formatDate; private readonly Func<DateTime, string, JsValue> formatDate;
public DateTimeScriptExtension() public DateTimeJintExtension()
{ {
formatDate = new Func<DateTime, string, JsValue>(FormatDate); formatDate = new Func<DateTime, string, JsValue>(FormatDate);
} }

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpScriptExtension.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -16,12 +16,12 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class HttpScriptExtension : IScriptExtension public sealed class HttpJintExtension : IJintExtension
{ {
private delegate void GetJsonDelegate(string url, Action<JsValue> callback, JsValue? headers = null); private delegate void GetJsonDelegate(string url, Action<JsValue> callback, JsValue? headers = null);
private readonly IHttpClientFactory httpClientFactory; private readonly IHttpClientFactory httpClientFactory;
public HttpScriptExtension(IHttpClientFactory httpClientFactory) public HttpJintExtension(IHttpClientFactory httpClientFactory)
{ {
Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); Guard.NotNull(httpClientFactory, nameof(httpClientFactory));

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs

@ -12,14 +12,14 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class StringScriptExtension : IScriptExtension public sealed class StringJintExtension : IJintExtension
{ {
private delegate JsValue StringSlugifyDelegate(string text, bool single = false); private delegate JsValue StringSlugifyDelegate(string text, bool single = false);
private readonly StringSlugifyDelegate slugify; private readonly StringSlugifyDelegate slugify;
private readonly Func<string, JsValue> toCamelCase; private readonly Func<string, JsValue> toCamelCase;
private readonly Func<string, JsValue> toPascalCase; private readonly Func<string, JsValue> toPascalCase;
public StringScriptExtension() public StringJintExtension()
{ {
slugify = new StringSlugifyDelegate(Slugify); slugify = new StringSlugifyDelegate(Slugify);

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptExtension.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs

@ -9,7 +9,7 @@ using Jint;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public interface IScriptExtension public interface IJintExtension
{ {
void Extend(Engine engine) void Extend(Engine engine)
{ {

12
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs

@ -13,16 +13,16 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
public interface IScriptEngine public interface IScriptEngine
{ {
Task ExecuteAsync(ScriptContext context, string script); Task ExecuteAsync(ScriptVars vars, string script);
Task<NamedContentData> ExecuteAndTransformAsync(ScriptContext context, string script); Task<NamedContentData> ExecuteAndTransformAsync(ScriptVars vars, string script);
Task<NamedContentData> TransformAsync(ScriptContext context, string script); Task<NamedContentData> TransformAsync(ScriptVars vars, string script);
Task<IJsonValue> GetAsync(ScriptContext context, string script); Task<IJsonValue> GetAsync(ScriptVars vars, string script);
bool Evaluate(ScriptContext context, string script); bool Evaluate(ScriptVars vars, string script);
string? Interpolate(ScriptContext context, string script); string? Interpolate(ScriptVars vars, string script);
} }
} }

64
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -26,23 +26,23 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class JintScriptEngine : IScriptEngine public sealed class JintScriptEngine : IScriptEngine
{ {
private readonly IScriptExtension[] extensions; private readonly IJintExtension[] extensions;
private readonly Parser parser; private readonly Parser parser;
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMilliseconds(4000); public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMilliseconds(4000);
public JintScriptEngine(IMemoryCache memoryCache, IEnumerable<IScriptExtension>? extensions = null) public JintScriptEngine(IMemoryCache memoryCache, IEnumerable<IJintExtension>? extensions = null)
{ {
parser = new Parser(memoryCache); parser = new Parser(memoryCache);
this.extensions = extensions?.ToArray() ?? Array.Empty<IScriptExtension>(); this.extensions = extensions?.ToArray() ?? Array.Empty<IJintExtension>();
} }
public async Task ExecuteAsync(ScriptContext context, string script) public async Task ExecuteAsync(ScriptVars vars, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script)); Guard.NotNullOrEmpty(script, nameof(script));
using (var cts = new CancellationTokenSource(ExecutionTimeout)) using (var cts = new CancellationTokenSource(ExecutionTimeout))
@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled())) using (cts.Token.Register(() => tcs.TrySetCanceled()))
{ {
var engine = var engine =
CreateEngine(context, true, cts.Token, tcs.TrySetException, true) CreateEngine(vars, true, cts.Token, tcs.TrySetException, true)
.AddDisallow() .AddDisallow()
.AddReject(); .AddReject();
@ -73,9 +73,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
public async Task<NamedContentData> ExecuteAndTransformAsync(ScriptContext context, string script) public async Task<NamedContentData> ExecuteAndTransformAsync(ScriptVars vars, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script)); Guard.NotNullOrEmpty(script, nameof(script));
using (var cts = new CancellationTokenSource(ExecutionTimeout)) using (var cts = new CancellationTokenSource(ExecutionTimeout))
@ -85,13 +85,13 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled())) using (cts.Token.Register(() => tcs.TrySetCanceled()))
{ {
var engine = var engine =
CreateEngine(context, true, cts.Token, tcs.TrySetException, true) CreateEngine(vars, true, cts.Token, tcs.TrySetException, true)
.AddDisallow() .AddDisallow()
.AddReject(); .AddReject();
engine.SetValue("complete", new Action<JsValue?>(value => engine.SetValue("complete", new Action<JsValue?>(value =>
{ {
tcs.TrySetResult(context.Data!); tcs.TrySetResult(vars.Data!);
})); }));
engine.SetValue("replace", new Action(() => engine.SetValue("replace", new Action(() =>
@ -108,7 +108,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
else else
{ {
tcs.TrySetResult(context.Data!); tcs.TrySetResult(vars.Data!);
} }
} }
} }
@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
if (engine.GetValue("async") != true) if (engine.GetValue("async") != true)
{ {
tcs.TrySetResult(context.Data!); tcs.TrySetResult(vars.Data!);
} }
return await tcs.Task; return await tcs.Task;
@ -126,9 +126,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
public async Task<NamedContentData> TransformAsync(ScriptContext context, string script) public async Task<NamedContentData> TransformAsync(ScriptVars vars, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script)); Guard.NotNullOrEmpty(script, nameof(script));
using (var cts = new CancellationTokenSource(ExecutionTimeout)) using (var cts = new CancellationTokenSource(ExecutionTimeout))
@ -137,11 +137,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled())) using (cts.Token.Register(() => tcs.TrySetCanceled()))
{ {
var engine = CreateEngine(context, true, cts.Token, tcs.TrySetException, true); var engine = CreateEngine(vars, true, cts.Token, tcs.TrySetException, true);
engine.SetValue("complete", new Action<JsValue?>(value => engine.SetValue("complete", new Action<JsValue?>(value =>
{ {
tcs.TrySetResult(context.Data!); tcs.TrySetResult(vars.Data!);
})); }));
engine.SetValue("replace", new Action(() => engine.SetValue("replace", new Action(() =>
@ -158,7 +158,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
else else
{ {
tcs.TrySetResult(context.Data!); tcs.TrySetResult(vars.Data!);
} }
} }
} }
@ -168,7 +168,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
if (engine.GetValue("async") != true) if (engine.GetValue("async") != true)
{ {
tcs.TrySetResult(context.Data!); tcs.TrySetResult(vars.Data!);
} }
return await tcs.Task; return await tcs.Task;
@ -176,14 +176,14 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
public bool Evaluate(ScriptContext context, string script) public bool Evaluate(ScriptVars vars, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script)); Guard.NotNullOrEmpty(script, nameof(script));
try try
{ {
var engine = CreateEngine(context, false); var engine = CreateEngine(vars, false);
Execute(engine, script); Execute(engine, script);
@ -197,14 +197,14 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
public string? Interpolate(ScriptContext context, string script) public string? Interpolate(ScriptVars vars, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script)); Guard.NotNullOrEmpty(script, nameof(script));
try try
{ {
var engine = CreateEngine(context, false); var engine = CreateEngine(vars, false);
Execute(engine, script); Execute(engine, script);
@ -218,9 +218,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
public Task<IJsonValue> GetAsync(ScriptContext context, string script) public Task<IJsonValue> GetAsync(ScriptVars vars, string script)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script)); Guard.NotNullOrEmpty(script, nameof(script));
using (var cts = new CancellationTokenSource(ExecutionTimeout)) using (var cts = new CancellationTokenSource(ExecutionTimeout))
@ -232,7 +232,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
tcs.TrySetCanceled(); tcs.TrySetCanceled();
})) }))
{ {
var engine = CreateEngine(context, true, cts.Token, ex => tcs.TrySetException(ex), true); var engine = CreateEngine(vars, true, cts.Token, ex => tcs.TrySetException(ex), true);
engine.SetValue("complete", new Action<JsValue?>(value => engine.SetValue("complete", new Action<JsValue?>(value =>
{ {
@ -251,7 +251,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
private Engine CreateEngine(ScriptContext context, bool nested, CancellationToken cancellationToken = default, ExceptionHandler? exceptionHandler = null, bool async = false) private Engine CreateEngine(ScriptVars vars, bool nested, CancellationToken cancellationToken = default, ExceptionHandler? exceptionHandler = null, bool async = false)
{ {
var engine = new Engine(options => var engine = new Engine(options =>
{ {
@ -271,16 +271,16 @@ namespace Squidex.Domain.Apps.Core.Scripting
extension.Extend(engine); extension.Extend(engine);
} }
var executionContext = new ExecutionContext(engine, cancellationToken, exceptionHandler); var executionvars = new ExecutionContext(engine, cancellationToken, exceptionHandler);
context.Add(executionContext, nested); vars.Add(executionvars, nested);
foreach (var extension in extensions) foreach (var extension in extensions)
{ {
extension.Extend(executionContext, async); extension.Extend(executionvars, async);
} }
return executionContext.Engine; return executionvars.Engine;
} }
private void Execute(Engine engine, string script) private void Execute(Engine engine, string script)

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs

@ -16,9 +16,9 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class ScriptContext : Dictionary<string, object?> public sealed class ScriptVars : Dictionary<string, object?>
{ {
public ScriptContext() public ScriptVars()
: base(StringComparer.OrdinalIgnoreCase) : base(StringComparer.OrdinalIgnoreCase)
{ {
} }

1
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -16,6 +16,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="Jint" Version="3.0.0-beta-1580" /> <PackageReference Include="Jint" Version="3.0.0-beta-1580" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.3" /> <PackageReference Include="Microsoft.OData.Core" Version="7.6.3" />

33
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Fluid;
using Fluid.Values;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
public sealed class ContentFluidExtension : IFluidExtension
{
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
FluidValue.SetTypeMapping<JsonObject>(x => new ObjectValue(x));
FluidValue.SetTypeMapping<JsonArray>(x => new JsonArrayFluidValue(x));
memberAccessStrategy.Register<NamedContentData, object?>(
(value, name) => value.GetOrDefault(name));
memberAccessStrategy.Register<JsonObject, object?>(
(value, name) => value.GetOrDefault(name));
memberAccessStrategy.Register<ContentFieldData, object?>(
(value, name) => value.GetOrDefault(name));
}
}
}

79
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs

@ -0,0 +1,79 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Fluid;
using Fluid.Values;
using NodaTime;
namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
public class DateTimeFluidExtension : IFluidExtension
{
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
TemplateContext.GlobalFilters.AddFilter("format_date", FormatDate);
TemplateContext.GlobalFilters.AddFilter("timestamp", FormatTimestamp);
TemplateContext.GlobalFilters.AddFilter("timestamp_sec", FormatTimestampSec);
}
public static FluidValue FormatTimestamp(FluidValue input, FilterArguments arguments, TemplateContext context)
{
return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds()));
}
public static FluidValue FormatTimestampSec(FluidValue input, FilterArguments arguments, TemplateContext context)
{
return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds() / 1000));
}
public static FluidValue FormatDate(FluidValue input, FilterArguments arguments, TemplateContext context)
{
if (arguments.Count == 1)
{
return FormatDate(input, x => Format(arguments, x));
}
return input;
}
private static FluidValue FormatDate(FluidValue input, Func<DateTimeOffset, FluidValue> formatter)
{
switch (input)
{
case DateTimeValue dateTime:
{
var value = (DateTimeOffset)dateTime.ToObjectValue();
return formatter(value);
}
case ObjectValue objectValue:
{
var value = objectValue.ToObjectValue();
if (value is Instant instant)
{
return formatter(instant.ToDateTimeOffset());
}
break;
}
}
return input;
}
private static FluidValue Format(FilterArguments arguments, DateTimeOffset value)
{
var formatted = value.ToString(arguments.At(0).ToStringValue());
return new StringValue(formatted);
}
}
}

106
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/JsonArrayFluidValue.cs

@ -0,0 +1,106 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Encodings.Web;
using Fluid;
using Fluid.Values;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
public sealed class JsonArrayFluidValue : FluidValue
{
private readonly JsonArray value;
public override FluidValues Type { get; } = FluidValues.Array;
public JsonArrayFluidValue(JsonArray value)
{
this.value = value;
}
public override bool Equals(FluidValue other)
{
return other is JsonArrayFluidValue array && array.value.Equals(value);
}
public override bool ToBooleanValue()
{
return true;
}
public override decimal ToNumberValue()
{
return 0;
}
public override object ToObjectValue()
{
return new ObjectValue(value);
}
public override string ToStringValue()
{
return value.ToString();
}
protected override FluidValue GetValue(string name, TemplateContext context)
{
switch (name)
{
case "size":
return NumberValue.Create(value.Count);
case "first":
if (value.Count > 0)
{
return Create(value[0]);
}
break;
case "last":
if (value.Count > 0)
{
return Create(value[^1]);
}
break;
}
return NilValue.Instance;
}
protected override FluidValue GetIndex(FluidValue index, TemplateContext context)
{
var i = (int)index.ToNumberValue();
if (i >= 0 && i < value.Count)
{
return Create(value[i]);
}
return NilValue.Instance;
}
public override IEnumerable<FluidValue> Enumerate()
{
foreach (var item in value)
{
yield return Create(item);
}
}
public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
writer.Write(value.ToString());
}
}
}

51
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Fluid;
using Fluid.Values;
using Newtonsoft.Json;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
public sealed class StringFluidExtension : IFluidExtension
{
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
TemplateContext.GlobalFilters.AddFilter("escape", Escape);
TemplateContext.GlobalFilters.AddFilter("slugify", Slugify);
TemplateContext.GlobalFilters.AddFilter("trim", Trim);
}
public static FluidValue Slugify(FluidValue input, FilterArguments arguments, TemplateContext context)
{
if (input is StringValue value)
{
var result = value.ToStringValue().Slugify();
return FluidValue.Create(result);
}
return input;
}
public static FluidValue Escape(FluidValue input, FilterArguments arguments, TemplateContext context)
{
var result = input.ToStringValue();
result = JsonConvert.ToString(result);
result = result[1..^1];
return FluidValue.Create(result);
}
public static FluidValue Trim(FluidValue input, FilterArguments arguments, TemplateContext context)
{
return FluidValue.Create(input.ToStringValue().Trim());
}
}
}

21
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Fluid;
using Fluid.Values;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
public sealed class UserFluidExtension : IFluidExtension
{
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
FluidValue.SetTypeMapping<IUser>(x => new UserFluidValue(x));
}
}
}

75
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidValue.cs

@ -0,0 +1,75 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using Fluid;
using Fluid.Values;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
public sealed class UserFluidValue : FluidValue
{
private readonly IUser value;
public override FluidValues Type { get; } = FluidValues.Object;
public UserFluidValue(IUser value)
{
this.value = value;
}
protected override FluidValue GetValue(string name, TemplateContext context)
{
switch (name)
{
case "id":
return Create(value.Id);
case "email":
return Create(value.Email);
case "name":
return Create(value.DisplayName());
default:
return Create(value.Claims.FirstOrDefault(x => string.Equals(name, x.Type, StringComparison.OrdinalIgnoreCase))?.Value);
}
}
public override bool Equals(FluidValue other)
{
return other is UserFluidValue user && user.value.Id == value.Id;
}
public override bool ToBooleanValue()
{
return true;
}
public override decimal ToNumberValue()
{
return 0;
}
public override object ToObjectValue()
{
return new UserFluidValue(value);
}
public override string ToStringValue()
{
return value.Id;
}
public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
writer.Write(value.Id);
}
}
}

93
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/FluidTemplateEngine.cs

@ -0,0 +1,93 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fluid;
using Fluid.Values;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Templates
{
public sealed class FluidTemplateEngine : ITemplateEngine
{
private readonly IEnumerable<IFluidExtension> extensions;
private sealed class SquidexTemplate : BaseFluidTemplate<SquidexTemplate>
{
public static void Setup(IEnumerable<IFluidExtension> extensions)
{
foreach (var extension in extensions)
{
extension.RegisterLanguageExtensions(Factory);
}
}
public static void SetupTypes(IEnumerable<IFluidExtension> extensions)
{
var globalTypes = TemplateContext.GlobalMemberAccessStrategy;
globalTypes.MemberNameStrategy = MemberNameStrategies.CamelCase;
foreach (var extension in extensions)
{
extension.RegisterGlobalTypes(globalTypes);
}
foreach (var type in SquidexCoreModel.Assembly.GetTypes().Where(x => x.IsEnum))
{
FluidValue.SetTypeMapping(type, x => new StringValue(x.ToString()));
}
globalTypes.Register<NamedId<Guid>>();
globalTypes.Register<NamedId<string>>();
globalTypes.Register<NamedId<long>>();
globalTypes.Register<RefToken>();
}
}
public FluidTemplateEngine(IEnumerable<IFluidExtension> extensions)
{
Guard.NotNull(extensions, nameof(extensions));
this.extensions = extensions;
SquidexTemplate.Setup(extensions);
SquidexTemplate.SetupTypes(extensions);
}
public async Task<string> RenderAsync(string template, TemplateVars variables)
{
Guard.NotNull(variables, nameof(variables));
if (SquidexTemplate.TryParse(template, out var parsed, out var errors))
{
var context = new TemplateContext();
foreach (var extension in extensions)
{
extension.BeforeRun(context);
}
foreach (var (key, value) in variables)
{
context.MemberAccessStrategy.Register(value.GetType());
context.SetValue(key, value);
}
var result = await parsed.RenderAsync(context);
return result;
}
throw new TemplateParseException(template, errors);
}
}
}

26
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/IFluidExtension.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Fluid;
namespace Squidex.Domain.Apps.Core.Templates
{
public interface IFluidExtension
{
void RegisterLanguageExtensions(FluidParserFactory factory)
{
}
void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
}
void BeforeRun(TemplateContext templateContext)
{
}
}
}

16
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/ITemplateEngine.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.Templates
{
public interface ITemplateEngine
{
Task<string> RenderAsync(string template, TemplateVars variables);
}
}

57
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateParseException.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
namespace Squidex.Domain.Apps.Core.Templates
{
[Serializable]
public class TemplateParseException : Exception
{
public IReadOnlyList<string> Errors { get; }
public TemplateParseException(string template, IEnumerable<string> errors)
: base(BuildErrorMessage(errors, template))
{
Errors = errors.ToList();
}
protected TemplateParseException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Errors = (info.GetValue(nameof(Errors), typeof(List<string>)) as List<string>) ?? new List<string>();
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(Errors), Errors.ToList());
}
private static string BuildErrorMessage(IEnumerable<string> errors, string template)
{
var sb = new StringBuilder();
sb.AppendLine("Failed to parse template");
foreach (var error in errors)
{
sb.Append(" * ");
sb.AppendLine(error);
}
sb.AppendLine();
sb.AppendLine("Template:");
sb.AppendLine(template);
return sb.ToString();
}
}
}

15
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateVars.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Templates
{
public sealed class TemplateVars : Dictionary<string, object>
{
}
}

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
@ -90,9 +91,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt));
} }
public async Task EnqueueAsync(RuleJob job, Instant nextAttempt, CancellationToken ct = default) public async Task EnqueueAsync(RuleJob job, Instant? nextAttempt, CancellationToken ct = default)
{ {
var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = job.Created, NextAttempt = nextAttempt });
await Collection.InsertOneIfNotExistsAsync(entity, ct); await Collection.InsertOneIfNotExistsAsync(entity, ct);
} }

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return true; return true;
} }
var context = new ScriptContext var context = new ScriptVars
{ {
["event"] = @event ["event"] = @event
}; };

2
backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs

@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
return true; return true;
} }
var context = new ScriptContext var context = new ScriptVars
{ {
["event"] = @event ["event"] = @event
}; };

45
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -14,62 +14,23 @@ using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>, IRuleEventFormatter public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>
{ {
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly IContentLoader contentLoader; private readonly IContentLoader contentLoader;
private readonly ILocalCache localCache;
public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader, ILocalCache localCache) public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader)
{ {
Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(contentLoader, nameof(contentLoader)); Guard.NotNull(contentLoader, nameof(contentLoader));
Guard.NotNull(localCache, nameof(localCache));
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.contentLoader = contentLoader; this.contentLoader = contentLoader;
this.localCache = localCache;
}
public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path)
{
if (value is JsonArray array &&
array.Count > 0 &&
array[0] is JsonString s &&
Guid.TryParse(s.Value, out var referenceId))
{
return (true, GetReferenceValueAsync(referenceId, path));
}
return default;
}
private async ValueTask<string?> GetReferenceValueAsync(Guid referenceId, string[] path)
{
var reference = await GetContentFromCacheAsync(referenceId);
var (result, remaining) = RuleVariable.GetValue(reference, path);
if (remaining.Length == 0)
{
return result?.ToString();
}
return default;
}
private Task<IContentEntity> GetContentFromCacheAsync(Guid referenceId)
{
var cacheKey = $"FORMAT_REFERENCE_{referenceId}";
return localCache.GetOrCreate(cacheKey, () => contentLoader.GetAsync(referenceId));
} }
protected override async Task<EnrichedContentEvent?> CreateEnrichedEventAsync(Envelope<ContentEvent> @event) protected override async Task<EnrichedContentEvent?> CreateEnrichedEventAsync(Envelope<ContentEvent> @event)
@ -183,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return true; return true;
} }
var context = new ScriptContext var context = new ScriptVars
{ {
["event"] = @event ["event"] = @event
}; };

10
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!c.DoNotScript) if (!c.DoNotScript)
{ {
c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Create, c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Create,
new ScriptContext new ScriptVars
{ {
Operation = "Create", Operation = "Create",
Data = c.Data, Data = c.Data,
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!c.DoNotScript && c.Publish) if (!c.DoNotScript && c.Publish)
{ {
await context.ExecuteScriptAsync(s => s.Change, await context.ExecuteScriptAsync(s => s.Change,
new ScriptContext new ScriptVars
{ {
Operation = "Published", Operation = "Published",
Data = c.Data, Data = c.Data,
@ -157,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!c.DoNotScript) if (!c.DoNotScript)
{ {
await context.ExecuteScriptAsync(s => s.Change, await context.ExecuteScriptAsync(s => s.Change,
new ScriptContext new ScriptVars
{ {
Operation = change.ToString(), Operation = change.ToString(),
Data = Snapshot.Data, Data = Snapshot.Data,
@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!c.DoNotScript) if (!c.DoNotScript)
{ {
await context.ExecuteScriptAsync(s => s.Delete, await context.ExecuteScriptAsync(s => s.Delete,
new ScriptContext new ScriptVars
{ {
Operation = "Delete", Operation = "Delete",
Data = Snapshot.Data, Data = Snapshot.Data,
@ -236,7 +236,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!command.DoNotScript) if (!command.DoNotScript)
{ {
newData = await context.ExecuteScriptAndTransformAsync(s => s.Update, newData = await context.ExecuteScriptAndTransformAsync(s => s.Update,
new ScriptContext new ScriptVars
{ {
Operation = "Create", Operation = "Create",
Data = newData, Data = newData,

6
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptContext context) public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptVars context)
{ {
Enrich(context); Enrich(context);
@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return await scriptEngine.ExecuteAndTransformAsync(context, actualScript); return await scriptEngine.ExecuteAndTransformAsync(context, actualScript);
} }
public async Task ExecuteScriptAsync(Func<SchemaScripts, string> script, ScriptContext context) public async Task ExecuteScriptAsync(Func<SchemaScripts, string> script, ScriptVars context)
{ {
Enrich(context); Enrich(context);
@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await scriptEngine.ExecuteAsync(context, GetScript(script)); await scriptEngine.ExecuteAsync(context, GetScript(script));
} }
private void Enrich(ScriptContext context) private void Enrich(ScriptVars context)
{ {
context.ContentId = command.ContentId; context.ContentId = command.ContentId;
context.AppId = app.Id; context.AppId = app.Id;

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterScriptExtension.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs

@ -13,11 +13,11 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Counter namespace Squidex.Domain.Apps.Entities.Contents.Counter
{ {
public sealed class CounterScriptExtension : IScriptExtension public sealed class CounterJintExtension : IJintExtension
{ {
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
public CounterScriptExtension(IGrainFactory grainFactory) public CounterJintExtension(IGrainFactory grainFactory)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));

2
backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!string.IsNullOrWhiteSpace(condition?.Expression)) if (!string.IsNullOrWhiteSpace(condition?.Expression))
{ {
var context = new ScriptContext var context = new ScriptVars
{ {
["data"] = data ["data"] = data
}; };

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
private async Task TransformAsync(Context context, string script, ContentEntity content) private async Task TransformAsync(Context context, string script, ContentEntity content)
{ {
var scriptContext = new ScriptContext var scriptContext = new ScriptVars
{ {
ContentId = content.Id, ContentId = content.Id,
Data = content.Data, Data = content.Data,

99
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs

@ -0,0 +1,99 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Fluid;
using Fluid.Ast;
using Fluid.Tags;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ReferencesFluidExtension : IFluidExtension
{
private readonly IContentQueryService contentQueryService;
private readonly IAppProvider appProvider;
private sealed class ReferenceTag : ArgumentsTag
{
private readonly IContentQueryService contentQueryService;
private readonly IAppProvider appProvider;
public ReferenceTag(IContentQueryService contentQueryService, IAppProvider appProvider)
{
this.contentQueryService = contentQueryService;
this.appProvider = appProvider;
}
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments)
{
if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent)
{
var app = await appProvider.GetAppAsync(enrichedEvent.AppId.Id);
if (app == null)
{
return Completion.Normal;
}
var appContext =
Context.Admin()
.WithoutContentEnrichment()
.WithoutCleanup()
.WithUnpublished();
appContext.App = app;
var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue();
if (Guid.TryParse(id, out var guid))
{
var references = await contentQueryService.QueryAsync(appContext, new List<Guid> { guid });
var reference = references.FirstOrDefault();
if (reference != null)
{
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue();
context.SetValue(name, reference);
}
}
}
return Completion.Normal;
}
}
public ReferencesFluidExtension(IContentQueryService contentQueryService, IAppProvider appProvider)
{
Guard.NotNull(contentQueryService, nameof(contentQueryService));
Guard.NotNull(appProvider, nameof(appProvider));
this.contentQueryService = contentQueryService;
this.appProvider = appProvider;
}
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
memberAccessStrategy.Register<IContentEntity>();
}
public void RegisterLanguageExtensions(FluidParserFactory factory)
{
factory.RegisterTag("reference", new ReferenceTag(contentQueryService, appProvider));
}
}
}

16
backend/src/Squidex.Domain.Apps.Entities/Context.cs

@ -13,6 +13,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using P = Squidex.Shared.Permissions;
using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet; using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
@ -48,7 +49,20 @@ namespace Squidex.Domain.Apps.Entities
public static Context Anonymous() public static Context Anonymous()
{ {
return new Context(new ClaimsPrincipal(new ClaimsIdentity())); var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
return new Context(claimsPrincipal);
}
public static Context Admin()
{
var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, P.All));
return new Context(claimsPrincipal);
} }
public Context Clone() public Context Clone()

2
backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories
{ {
Task UpdateAsync(RuleJob job, RuleJobUpdate update); Task UpdateAsync(RuleJob job, RuleJobUpdate update);
Task EnqueueAsync(RuleJob job, Instant nextAttempt, CancellationToken ct = default); Task EnqueueAsync(RuleJob job, Instant? nextAttempt, CancellationToken ct = default);
Task EnqueueAsync(Guid id, Instant nextAttempt); Task EnqueueAsync(Guid id, Instant nextAttempt);

17
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -76,7 +76,22 @@ namespace Squidex.Domain.Apps.Entities.Rules
foreach (var job in jobs) foreach (var job in jobs)
{ {
await ruleEventRepository.EnqueueAsync(job, job.Created); if (job.Exception != null)
{
await ruleEventRepository.EnqueueAsync(job, null);
await ruleEventRepository.UpdateAsync(job, new RuleJobUpdate
{
JobResult = RuleJobResult.Failed,
ExecutionResult = RuleResult.Failed,
ExecutionDump = job.Exception.ToString(),
Finished = job.Created
});
}
else
{
await ruleEventRepository.EnqueueAsync(job, job.Created);
}
} }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs

@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
return true; return true;
} }
var context = new ScriptContext var context = new ScriptVars
{ {
["event"] = @event ["event"] = @event
}; };

30
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -18,6 +18,8 @@ using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.Templates.Extensions;
using Squidex.Domain.Apps.Entities.Contents.Counter; using Squidex.Domain.Apps.Entities.Contents.Counter;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
@ -60,17 +62,29 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<JintScriptEngine>() services.AddSingletonAs<JintScriptEngine>()
.AsOptional<IScriptEngine>(); .AsOptional<IScriptEngine>();
services.AddSingletonAs<CounterScriptExtension>() services.AddSingletonAs<CounterJintExtension>()
.As<IScriptExtension>(); .As<IJintExtension>();
services.AddSingletonAs<DateTimeScriptExtension>() services.AddSingletonAs<DateTimeJintExtension>()
.As<IScriptExtension>(); .As<IJintExtension>();
services.AddSingletonAs<StringScriptExtension>() services.AddSingletonAs<StringJintExtension>()
.As<IScriptExtension>(); .As<IJintExtension>();
services.AddSingletonAs<HttpScriptExtension>() services.AddSingletonAs<HttpJintExtension>()
.As<IScriptExtension>(); .As<IJintExtension>();
services.AddSingletonAs<FluidTemplateEngine>()
.AsOptional<ITemplateEngine>();
services.AddSingletonAs<ContentFluidExtension>()
.AsOptional<IFluidExtension>();
services.AddSingletonAs<DateTimeFluidExtension>()
.AsOptional<IFluidExtension>();
services.AddSingletonAs<UserFluidExtension>()
.AsOptional<IFluidExtension>();
services.AddSingleton<Func<IIncomingGrainCallContext, string>>(DomainObjectGrainFormatter.Format); services.AddSingleton<Func<IIncomingGrainCallContext, string>>(DomainObjectGrainFormatter.Format);
} }

15
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -8,8 +8,9 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Scripting; using Squidex.Domain.Apps.Core.HandleRules.Extensions;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
@ -44,7 +45,10 @@ namespace Squidex.Config.Domain
.As<IRuleTriggerHandler>(); .As<IRuleTriggerHandler>();
services.AddSingletonAs<ContentChangedTriggerHandler>() services.AddSingletonAs<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>().As<IRuleEventFormatter>(); .As<IRuleTriggerHandler>();
services.AddSingletonAs<ReferencesFluidExtension>()
.As<IFluidExtension>();
services.AddSingletonAs<ManualTriggerHandler>() services.AddSingletonAs<ManualTriggerHandler>()
.As<IRuleTriggerHandler>(); .As<IRuleTriggerHandler>();
@ -70,8 +74,11 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleRegistry>() services.AddSingletonAs<RuleRegistry>()
.As<ITypeProvider>().AsSelf(); .As<ITypeProvider>().AsSelf();
services.AddSingletonAs<EventScriptExtension>() services.AddSingletonAs<EventJintExtension>()
.As<IScriptExtension>(); .As<IJintExtension>();
services.AddSingletonAs<EventFluidExtensions>()
.As<IFluidExtension>();
services.AddSingletonAs<PredefinedPatternsFormatter>() services.AddSingletonAs<PredefinedPatternsFormatter>()
.As<IRuleEventFormatter>(); .As<IRuleEventFormatter>();

60
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs

@ -0,0 +1,60 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Reflection;
using Xunit.Sdk;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
public sealed class ExpressionsAttribute : DataAttribute
{
private readonly string? script;
private readonly string? interpolationOld;
private readonly string? interpolationNew;
private readonly string? liquid;
public ExpressionsAttribute(string? interpolationOld, string? interpolationNew, string? script, string? liquid)
{
this.liquid = liquid;
this.interpolationOld = interpolationOld;
this.interpolationNew = interpolationNew;
this.script = script;
}
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
if (interpolationOld != null)
{
yield return new object[] { interpolationOld };
}
if (interpolationNew != null)
{
yield return new object[] { interpolationNew };
}
if (script != null)
{
yield return new object[]
{
string.Format("Script(`{0}`)", script)
};
}
if (liquid != null)
{
yield return new object[]
{
string.Format("Liquid({0})", liquid)
};
}
}
}
}

793
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs

@ -0,0 +1,793 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Extensions;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.Templates.Extensions;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
public class RuleEventFormatterCompareTests
{
private readonly IUser user = A.Fake<IUser>();
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
private readonly Guid contentId = Guid.NewGuid();
private readonly Guid assetId = Guid.NewGuid();
private readonly RuleEventFormatter sut;
private class FakeContentResolver : IRuleEventFormatter
{
public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path)
{
if (path[0] == "data" && value is JsonArray _)
{
return (true, GetValueAsync());
}
return default;
}
private async ValueTask<string?> GetValueAsync()
{
await Task.Delay(5);
return "Reference";
}
}
public RuleEventFormatterCompareTests()
{
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.Returns("content-url");
A.CallTo(() => urlGenerator.AssetContent(assetId))
.Returns("asset-content-url");
A.CallTo(() => user.Id)
.Returns("user123");
A.CallTo(() => user.Email)
.Returns("me@email.com");
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") });
JintScriptEngine scriptEngine = BuildScriptEngine();
var formatters = new IRuleEventFormatter[]
{
new PredefinedPatternsFormatter(urlGenerator),
new FakeContentResolver()
};
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine());
}
private FluidTemplateEngine BuildTemplateEngine()
{
var extensions = new IFluidExtension[]
{
new ContentFluidExtension(),
new DateTimeFluidExtension(),
new EventFluidExtensions(urlGenerator),
new StringFluidExtension(),
new UserFluidExtension()
};
return new FluidTemplateEngine(extensions);
}
private JintScriptEngine BuildScriptEngine()
{
var extensions = new IJintExtension[]
{
new DateTimeJintExtension(),
new EventJintExtension(urlGenerator),
new StringJintExtension()
};
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
return new JintScriptEngine(cache, extensions);
}
[Theory]
[Expressions(
"Name $APP_NAME has id $APP_ID",
"Name ${EVENT_APPID.NAME} has id ${EVENT_APPID.ID}",
"Name ${event.appId.name} has id ${event.appId.id}",
"Name {{event.appId.name}} has id {{event.appId.id}}"
)]
public async Task Should_format_app_information_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Name my-app has id {appId.Id}", result);
}
[Theory]
[Expressions(
"Name $SCHEMA_NAME has id $SCHEMA_ID",
"Name ${EVENT_SCHEMAID.NAME} has id ${EVENT_SCHEMAID.ID}",
"Name ${event.schemaId.name} has id ${event.schemaId.id}",
"Name {{event.schemaId.name}} has id {{event.schemaId.id}}"
)]
public async Task Should_format_schema_information_from_event(string script)
{
var @event = new EnrichedContentEvent { SchemaId = schemaId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Name my-schema has id {schemaId.Id}", result);
}
[Theory]
[Expressions(
"DateTime: $TIMESTAMP_DATETIME",
null,
"DateTime: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}",
"DateTime: {{event.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss'}}"
)]
public async Task Should_format_timestamp_information_from_event(string script)
{
var @event = new EnrichedContentEvent { Timestamp = now };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"DateTime: {now:yyyy-MM-dd-hh-mm-ss}", result);
}
[Theory]
[Expressions(
"Date: $TIMESTAMP_DATE",
null,
"Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}",
"Date: {{event.timestamp | format_date: 'yyyy-MM-dd'}}"
)]
public async Task Should_format_timestamp_date_information_from_event(string script)
{
var @event = new EnrichedContentEvent { Timestamp = now };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Date: {now:yyyy-MM-dd}", result);
}
[Theory]
[Expressions(
"From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)",
"From ${EVENT_MENTIONEDUSER.NAME} (${EVENT_MENTIONEDUSER.EMAIL}, ${EVENT_MENTIONEDUSER.ID})",
"From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})",
"From {{event.mentionedUser.name}} ({{event.mentionedUser.email}}, {{event.mentionedUser.id}})"
)]
public async Task Should_format_email_and_display_name_from_mentioned_user(string script)
{
var @event = new EnrichedCommentEvent { MentionedUser = user };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From me (me@email.com, user123)", result);
}
[Theory]
[Expressions(
"From $USER_NAME ($USER_EMAIL, $USER_ID)",
"From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})",
"From ${event.user.name} (${event.user.email}, ${event.user.id})",
"From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})"
)]
public async Task Should_format_email_and_display_name_from_user(string script)
{
var @event = new EnrichedContentEvent { User = user };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From me (me@email.com, user123)", result);
}
[Theory]
[Expressions(
"From $USER_NAME ($USER_EMAIL, $USER_ID)",
"From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})",
"From ${event.user.name} (${event.user.email}, ${event.user.id})",
"From {{event.user.name | default: 'null'}} ({{event.user.email | default: 'null'}}, {{event.user.id | default: 'null'}})"
)]
public async Task Should_return_null_if_user_is_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From null (null, null)", result);
}
[Theory]
[Expressions(
"From $USER_NAME ($USER_EMAIL, $USER_ID)",
"From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})",
"From ${event.user.name} (${event.user.email}, ${event.user.id})",
"From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})"
)]
public async Task Should_format_email_and_display_name_from_client(string script)
{
var @event = new EnrichedContentEvent { User = new ClientUser(new RefToken(RefTokenType.Client, "android")) };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From client:android (client:android, android)", result);
}
[Theory]
[Expressions(
"Version: $ASSET_VERSION",
"Version: ${EVENT_VERSION}",
"Version: ${event.version}",
"Version: {{event.version}}"
)]
public async Task Should_format_base_property(string script)
{
var @event = new EnrichedAssetEvent { Version = 13 };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Version: 13", result);
}
[Theory]
[Expressions(
"File: $ASSET_FILENAME",
"File: ${EVENT_FILENAME}",
"File: ${event.fileName}",
"File: {{event.fileName}}"
)]
public async Task Should_format_asset_file_name_from_event(string script)
{
var @event = new EnrichedAssetEvent { FileName = "my-file.png" };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("File: my-file.png", result);
}
[Theory]
[Expressions(
"Type: $ASSSET_ASSETTYPE",
"Type: ${EVENT_ASSETTYPE}",
"Type: ${event.assetType}",
"Type: {{event.assetType}}"
)]
public async Task Should_format_asset_asset_type_from_event(string script)
{
var @event = new EnrichedAssetEvent { AssetType = AssetType.Audio };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Type: Audio", result);
}
[Theory]
[Expressions(
"Download at $ASSET_CONTENT_URL",
null,
"Download at ${assetContentUrl()}",
"Download at {{event.id | assetContentUrl}}"
)]
public async Task Should_format_asset_content_url_from_event(string script)
{
var @event = new EnrichedAssetEvent { Id = assetId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at asset-content-url", result);
}
[Theory]
[Expressions(
"Download at $ASSET_CONTENT_URL",
null,
"Download at ${assetContentUrl()}",
"Download at {{event.id | assetContentUrl | default: 'null'}}"
)]
public async Task Should_return_null_when_asset_content_url_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at null", result);
}
[Theory]
[Expressions(
"Go to $CONTENT_URL",
null,
"Go to ${contentUrl()}",
"Go to {{event.id | contentUrl | default: 'null'}}"
)]
public async Task Should_format_content_url_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Go to content-url", result);
}
[Theory]
[Expressions(
"Go to $CONTENT_URL",
null,
"Go to ${contentUrl()}",
"Go to {{event.id | contentUrl | default: 'null'}}"
)]
public async Task Should_return_null_when_content_url_when_not_found(string script)
{
var @event = new EnrichedAssetEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Go to null", result);
}
[Theory]
[Expressions(
"$CONTENT_STATUS",
"${EVENT_STATUS}",
"${contentAction()}",
"{{event.status}}"
)]
public async Task Should_format_content_status_when_found(string script)
{
var @event = new EnrichedContentEvent { Status = Status.Published };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Published", result);
}
[Theory]
[Expressions(
"$CONTENT_STATUS",
"${EVENT_STATUS}",
"${contentAction()}",
"{{event.status | default: 'null'}}"
)]
public async Task Should_return_null_when_content_status_not_found(string script)
{
var @event = new EnrichedAssetEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[Expressions(
"$CONTENT_ACTION",
"${EVENT_TYPE}",
"${event.type}",
"{{event.type}}"
)]
public async Task Should_format_content_actions_when_found(string script)
{
var @event = new EnrichedContentEvent { Type = EnrichedContentEventType.Created };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Created", result);
}
[Theory]
[Expressions(
"$CONTENT_STATUS",
"${CONTENT_STATUS}",
"${contentAction()}",
null
)]
public async Task Should_return_null_when_content_action_not_found(string script)
{
var @event = new EnrichedAssetEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.country.iv",
"${CONTENT_DATA.country.iv}",
"${event.data.country.iv}",
"{{event.data.country.iv | default: 'null'}}"
)]
public async Task Should_return_null_when_field_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", "Berlin"))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.country.iv",
"${CONTENT_DATA.country.iv}",
"${event.data.country.iv}",
"{{event.data.country.iv | default: 'null'}}"
)]
public async Task Should_return_null_when_partition_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", "Berlin"))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.country.iv.10",
"${CONTENT_DATA.country.iv.10}",
"${event.data.country.iv[10]}",
"{{event.data.country.iv[10] | default: 'null'}}"
)]
public async Task Should_return_null_when_array_item_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Array()))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.country.iv.Location",
"${CONTENT_DATA.country.iv.Location}",
"${event.data.country.iv.Location}",
"{{event.data.country.iv.Location | default: 'null'}}"
)]
public async Task Should_return_null_when_property_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.city.iv",
"${CONTENT_DATA.city.iv}",
"${event.data.city.iv}",
"{{event.data.city.iv}}"
)]
public async Task Should_return_plain_value_when_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", "Berlin"))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Berlin", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.city.iv.0",
"${CONTENT_DATA.city.iv.0}",
"${event.data.city.iv[0]}",
"{{event.data.city.iv[0]}}"
)]
public async Task Should_return_plain_value_from_array_when_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Array("Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Berlin", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.city.iv.name",
"${CONTENT_DATA.city.iv.name}",
"${event.data.city.iv.name}",
"{{event.data.city.iv.name}}"
)]
public async Task Should_return_plain_value_from_object_when_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Berlin", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.city.iv",
"${CONTENT_DATA.city.iv}",
"${JSON.stringify(event.data.city.iv)}",
"{{event.data.city.iv}}"
)]
public async Task Should_return_json_string_when_object(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("{\"name\":\"Berlin\"}", result);
}
[Theory]
[Expressions(
"$CONTENT_DATA.city.iv",
"${CONTENT_DATA.city.iv}",
"${JSON.stringify(event.data.city.iv)}",
"{{event.data.city.iv}}"
)]
public async Task Should_return_json_string_when_array(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(1, 2, 3)))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty));
}
[Theory]
[Expressions(
null,
"From ${EVENT_ACTOR}",
"From ${event.actor}",
"From {{event.actor}}"
)]
public async Task Should_format_actor(string script)
{
var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From client:android", result);
}
[Theory]
[Expressions(
null,
"${ASSET_LASTMODIFIED | timestamp}",
"${event.lastModified.getTime()}",
"{{event.lastModified | timestamp}}"
)]
public async Task Should_transform_timestamp(string script)
{
var @event = new EnrichedAssetEvent { LastModified = Instant.FromUnixTimeSeconds(1590769584) };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("1590769584000", result);
}
[Theory]
[Expressions(
null,
"${ASSET_LASTMODIFIED | timestamp_sec}",
"${event.lastModified.getTime() / 1000}",
"{{event.lastModified | timestamp_sec}}"
)]
public async Task Should_transform_timestamp_seconds(string script)
{
var @event = new EnrichedAssetEvent { LastModified = Instant.FromUnixTimeSeconds(1590769584) };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("1590769584", result);
}
[Theory]
[Expressions(
"${USER_NAME | Upper}",
"${EVENT_USER.NAME | Upper}",
"${event.user.name.toUpperCase()}",
"{{event.user.name | upcase}}"
)]
public async Task Should_transform_upper(string script)
{
var @event = new EnrichedContentEvent { User = user };
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") });
var result = await sut.FormatAsync(script, @event);
Assert.Equal("DONALD DUCK", result);
}
[Theory]
[Expressions(
"${USER_NAME | Lower}",
"${EVENT_USER.NAME | Lower}",
"${event.user.name.toLowerCase()}",
"{{event.user.name | downcase}}"
)]
public async Task Should_transform_lower(string script)
{
var @event = new EnrichedContentEvent { User = user };
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") });
var result = await sut.FormatAsync(script, @event);
Assert.Equal("donald duck", result);
}
[Theory]
[Expressions(
"${USER_NAME | Trim}",
"${EVENT_USER.NAME | Trim}",
"${event.user.name.trim()}",
"{{event.user.name | trim}}"
)]
public async Task Should_transform_trimmed(string script)
{
var @event = new EnrichedContentEvent { User = user };
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") });
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Donald Duck", result);
}
[Theory]
[Expressions(
"${USER_NAME | Slugify}",
"${EVENT_USER.NAME | Slugify}",
"${slugify(event.user.name)}",
"{{event.user.name | slugify}}"
)]
public async Task Should_transform_slugify(string script)
{
var @event = new EnrichedContentEvent { User = user };
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") });
var result = await sut.FormatAsync(script, @event);
Assert.Equal("donald-duck", result);
}
[Theory]
[Expressions(
"${USER_NAME | Upper | Trim}",
"${EVENT_USER.NAME | Upper | Trim}",
"${event.user.name.toUpperCase().trim()}",
"{{event.user.name | upcase | trim}}"
)]
public async Task Should_transform_chained(string script)
{
var @event = new EnrichedContentEvent { User = user };
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") });
var result = await sut.FormatAsync(script, @event);
Assert.Equal("DONALD DUCK", result);
}
[Theory]
[Expressions(
"${USER_NAME | Escape}",
"${EVENT_USER.NAME | Escape}",
null,
"{{event.user.name | escape}}"
)]
public async Task Should_transform_json_escape(string script)
{
var @event = new EnrichedContentEvent { User = user };
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald\"Duck") });
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Donald\\\"Duck", result);
}
}
}

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

@ -12,14 +12,14 @@ using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Scripting; using Squidex.Domain.Apps.Core.HandleRules.Extensions;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.Templates.Extensions;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -34,7 +34,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
private readonly Guid contentId = Guid.NewGuid(); private readonly Guid contentId = Guid.NewGuid();
private readonly Guid assetId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid();
private readonly RuleEventFormatter sut; private readonly RuleEventFormatter sut;
@ -76,14 +75,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => user.Claims) A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") }); .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") });
var extensions = new IScriptExtension[] JintScriptEngine scriptEngine = BuildScriptEngine();
{
new DateTimeScriptExtension(),
new EventScriptExtension(urlGenerator),
new StringScriptExtension()
};
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
var formatters = new IRuleEventFormatter[] var formatters = new IRuleEventFormatter[]
{ {
@ -91,7 +83,32 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new FakeContentResolver() new FakeContentResolver()
}; };
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, new JintScriptEngine(cache, extensions)); sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine());
}
private static FluidTemplateEngine BuildTemplateEngine()
{
var extensions = new IFluidExtension[]
{
new DateTimeFluidExtension(),
new UserFluidExtension()
};
return new FluidTemplateEngine(extensions);
}
private JintScriptEngine BuildScriptEngine()
{
var extensions = new IJintExtension[]
{
new DateTimeJintExtension(),
new EventJintExtension(urlGenerator),
new StringJintExtension()
};
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
return new JintScriptEngine(cache, extensions);
} }
[Fact] [Fact]
@ -122,389 +139,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Contains("MyEventName", result); Assert.Contains("MyEventName", result);
} }
[Theory]
[InlineData("Name $APP_NAME has id $APP_ID")]
[InlineData("Name ${EVENT_APPID.NAME} has id ${EVENT_APPID.ID}")]
[InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")]
public async Task Should_format_app_information_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Name my-app has id {appId.Id}", result);
}
[Theory]
[InlineData("Name $SCHEMA_NAME has id $SCHEMA_ID")]
[InlineData("Script(`Name ${event.schemaId.name} has id ${event.schemaId.id}`)")]
public async Task Should_format_schema_information_from_event(string script)
{
var @event = new EnrichedContentEvent { SchemaId = schemaId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Name my-schema has id {schemaId.Id}", result);
}
[Theory]
[InlineData("Full: $TIMESTAMP_DATETIME")]
[InlineData("Script(`Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")]
public async Task Should_format_timestamp_information_from_event(string script)
{
var @event = new EnrichedContentEvent { Timestamp = now };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Full: {now:yyyy-MM-dd-hh-mm-ss}", result);
}
[Theory]
[InlineData("Date: $TIMESTAMP_DATE")]
[InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}`)")]
public async Task Should_format_timestamp_date_information_from_event(string script)
{
var @event = new EnrichedContentEvent { Timestamp = now };
var result = await sut.FormatAsync(script, @event);
Assert.Equal($"Date: {now:yyyy-MM-dd}", result);
}
[Theory]
[InlineData("From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)")]
[InlineData("From ${COMMENT_MENTIONEDUSER.NAME} (${COMMENT_MENTIONEDUSER.EMAIL}, ${COMMENT_MENTIONEDUSER.ID})")]
[InlineData("Script(`From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})`)")]
public async Task Should_format_email_and_display_name_from_mentioned_user(string script)
{
var @event = new EnrichedCommentEvent { MentionedUser = user };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From me (me@email.com, user123)", result);
}
[Theory]
[InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]
public async Task Should_format_email_and_display_name_from_user(string script)
{
var @event = new EnrichedContentEvent { User = user };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From me (me@email.com, user123)", result);
}
[Theory]
[InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]
public async Task Should_return_null_if_user_is_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From null (null, null)", result);
}
[Theory]
[InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")]
[InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")]
public async Task Should_format_email_and_display_name_from_client(string script)
{
var @event = new EnrichedContentEvent { User = new ClientUser(new RefToken(RefTokenType.Client, "android")) };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From client:android (client:android, android)", result);
}
[Theory]
[InlineData("Version: $ASSET_VERSION")]
[InlineData("Script(`Version: ${event.version}`)")]
public async Task Should_format_base_property(string script)
{
var @event = new EnrichedAssetEvent { Version = 13 };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Version: 13", result);
}
[Theory]
[InlineData("File: $ASSET_FILENAME")]
[InlineData("Script(`File: ${event.fileName}`)")]
public async Task Should_format_asset_file_name_from_event(string script)
{
var @event = new EnrichedAssetEvent { FileName = "my-file.png" };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("File: my-file.png", result);
}
[Theory]
[InlineData("Type: $ASSET_ASSETTYPE")]
[InlineData("Script(`Type: ${event.assetType}`)")]
public async Task Should_format_asset_asset_type_from_event(string script)
{
var @event = new EnrichedAssetEvent { AssetType = AssetType.Audio };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Type: Audio", result);
}
[Theory]
[InlineData("Download at $ASSET_CONTENT_URL")]
[InlineData("Script(`Download at ${assetContentUrl()}`)")]
public async Task Should_format_asset_content_url_from_event(string script)
{
var @event = new EnrichedAssetEvent { Id = assetId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at asset-content-url", result);
}
[Theory]
[InlineData("Download at $ASSET_CONTENT_URL")]
[InlineData("Script(`Download at ${assetContentUrl()}`)")]
public async Task Should_return_null_when_asset_content_url_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at null", result);
}
[Theory]
[InlineData("Go to $CONTENT_URL")]
[InlineData("Script(`Go to ${contentUrl()}`)")]
public async Task Should_format_content_url_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Go to content-url", result);
}
[Theory]
[InlineData("Go to $CONTENT_URL")]
[InlineData("Script(`Go to ${contentUrl()}`)")]
public async Task Should_return_null_when_content_url_when_not_found(string script)
{
var @event = new EnrichedAssetEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Go to null", result);
}
[Theory]
[InlineData("$CONTENT_STATUS")]
[InlineData("Script(contentAction())")]
[InlineData("Script(`${event.status}`)")]
public async Task Should_format_content_status_when_found(string script)
{
var @event = new EnrichedContentEvent { Status = Status.Published };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Published", result);
}
[Theory]
[InlineData("$CONTENT_ACTION")]
[InlineData("Script(contentAction())")]
public async Task Should_return_null_when_content_status_not_found(string script)
{
var @event = new EnrichedAssetEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[InlineData("$CONTENT_ACTION")]
[InlineData("Script(`${event.type}`)")]
public async Task Should_format_content_actions_when_found(string script)
{
var @event = new EnrichedContentEvent { Type = EnrichedContentEventType.Created };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Created", result);
}
[Theory]
[InlineData("$CONTENT_ACTION")]
[InlineData("Script(contentAction())")]
public async Task Should_return_null_when_content_action_not_found(string script)
{
var @event = new EnrichedAssetEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[InlineData("$CONTENT_DATA.country.iv")]
[InlineData("Script(`${event.data.country.iv}`)")]
public async Task Should_return_null_when_field_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", "Berlin"))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.de")]
[InlineData("Script(`${event.data.country.iv}`)")]
public async Task Should_return_null_when_partition_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", "Berlin"))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.iv.10")]
[InlineData("Script(`${event.data.country.de[10]}`)")]
public async Task Should_return_null_when_array_item_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Array()))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.de.Name")]
[InlineData("Script(`${event.data.city.de.Location}`)")]
public async Task Should_return_null_when_property_not_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("null", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.iv")]
[InlineData("Script(`${event.data.city.iv}`)")]
public async Task Should_return_plain_value_when_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddValue("iv", "Berlin"))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Berlin", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.iv.0")]
[InlineData("Script(`${event.data.city.iv[0]}`)")]
public async Task Should_return_plain_value_from_array_when_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Array("Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Berlin", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.iv.name")]
[InlineData("Script(`${event.data.city.iv.name}`)")]
public async Task Should_return_plain_value_from_object_when_found(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Berlin", result);
}
[Theory]
[InlineData("$CONTENT_DATA.city.iv")]
[InlineData("Script(`${JSON.stringify(event.data.city.iv)}`)")]
public async Task Should_return_json_string_when_object(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("{\"name\":\"Berlin\"}", result);
}
[Fact] [Fact]
public async Task Should_resolve_reference() public async Task Should_resolve_reference()
{ {
@ -522,17 +156,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal("Reference", result); Assert.Equal("Reference", result);
} }
[Theory]
[InlineData("Script(`From ${event.actor}`)")]
public async Task Should_format_actor(string script)
{
var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("From client:android", result);
}
[Theory] [Theory]
[InlineData("${EVENT_INVALID ? file}", "file")] [InlineData("${EVENT_INVALID ? file}", "file")]
public async Task Should_provide_fallback_if_path_is_invalid(string script, string expect) public async Task Should_provide_fallback_if_path_is_invalid(string script, string expect)
@ -615,18 +238,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal(expect, result); Assert.Equal(expect, result);
} }
[Theory]
[InlineData("${ASSET_LASTMODIFIED | timestamp_seconds}", "1590769584")]
[InlineData("${ASSET_LASTMODIFIED | timestamp_ms}", "1590769584000")]
public async Task Should_transform_timestamp(string script, string expect)
{
var @event = new EnrichedAssetEvent { LastModified = Instant.FromUnixTimeSeconds(1590769584) };
var result = await sut.FormatAsync(script, @event);
Assert.Equal(expect, result);
}
[Fact] [Fact]
public async Task Should_format_json() public async Task Should_format_json()
{ {

36
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -29,11 +29,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public JintScriptEngineHelperTests() public JintScriptEngineHelperTests()
{ {
var extensions = new IScriptExtension[] var extensions = new IJintExtension[]
{ {
new DateTimeScriptExtension(), new DateTimeJintExtension(),
new HttpScriptExtension(httpClientFactory), new HttpJintExtension(httpClientFactory),
new StringScriptExtension() new StringJintExtension()
}; };
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
@ -51,12 +51,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
return toCamelCase(value); return toCamelCase(value);
"; ";
var context = new ScriptContext var vars = new ScriptVars
{ {
["value"] = "Hello World" ["value"] = "Hello World"
}; };
var result = sut.Interpolate(context, script); var result = sut.Interpolate(vars, script);
Assert.Equal("helloWorld", result); Assert.Equal("helloWorld", result);
} }
@ -68,12 +68,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
return toPascalCase(value); return toPascalCase(value);
"; ";
var context = new ScriptContext var vars = new ScriptVars
{ {
["value"] = "Hello World" ["value"] = "Hello World"
}; };
var result = sut.Interpolate(context, script); var result = sut.Interpolate(vars, script);
Assert.Equal("HelloWorld", result); Assert.Equal("HelloWorld", result);
} }
@ -85,12 +85,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
return slugify(value); return slugify(value);
"; ";
var context = new ScriptContext var vars = new ScriptVars
{ {
["value"] = "4 Häuser" ["value"] = "4 Häuser"
}; };
var result = sut.Interpolate(context, script); var result = sut.Interpolate(vars, script);
Assert.Equal("4-haeuser", result); Assert.Equal("4-haeuser", result);
} }
@ -102,12 +102,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
return slugify(value, true); return slugify(value, true);
"; ";
var context = new ScriptContext var vars = new ScriptVars
{ {
["value"] = "4 Häuser" ["value"] = "4 Häuser"
}; };
var result = sut.Interpolate(context, script); var result = sut.Interpolate(vars, script);
Assert.Equal("4-hauser", result); Assert.Equal("4-hauser", result);
} }
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
reject() reject()
"; ";
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script)); var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script));
Assert.Empty(ex.Errors); Assert.Empty(ex.Errors);
} }
@ -131,7 +131,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
reject('Not valid') reject('Not valid')
"; ";
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script)); var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script));
Assert.Equal("Not valid", ex.Errors.Single().Message); Assert.Equal("Not valid", ex.Errors.Single().Message);
} }
@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
disallow() disallow()
"; ";
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptContext(), script)); var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptVars(), script));
Assert.Equal("Not allowed", ex.Message); Assert.Equal("Not allowed", ex.Message);
} }
@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
disallow('Operation not allowed') disallow('Operation not allowed')
"; ";
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptContext(), script)); var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptVars(), script));
Assert.Equal("Operation not allowed", ex.Message); Assert.Equal("Operation not allowed", ex.Message);
} }
@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
}); });
"; ";
var result = await sut.GetAsync(new ScriptContext(), script); var result = await sut.GetAsync(new ScriptVars(), script);
httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/"); httpHandler.ShouldBeUrl("http://squidex.io/");
@ -201,7 +201,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
}, headers); }, headers);
"; ";
var result = await sut.GetAsync(new ScriptContext(), script); var result = await sut.GetAsync(new ScriptVars(), script);
httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/"); httpHandler.ShouldBeUrl("http://squidex.io/");

44
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -29,11 +29,11 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public JintScriptEngineTests() public JintScriptEngineTests()
{ {
var extensions = new IScriptExtension[] var extensions = new IJintExtension[]
{ {
new DateTimeScriptExtension(), new DateTimeJintExtension(),
new HttpScriptExtension(httpClientFactory), new HttpJintExtension(httpClientFactory),
new StringScriptExtension() new StringJintExtension()
}; };
var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
invalid() invalid()
"; ";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script));
} }
[Fact] [Fact]
@ -71,14 +71,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
throw 'Error'; throw 'Error';
"; ";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script));
} }
[Fact] [Fact]
public async Task TransformAsync_should_return_original_content_when_script_failed() public async Task TransformAsync_should_return_original_content_when_script_failed()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptVars { Data = content };
const string script = @" const string script = @"
x => x x => x
@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 10.0)); .AddValue("iv", 10.0));
var context = new ScriptContext { Data = content }; var context = new ScriptVars { Data = content };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -134,14 +134,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
throw 'Error'; throw 'Error';
"; ";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAndTransformAsync(new ScriptContext(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAndTransformAsync(new ScriptVars(), script));
} }
[Fact] [Fact]
public async Task ExecuteAndTransformAsync_should_throw_when_script_failed() public async Task ExecuteAndTransformAsync_should_throw_when_script_failed()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptVars { Data = content };
const string script = @" const string script = @"
invalid(); invalid();
@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced() public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptVars { Data = content };
const string script = @" const string script = @"
var x = 0; var x = 0;
@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced_async() public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced_async()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptVars { Data = content };
const string script = @" const string script = @"
async = true; async = true;
@ -197,7 +197,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddValue("iv", "MyOperation")); .AddValue("iv", "MyOperation"));
var context = new ScriptContext { Data = content, Operation = "MyOperation" }; var context = new ScriptVars { Data = content, Operation = "MyOperation" };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -223,7 +223,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 42)); .AddValue("iv", 42));
var context = new ScriptContext { Data = content, Operation = "MyOperation" }; var context = new ScriptVars { Data = content, Operation = "MyOperation" };
const string script = @" const string script = @"
async = true; async = true;
@ -247,7 +247,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task ExecuteAndTransformAsync_should_ignore_transformation_when_async_not_set() public async Task ExecuteAndTransformAsync_should_ignore_transformation_when_async_not_set()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content, Operation = "MyOperation" }; var context = new ScriptVars { Data = content, Operation = "MyOperation" };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -269,7 +269,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task ExecuteAndTransformAsync_should_timeout_when_replace_never_called() public async Task ExecuteAndTransformAsync_should_timeout_when_replace_never_called()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content, Operation = "MyOperation" }; var context = new ScriptVars { Data = content, Operation = "MyOperation" };
const string script = @" const string script = @"
async = true; async = true;
@ -305,7 +305,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 10.0)); .AddValue("iv", 10.0));
var context = new ScriptContext { Data = content }; var context = new ScriptVars { Data = content };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2")); userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2"));
var context = new ScriptContext { Data = content, DataOld = oldContent, User = userPrincipal }; var context = new ScriptVars { Data = content, DataOld = oldContent, User = userPrincipal };
const string script = @" const string script = @"
ctx.data.number0.iv = ctx.data.number0.iv + ctx.oldData.number0.iv * parseInt(ctx.user.id, 10); ctx.data.number0.iv = ctx.data.number0.iv + ctx.oldData.number0.iv * parseInt(ctx.user.id, 10);
@ -369,7 +369,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
value.i == 2 value.i == 2
"; ";
var context = new ScriptContext var context = new ScriptVars
{ {
["value"] = new { i = 2 } ["value"] = new { i = 2 }
}; };
@ -386,7 +386,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
value.status == 'Published' value.status == 'Published'
"; ";
var context = new ScriptContext var context = new ScriptVars
{ {
["value"] = new { status = Status.Published } ["value"] = new { status = Status.Published }
}; };
@ -403,7 +403,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
value.i == 3 value.i == 3
"; ";
var context = new ScriptContext var context = new ScriptVars
{ {
["value"] = new { i = 2 } ["value"] = new { i = 2 }
}; };
@ -420,7 +420,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
function(); function();
"; ";
var context = new ScriptContext var context = new ScriptVars
{ {
["value"] = new { i = 2 } ["value"] = new { i = 2 }
}; };

129
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs

@ -0,0 +1,129 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.Templates.Extensions;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.Templates
{
public class FluidTemplateEngineTests
{
private readonly FluidTemplateEngine sut;
public FluidTemplateEngineTests()
{
var extensions = new IFluidExtension[]
{
new DateTimeFluidExtension()
};
sut = new FluidTemplateEngine(extensions);
}
[Theory]
[InlineData("{{ e.user }}", "subject:me")]
[InlineData("{{ e.user.type }}", "subject")]
[InlineData("{{ e.user.identifier }}", "me")]
public async Task Should_render_ref_token(string template, string expected)
{
var value = new
{
User = new RefToken(RefTokenType.Subject, "me")
};
var result = await RenderAync(template, value);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("{{ e.id }}", "42,my-app")]
[InlineData("{{ e.id.name}}", "my-app")]
[InlineData("{{ e.id.id }}", "42")]
public async Task Should_render_named_id(string template, string expected)
{
var value = new
{
Id = NamedId.Of("42", "my-app")
};
var result = await RenderAync(template, value);
Assert.Equal(expected, result);
}
[Fact]
public async Task Should_format_enum()
{
var value = new
{
Type = EnrichedContentEventType.Created
};
var template = "{{ e.type }}";
var result = await RenderAync(template, value);
Assert.Equal(value.Type.ToString(), result);
}
[Fact]
public async Task Should_format_date()
{
var now = DateTime.UtcNow;
var value = new
{
Timestamp = now
};
var template = "{{ e.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss' }}";
var result = await RenderAync(template, value);
Assert.Equal($"{now:yyyy-MM-dd-hh-mm-ss}", result);
}
[Fact]
public async Task Should_format_content_data()
{
var template = "{{ e.data.value.en }}";
var value = new
{
Data =
new NamedContentData()
.AddField("value",
new ContentFieldData()
.AddValue("en", "Hello"))
};
var result = await RenderAync(template, value);
Assert.Equal("Hello", result);
}
[Fact]
public async Task Should_throw_exception_when_template_invalid()
{
var template = "{% for x of event %}";
await Assert.ThrowsAsync<TemplateParseException>(() => sut.RenderAsync(template, new TemplateVars()));
}
private Task<string> RenderAync(string template, object value)
{
return sut.RenderAsync(template, new TemplateVars { ["e"] = value });
}
}
}

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs

@ -32,10 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
public AssetChangedTriggerHandlerTests() public AssetChangedTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false"))
.Returns(false); .Returns(false);
sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader);
@ -149,12 +149,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs

@ -33,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Comments
public CommentTriggerHandlerTests() public CommentTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false"))
.Returns(false); .Returns(false);
sut = new CommentTriggerHandler(scriptEngine, userResolver); sut = new CommentTriggerHandler(scriptEngine, userResolver);
@ -290,12 +290,12 @@ namespace Squidex.Domain.Apps.Entities.Comments
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

112
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs

@ -22,7 +22,6 @@ using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
#pragma warning disable SA1401 // Fields must be private #pragma warning disable SA1401 // Fields must be private
@ -35,23 +34,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly ILocalCache localCache = new AsyncLocalCache(); private readonly ILocalCache localCache = new AsyncLocalCache();
private readonly IContentLoader contentLoader = A.Fake<IContentLoader>(); private readonly IContentLoader contentLoader = A.Fake<IContentLoader>();
private readonly ContentChangedTriggerHandler sut; private readonly IRuleTriggerHandler sut;
private readonly IRuleTriggerHandler handler;
private readonly Guid ruleId = Guid.NewGuid(); private readonly Guid ruleId = Guid.NewGuid();
private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1");
private static readonly NamedId<Guid> SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); private static readonly NamedId<Guid> SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2");
public ContentChangedTriggerHandlerTests() public ContentChangedTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false"))
.Returns(false); .Returns(false);
sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader, localCache); sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader);
handler = sut;
} }
public static IEnumerable<object[]> TestEvents() public static IEnumerable<object[]> TestEvents()
@ -64,58 +60,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
yield return new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished }; yield return new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished };
} }
[Fact]
public async Task Should_resolve_reference_if_value_from_content_loader()
{
var referenceId = Guid.NewGuid();
var referenceValue = JsonValue.Array(referenceId);
SetupReference(referenceId);
var (handled, result) = sut.Format(null!, referenceValue, new[] { "data", "field1", "iv" });
Assert.True(handled);
Assert.Equal("Hello", await result);
}
[Fact]
public async Task Should_resolve_reference_only_once()
{
using (localCache.StartContext())
{
var referenceId = Guid.NewGuid();
var referenceValue = JsonValue.Array(referenceId);
SetupReference(referenceId);
var (handled1, result1) = sut.Format(null!, referenceValue, new[] { "data", "field1", "iv" });
var (handled2, result2) = sut.Format(null!, referenceValue, new[] { "data", "field2", "iv" });
Assert.True(handled1);
Assert.Equal("Hello", await result1);
Assert.True(handled2);
Assert.Equal("World", await result2);
A.CallTo(() => contentLoader.GetAsync(A<Guid>._, A<long>._))
.MustHaveHappenedOnceExactly();
}
}
[Fact]
public async Task Should_not_return_value_if_path_not_found_in_reference()
{
var referenceId = Guid.NewGuid();
var referenceValue = JsonValue.Array(referenceId);
SetupReference(referenceId);
var (handled, result) = sut.Format(null!, referenceValue, new[] { "data", "invalid", "iv" });
Assert.True(handled);
Assert.Null(await result);
}
[Theory] [Theory]
[MemberData(nameof(TestEvents))] [MemberData(nameof(TestEvents))]
public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type) public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type)
@ -125,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12))
.Returns(new ContentEntity { SchemaId = SchemaMatch }); .Returns(new ContentEntity { SchemaId = SchemaMatch });
var result = await handler.CreateEnrichedEventsAsync(envelope); var result = await sut.CreateEnrichedEventsAsync(envelope);
var enrichedEvent = result.Single() as EnrichedContentEvent; var enrichedEvent = result.Single() as EnrichedContentEvent;
@ -148,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 11)) A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 11))
.Returns(new ContentEntity { SchemaId = SchemaMatch, Version = 11, Data = dataOld }); .Returns(new ContentEntity { SchemaId = SchemaMatch, Version = 11, Data = dataOld });
var result = await handler.CreateEnrichedEventsAsync(envelope); var result = await sut.CreateEnrichedEventsAsync(envelope);
var enrichedEvent = result.Single() as EnrichedContentEvent; var enrichedEvent = result.Single() as EnrichedContentEvent;
@ -161,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new AssetCreated(), trigger, ruleId); var result = sut.Trigger(new AssetCreated(), trigger, ruleId);
Assert.False(result); Assert.False(result);
}); });
@ -172,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId);
Assert.False(result); Assert.False(result);
}); });
@ -183,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId);
Assert.True(result); Assert.True(result);
}); });
@ -194,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger =>
{ {
var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId);
Assert.True(result); Assert.True(result);
}); });
@ -205,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId);
Assert.False(result); Assert.False(result);
}); });
@ -216,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new EnrichedAssetEvent(), trigger); var result = sut.Trigger(new EnrichedAssetEvent(), trigger);
Assert.False(result); Assert.False(result);
}); });
@ -227,7 +171,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger);
Assert.False(result); Assert.False(result);
}); });
@ -238,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger);
Assert.True(result); Assert.True(result);
}); });
@ -249,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger =>
{ {
var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger);
Assert.True(result); Assert.True(result);
}); });
@ -260,7 +204,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger =>
{ {
var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger);
Assert.True(result); Assert.True(result);
}); });
@ -271,7 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger =>
{ {
var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger);
Assert.False(result); Assert.False(result);
}); });
@ -282,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger =>
{ {
var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger);
Assert.False(result); Assert.False(result);
}); });
@ -307,30 +251,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, A<string>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }
private void SetupReference(Guid referenceId)
{
A.CallTo(() => contentLoader.GetAsync(referenceId, EtagVersion.Any))
.Returns(new ContentEntity
{
Data =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddJsonValue(JsonValue.Create("Hello")))
.AddField("field2",
new ContentFieldData()
.AddJsonValue(JsonValue.Create("World")))
});
}
} }
} }

26
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -100,8 +100,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId))
.Returns((app, schema)); .Returns((app, schema));
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptVars>._, A<string>._))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<ScriptContext>(0)!.Data!)); .ReturnsLazily(x => Task.FromResult(x.GetArgument<ScriptVars>(0)!.Data!));
patched = patch.MergeInto(data); patched = patch.MergeInto(data);
@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>"))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -233,7 +233,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(LastEvents); Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptVars>._, "<update-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -306,7 +306,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(LastEvents); Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptVars>._, "<update-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -424,7 +424,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -452,7 +452,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -480,7 +480,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentSchedulingCancelled()) CreateContentEvent(new ContentSchedulingCancelled())
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -577,17 +577,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published }));
} }
private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus) private ScriptVars ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus)
{ {
return A<ScriptContext>.That.Matches(x => M(x, newData, oldData, newStatus, default)); return A<ScriptVars>.That.Matches(x => M(x, newData, oldData, newStatus, default));
} }
private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus) private ScriptVars ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus)
{ {
return A<ScriptContext>.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus)); return A<ScriptVars>.That.Matches(x => M(x, newData, oldData, newStatus, oldStatus));
} }
private bool M(ScriptContext x, NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus) private bool M(ScriptVars x, NamedContentData? newData, NamedContentData? oldData, Status newStatus, Status oldStatus)
{ {
return return
Equals(x.Data, newData) && Equals(x.Data, newData) &&

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterScriptExtensionTests.cs → backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs

@ -15,17 +15,17 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Counter namespace Squidex.Domain.Apps.Entities.Contents.Counter
{ {
public class CounterScriptExtensionTests public class CounterJintExtensionTests
{ {
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly ICounterGrain counter = A.Fake<ICounterGrain>(); private readonly ICounterGrain counter = A.Fake<ICounterGrain>();
private readonly JintScriptEngine sut; private readonly JintScriptEngine sut;
public CounterScriptExtensionTests() public CounterJintExtensionTests()
{ {
var extensions = new IScriptExtension[] var extensions = new IJintExtension[]
{ {
new CounterScriptExtension(grainFactory) new CounterJintExtension(grainFactory)
}; };
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
return resetCounter('my', 4); return resetCounter('my', 4);
"; ";
var context = new ScriptContext var context = new ScriptVars
{ {
["appId"] = appId ["appId"] = appId
}; };
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
return incrementCounter('my'); return incrementCounter('my');
"; ";
var context = new ScriptContext var context = new ScriptVars
{ {
["appId"] = appId ["appId"] = appId
}; };

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs

@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptVars>._, A<string>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptVars>._, A<string>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var content = new ContentEntity { SchemaId = schemaWithScriptId, Data = oldData }; var content = new ContentEntity { SchemaId = schemaWithScriptId, Data = oldData };
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptContext>._, "my-query")) A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "my-query"))
.Returns(new NamedContentData()); .Returns(new NamedContentData());
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.NotSame(oldData, content.Data); Assert.NotSame(oldData, content.Data);
A.CallTo(() => scriptEngine.TransformAsync( A.CallTo(() => scriptEngine.TransformAsync(
A<ScriptContext>.That.Matches(x => A<ScriptVars>.That.Matches(x =>
ReferenceEquals(x.User, ctx.User) && ReferenceEquals(x.User, ctx.User) &&
ReferenceEquals(x.Data, oldData) && ReferenceEquals(x.Data, oldData) &&
x.ContentId == content.Id), x.ContentId == content.Id),

104
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs

@ -0,0 +1,104 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ReferenceFluidExtensionTests
{
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly FluidTemplateEngine sut;
public ReferenceFluidExtensionTests()
{
var extensions = new IFluidExtension[]
{
new ReferencesFluidExtension(contentQuery, appProvider)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id))
.Returns(Mocks.App(appId));
sut = new FluidTemplateEngine(extensions);
}
[Fact]
public async Task Should_resolve_references_in_loop()
{
var referenceId1 = Guid.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var referenceId2 = Guid.NewGuid();
var reference2 = CreateReference(referenceId1, 2);
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(referenceId1, referenceId2))),
AppId = appId
};
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<Guid>>.That.Contains(referenceId1)))
.Returns(ResultList.CreateFrom(1, reference1));
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<Guid>>.That.Contains(referenceId2)))
.Returns(ResultList.CreateFrom(1, reference2));
var vars = new TemplateVars
{
["event"] = @event
};
var template = @"
{% for id in event.data.references.iv %}
{% reference 'ref', id %}
Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }}
{% endfor %}
";
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(expected, result);
}
private IEnrichedContentEntity CreateReference(Guid referenceId, int index)
{
return new ContentEntity
{
Data =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddJsonValue(JsonValue.Create($"Hello {index}")))
.AddField("field2",
new ContentFieldData()
.AddJsonValue(JsonValue.Create($"World {index}"))),
Id = referenceId
};
}
}
}

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs

@ -31,10 +31,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public SchemaChangedTriggerHandlerTests() public SchemaChangedTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false"))
.Returns(false); .Returns(false);
sut = new SchemaChangedTriggerHandler(scriptEngine); sut = new SchemaChangedTriggerHandler(scriptEngine);
@ -136,12 +136,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

8
frontend/app/features/rules/pages/events/rule-events-page.component.scss

@ -2,6 +2,10 @@ h3 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.expanded {
border-bottom: 0;
}
.event { .event {
&-stats { &-stats {
font-size: .8rem; font-size: .8rem;
@ -17,7 +21,7 @@ h3 {
&-header { &-header {
& { & {
background: $color-border; background: $color-table-footer;
border: 0; border: 0;
margin: -.75rem -1.25rem; margin: -.75rem -1.25rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@ -26,7 +30,7 @@ h3 {
} }
&::before { &::before {
@include caret-top($color-border); @include caret-top($color-table-footer);
@include absolute(-1.1rem, 1.8rem, auto, auto); @include absolute(-1.1rem, 1.8rem, auto, auto);
} }

Loading…
Cancel
Save