Browse Source

Script and template extensions for references and assets.

pull/637/head
Sebastian 5 years ago
parent
commit
7173e95d5d
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  2. 45
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs
  3. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  4. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs
  5. 28
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  6. 37
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  7. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  8. 92
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs
  9. 114
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  10. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  11. 43
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs
  12. 116
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  13. 9
      backend/src/Squidex/Config/Domain/RuleServices.cs
  14. 63
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  15. 99
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs
  16. 132
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  17. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  18. 36
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs
  19. 143
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs
  20. 1
      backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
this.urlGenerator = urlGenerator;
}
public void Extend(ExecutionContext context, bool async)
public void Extend(ExecutionContext context)
{
context.Engine.SetValue("contentAction", new EventDelegate(() =>
{

45
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs

@ -15,26 +15,19 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Core.Scripting
{
public delegate bool ExceptionHandler(Exception exception);
public sealed class ExecutionContext : Dictionary<string, object>
public sealed class ExecutionContext : ScriptContext
{
private readonly ExceptionHandler? exceptionHandler;
private Func<Exception, bool>? completion;
public Engine Engine { get; }
public CancellationToken CancellationToken { get; }
public CancellationToken CancellationToken { get; private set; }
public bool IsAsync { get; private set; }
internal ExecutionContext(Engine engine, CancellationToken cancellationToken, ExceptionHandler? exceptionHandler = null)
: base(StringComparer.OrdinalIgnoreCase)
internal ExecutionContext(Engine engine)
{
Engine = engine;
CancellationToken = cancellationToken;
this.exceptionHandler = exceptionHandler;
}
public void MarkAsync()
@ -44,10 +37,34 @@ namespace Squidex.Domain.Apps.Core.Scripting
public void Fail(Exception exception)
{
exceptionHandler?.Invoke(exception);
completion?.Invoke(exception);
}
public ExecutionContext ExtendAsync(IEnumerable<IJintExtension> extensions, Func<Exception, bool> completion, CancellationToken ct)
{
CancellationToken = ct;
this.completion = completion;
foreach (var extension in extensions)
{
extension.ExtendAsync(this);
}
return this;
}
public ExecutionContext Extend(IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
{
extension.Extend(this);
}
return this;
}
public void AddVariables(ScriptVars vars, ScriptOptions options)
public ExecutionContext Extend(ScriptVars vars, ScriptOptions options)
{
var engine = Engine;
@ -86,6 +103,8 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
engine.SetValue("async", true);
return this;
}
}
}

21
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -29,14 +29,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
this.httpClientFactory = httpClientFactory;
}
public void Extend(ExecutionContext context, bool async)
public void ExtendAsync(ExecutionContext context)
{
if (async)
{
var engine = context.Engine;
var action = new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers));
engine.SetValue("getJSON", new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers)));
}
context.Engine.SetValue("getJSON", action);
}
private void GetJson(ExecutionContext context, string url, Action<JsValue> callback, JsValue? headers)
@ -46,6 +43,18 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
private async Task GetJsonAsync(ExecutionContext context, string url, Action<JsValue> callback, JsValue? headers)
{
if (callback == null)
{
context.Fail(new JavaScriptException("Callback cannot be null."));
return;
}
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
context.Fail(new JavaScriptException("URL is not valid."));
return;
}
context.MarkAsync();
try

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs

@ -15,7 +15,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
}
void Extend(ExecutionContext context, bool async)
void Extend(ExecutionContext context)
{
}
void ExtendAsync(ExecutionContext context)
{
}
}

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

@ -52,7 +52,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token);
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions, tcs.TrySetException, cts.Token);
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
@ -82,7 +86,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token);
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions, tcs.TrySetException, cts.Token);
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
@ -126,14 +134,17 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script));
var context = CreateEngine(vars, options);
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions);
Execute(context.Engine, script);
return JsonMapper.Map(context.Engine.GetCompletionValue());
}
private ExecutionContext CreateEngine(ScriptVars vars, ScriptOptions options, ExceptionHandler? exceptionHandler = null, bool async = false, CancellationToken ct = default)
private ExecutionContext CreateEngine(ScriptOptions options)
{
var engine = new Engine(engineOptions =>
{
@ -158,14 +169,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
extension.Extend(engine);
}
var context = new ExecutionContext(engine, ct, exceptionHandler);
context.AddVariables(vars, options);
foreach (var extension in extensions)
{
extension.Extend(context, async);
}
var context = new ExecutionContext(engine);
return context;
}

37
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -0,0 +1,37 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public class ScriptContext : Dictionary<string, object?>
{
public ScriptContext()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
{
Guard.NotNull(key, nameof(key));
value = default!;
if (TryGetValue(key, out var temp) && temp is T typed)
{
value = typed;
return true;
}
return false;
}
}
}

26
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs

@ -6,21 +6,17 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptVars : Dictionary<string, object?>
public sealed class ScriptVars : ScriptContext
{
public ScriptVars()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public ClaimsPrincipal? User
{
get => GetValue<ClaimsPrincipal?>();
@ -68,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
get => GetValue<ContentData?>();
set
{
SetValue(value, "oldData");
SetValue(value, nameof(OldData));
SetValue(value);
}
}
@ -78,11 +74,23 @@ namespace Squidex.Domain.Apps.Core.Scripting
get => GetValue<Status>();
set
{
SetValue(value, "oldStatus");
SetValue(value, nameof(OldStatus));
SetValue(value);
}
}
[Obsolete("Use dataOld")]
public ContentData? OldData
{
get => GetValue<ContentData?>();
}
[Obsolete("Use statusOld")]
public Status? OldStatus
{
get => GetValue<Status?>();
}
public void SetValue(object? value, [CallerMemberName] string? key = null)
{
if (key != null)

92
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs

@ -0,0 +1,92 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.IO;
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.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetsFluidExtension : IFluidExtension
{
private readonly IAppProvider appProvider;
private readonly IAssetQueryService assetQuery;
private sealed class AssetTag : ArgumentsTag
{
private readonly AssetsFluidExtension root;
public AssetTag(AssetsFluidExtension root)
{
this.root = root;
}
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 root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false);
if (app == null)
{
return Completion.Normal;
}
var requestContext =
Context.Admin(app).Clone(b => b
.WithoutTotal());
var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue();
var asset = await root.assetQuery.FindAsync(requestContext, DomainId.Create(id));
if (asset != null)
{
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue();
context.SetValue(name, asset);
}
}
return Completion.Normal;
}
}
public AssetsFluidExtension(IAppProvider appProvider, IAssetQueryService assetQuery)
{
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(appProvider, nameof(appProvider));
this.assetQuery = assetQuery;
this.appProvider = appProvider;
}
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
memberAccessStrategy.Register<IAssetEntity>();
memberAccessStrategy.Register<IAssetInfo>();
memberAccessStrategy.Register<IEntity>();
memberAccessStrategy.Register<IEntityWithCreatedBy>();
memberAccessStrategy.Register<IEntityWithLastModifiedBy>();
memberAccessStrategy.Register<IEntityWithVersion>();
memberAccessStrategy.Register<IEnrichedAssetEntity>();
}
public void RegisterLanguageExtensions(FluidParserFactory factory)
{
factory.RegisterTag("asset", new AssetTag(this));
}
}
}

114
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs

@ -0,0 +1,114 @@
// ==========================================================================
// 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.Security.Claims;
using System.Threading.Tasks;
using Jint.Native;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetsJintExtension : IJintExtension
{
private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback);
private readonly IAppProvider appProvider;
private readonly IAssetQueryService assetQuery;
public AssetsJintExtension(IAppProvider appProvider, IAssetQueryService assetQuery)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetQuery, nameof(assetQuery));
this.appProvider = appProvider;
this.assetQuery = assetQuery;
}
public void ExtendAsync(ExecutionContext context)
{
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user))
{
return;
}
var action = new GetAssetsDelegate((references, callback) => GetReferences(context, appId, user, references, callback));
context.Engine.SetValue("getAsset", action);
context.Engine.SetValue("getAssets", action);
}
private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
GetReferencesAsync(context, appId, user, references, callback).Forget();
}
private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback, nameof(callback));
var ids = new List<DomainId>();
if (references.IsString())
{
ids.Add(DomainId.Create(references.ToString()));
}
else if (references.IsArray())
{
foreach (var value in references.AsArray())
{
if (value.IsString())
{
ids.Add(DomainId.Create(value.ToString()));
}
}
}
if (ids.Count == 0)
{
var emptyAssets = Array.Empty<IEnrichedAssetEntity>();
callback(JsValue.FromObject(context.Engine, emptyAssets));
return;
}
context.MarkAsync();
try
{
var app = await appProvider.GetAppAsync(appId);
if (app == null)
{
throw new JavaScriptException("App does not exist.");
}
var requestContext =
new Context(user, app).Clone(b => b
.WithoutTotal());
var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids));
callback(JsValue.FromObject(context.Engine, assets.ToArray()));
}
catch (Exception ex)
{
context.Fail(ex);
}
}
}
}

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

@ -24,9 +24,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
this.grainFactory = grainFactory;
}
public void Extend(ExecutionContext context, bool async)
public void ExtendAsync(ExecutionContext context)
{
if (context.TryGetValue("appId", out var temp) && temp is DomainId appId)
if (context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
{
var engine = context.Engine;

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

@ -23,50 +23,48 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ReferencesFluidExtension : IFluidExtension
{
private readonly IContentQueryService contentQueryService;
private readonly IAppProvider appProvider;
private readonly IContentQueryService contentQuery;
private sealed class ReferenceTag : ArgumentsTag
{
private readonly IContentQueryService contentQueryService;
private readonly IAppProvider appProvider;
private readonly ReferencesFluidExtension root;
public ReferenceTag(IContentQueryService contentQueryService, IAppProvider appProvider)
public ReferenceTag(ReferencesFluidExtension root)
{
this.contentQueryService = contentQueryService;
this.appProvider = appProvider;
this.root = root;
}
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, false);
var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false);
if (app == null)
{
return Completion.Normal;
}
var appContext = Context.Admin(app).Clone(b => b
.WithoutContentEnrichment()
.WithoutCleanup()
.WithUnpublished());
var requestContext =
Context.Admin(app).Clone(b => b
.WithoutContentEnrichment()
.WithUnpublished()
.WithoutTotal());
var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue();
var domainId = DomainId.Create(id);
var domainIds = new List<DomainId> { domainId };
var references = await contentQueryService.QueryAsync(appContext, Q.Empty.WithIds(domainIds));
var reference = references.FirstOrDefault();
var contents = await root.contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds));
var content = contents.FirstOrDefault();
if (reference != null)
if (content != null)
{
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue();
context.SetValue(name, reference);
context.SetValue(name, content);
}
}
@ -74,12 +72,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public ReferencesFluidExtension(IContentQueryService contentQueryService, IAppProvider appProvider)
public ReferencesFluidExtension(IAppProvider appProvider, IContentQueryService contentQuery)
{
Guard.NotNull(contentQueryService, nameof(contentQueryService));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(appProvider, nameof(appProvider));
this.contentQueryService = contentQueryService;
this.contentQuery = contentQuery;
this.appProvider = appProvider;
}
@ -87,11 +85,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
memberAccessStrategy.Register<IContentEntity>();
memberAccessStrategy.Register<IEntity>();
memberAccessStrategy.Register<IEntityWithCreatedBy>();
memberAccessStrategy.Register<IEntityWithLastModifiedBy>();
memberAccessStrategy.Register<IEntityWithVersion>();
memberAccessStrategy.Register<IEnrichedContentEntity>();
}
public void RegisterLanguageExtensions(FluidParserFactory factory)
{
factory.RegisterTag("reference", new ReferenceTag(contentQueryService, appProvider));
factory.RegisterTag("reference", new ReferenceTag(this));
}
}
}

116
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs

@ -0,0 +1,116 @@
// ==========================================================================
// 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.Security.Claims;
using System.Threading.Tasks;
using Jint.Native;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ReferencesJintExtension : IJintExtension
{
private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback);
private readonly IAppProvider appProvider;
private readonly IContentQueryService contentQuery;
public ReferencesJintExtension(IAppProvider appProvider, IContentQueryService contentQuery)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(contentQuery, nameof(contentQuery));
this.appProvider = appProvider;
this.contentQuery = contentQuery;
}
public void ExtendAsync(ExecutionContext context)
{
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user))
{
return;
}
var action = new GetReferencesDelegate((references, callback) => GetReferences(context, appId, user, references, callback));
context.Engine.SetValue("getReference", action);
context.Engine.SetValue("getReferences", action);
}
private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
GetReferencesAsync(context, appId, user, references, callback).Forget();
}
private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback, nameof(callback));
var ids = new List<DomainId>();
if (references.IsString())
{
ids.Add(DomainId.Create(references.ToString()));
}
else if (references.IsArray())
{
foreach (var value in references.AsArray())
{
if (value.IsString())
{
ids.Add(DomainId.Create(value.ToString()));
}
}
}
if (ids.Count == 0)
{
var emptyContents = Array.Empty<IEnrichedContentEntity>();
callback(JsValue.FromObject(context.Engine, emptyContents));
return;
}
context.MarkAsync();
try
{
var app = await appProvider.GetAppAsync(appId);
if (app == null)
{
throw new JavaScriptException("App does not exist.");
}
var requestContext =
new Context(user, app).Clone(b => b
.WithoutContentEnrichment()
.WithUnpublished()
.WithoutTotal());
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids));
callback(JsValue.FromObject(context.Engine, contents.ToArray()));
}
catch (Exception ex)
{
context.Fail(ex);
}
}
}
}

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

@ -47,9 +47,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>();
services.AddSingletonAs<AssetsFluidExtension>()
.As<IFluidExtension>();
services.AddSingletonAs<AssetsJintExtension>()
.As<IJintExtension>();
services.AddSingletonAs<ReferencesFluidExtension>()
.As<IFluidExtension>();
services.AddSingletonAs<ReferencesJintExtension>()
.As<IJintExtension>();
services.AddSingletonAs<ManualTriggerHandler>()
.As<IRuleTriggerHandler>();

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

@ -11,6 +11,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FakeItEasy;
using Jint.Runtime;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Scripting;
@ -195,7 +196,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanReject = true
};
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options));
Assert.NotEmpty(ex.Errors);
}
@ -212,7 +215,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanReject = true
};
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options));
Assert.Equal("Not valid", ex.Errors.Single().Message);
}
@ -229,7 +234,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanDisallow = true
};
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options));
Assert.Equal("Script has forbidden the operation.", ex.Message);
}
@ -246,25 +253,57 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanDisallow = true
};
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options));
Assert.Equal("Operation not allowed", ex.Message);
}
[Fact]
public async Task Should_throw_exception_when_getJson_url_is_null()
{
const string script = @"
getJSON(null, function(result) {
complete(result);
});
";
var vars = new ScriptVars();
await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script));
}
[Fact]
public async Task Should_throw_exception_when_getJson_callback_is_null()
{
const string script = @"
var url = 'http://squidex.io';
getJSON(url, null);
";
var vars = new ScriptVars();
await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script));
}
[Fact]
public async Task Should_make_json_request()
{
var httpHandler = SetupRequest();
const string script = @"
async = true;
var url = 'http://squidex.io';
getJSON('http://squidex.io', function(result) {
getJSON(url, function(result) {
complete(result);
});
";
var result = await sut.ExecuteAsync(new ScriptVars(), script);
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");
@ -280,19 +319,21 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
var httpHandler = SetupRequest();
const string script = @"
async = true;
var headers = {
'X-Header1': 1,
'X-Header2': '2'
};
getJSON('http://squidex.io', function(result) {
var url = 'http://squidex.io';
getJSON(url, function(result) {
complete(result);
}, headers);
";
var result = await sut.ExecuteAsync(new ScriptVars(), script);
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");

99
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs

@ -0,0 +1,99 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
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.Assets
{
public class AssetsFluidExtensionTests
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly FluidTemplateEngine sut;
public AssetsFluidExtensionTests()
{
var extensions = new IFluidExtension[]
{
new AssetsFluidExtension(appProvider, assetQuery)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
.Returns(Mocks.App(appId));
sut = new FluidTemplateEngine(extensions);
}
[Fact]
public async Task Should_resolve_assets_in_loop()
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1);
var assetId2 = DomainId.NewGuid();
var asset2 = CreateAsset(assetId2, 2);
var @event = new EnrichedContentEvent
{
Data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(assetId1, assetId2))),
AppId = appId
};
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId1, EtagVersion.Any))
.Returns(asset1);
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId2, EtagVersion.Any))
.Returns(asset2);
var vars = new TemplateVars
{
["event"] = @event
};
var template = @"
{% for id in event.data.assets.iv %}
{% asset 'ref', id %}
Text: {{ ref.fileName }} {{ ref.id }}
{% endfor %}
";
var expected = $@"
Text: file1.jpg {assetId1}
Text: file2.jpg {assetId2}
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index)
{
return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId };
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

132
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs

@ -0,0 +1,132 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetsJintExtensionTests : IClassFixture<TranslationsFixture>
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly JintScriptEngine sut;
public AssetsJintExtensionTests()
{
var extensions = new IJintExtension[]
{
new AssetsJintExtension(appProvider, assetQuery)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
.Returns(Mocks.App(appId));
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions);
}
[Fact]
public async Task Should_resolve_asset()
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(assetId1)));
A.CallTo(() => assetQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1)))
.Returns(ResultList.CreateFrom(1, asset1));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getAsset(data.assets.iv[0], function (assets) {
var result1 = `Text: ${assets[0].fileName}`;
complete(`${result1}`);
})";
var expected = @"
Text: file1.jpg
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_resolve_assets()
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1);
var assetId2 = DomainId.NewGuid();
var asset2 = CreateAsset(assetId1, 2);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(assetId1, assetId2)));
A.CallTo(() => assetQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1, assetId2)))
.Returns(ResultList.CreateFrom(2, asset1, asset2));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getAssets(data.assets.iv, function (assets) {
var result1 = `Text: ${assets[0].fileName}`;
var result2 = `Text: ${assets[1].fileName}`;
complete(`${result1}\n${result2}`);
})";
var expected = @"
Text: file1.jpg
Text: file2.jpg
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index)
{
return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId };
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

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

@ -11,7 +11,6 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;

36
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs → backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs

@ -17,18 +17,18 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ReferenceFluidExtensionTests
public class ReferencesFluidExtensionTests
{
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly FluidTemplateEngine sut;
public ReferenceFluidExtensionTests()
public ReferencesFluidExtensionTests()
{
var extensions = new IFluidExtension[]
{
new ReferencesFluidExtension(contentQuery, appProvider)
new ReferencesFluidExtension(appProvider, contentQuery)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var referenceId1 = DomainId.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var referenceId2 = DomainId.NewGuid();
var reference2 = CreateReference(referenceId1, 2);
var reference2 = CreateReference(referenceId2, 2);
var @event = new EnrichedContentEvent
{
@ -67,20 +67,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
};
var template = @"
{% for id in event.data.references.iv %}
{% reference 'ref', id %}
Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }}
{% endfor %}
";
{% for id in event.data.references.iv %}
{% reference 'ref', id %}
Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }}
{% endfor %}
";
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var expected = $@"
Text: Hello 1 World 1 {referenceId1}
Text: Hello 2 World 2 {referenceId2}
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(expected, result);
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index)
@ -98,5 +98,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
Id = referenceId
};
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

143
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs

@ -0,0 +1,143 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ReferencesJintExtensionTests : IClassFixture<TranslationsFixture>
{
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly JintScriptEngine sut;
public ReferencesJintExtensionTests()
{
var extensions = new IJintExtension[]
{
new ReferencesJintExtension(appProvider, contentQuery)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
.Returns(Mocks.App(appId));
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions);
}
[Fact]
public async Task Should_resolve_reference()
{
var referenceId1 = DomainId.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(referenceId1)));
A.CallTo(() => contentQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), A<Q>.That.HasIds(referenceId1)))
.Returns(ResultList.CreateFrom(1, reference1));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getReference(data.references.iv[0], function (references) {
var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
complete(`${result1}`);
})";
var expected = @"
Text: Hello 1 World 1
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_resolve_references()
{
var referenceId1 = DomainId.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var referenceId2 = DomainId.NewGuid();
var reference2 = CreateReference(referenceId1, 2);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(referenceId1, referenceId2)));
A.CallTo(() => contentQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), A<Q>.That.HasIds(referenceId1, referenceId2)))
.Returns(ResultList.CreateFrom(2, reference1, reference2));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getReferences(data.references.iv, function (references) {
var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
var result2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`;
complete(`${result1}\n${result2}`);
})";
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index)
{
return new ContentEntity
{
Data =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddJsonValue(JsonValue.Create($"Hello {index}")))
.AddField("field2",
new ContentFieldData()
.AddJsonValue(JsonValue.Create($"World {index}"))),
Id = referenceId
};
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

1
backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

@ -9,7 +9,6 @@ using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;

Loading…
Cancel
Save