Browse Source

Blur hash. (#845)

* Blur hash.

* Cleanup

* More fixes.
pull/847/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
cb9a14a248
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  2. 71
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  3. 38
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs
  4. 106
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  5. 110
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  6. 22
      backend/src/Squidex.Domain.Apps.Entities/AppTag.cs
  7. 28
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs
  8. 100
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs
  9. 221
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  10. 92
      backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs
  11. 87
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  12. 32
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs
  13. 66
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  14. 33
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs
  15. 11
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx
  16. 2
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  17. 1
      backend/src/Squidex.Web/ETagExtensions.cs
  18. 18
      backend/src/Squidex/Squidex.csproj
  19. 18
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  20. 184
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs
  21. 258
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  22. 54
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs
  23. 78
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs

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

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
public void Describe(AddDescription describe, ScriptScope scope)
{
if ((scope & ScriptScope.ContentTrigger) == ScriptScope.ContentTrigger)
if (scope.HasFlag(ScriptScope.ContentTrigger))
{
describe(JsonType.Function, "contentAction",
Resources.ScriptingContentAction);
@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
Resources.ScriptingContentUrl);
}
if ((scope & ScriptScope.AssetTrigger) == ScriptScope.AssetTrigger)
if (scope.HasFlag(ScriptScope.AssetTrigger))
{
describe(JsonType.Function, "assetContentUrl",
Resources.ScriptingAssetContentUrl);

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

@ -11,7 +11,6 @@ using Jint.Native;
using Jint.Native.Json;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{
@ -28,28 +27,28 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
public void ExtendAsync(ScriptExecutionContext context)
{
AdBodyMethod(context, HttpMethod.Patch, "patchJSON");
AdBodyMethod(context, HttpMethod.Post, "postJSON");
AdBodyMethod(context, HttpMethod.Put, "putJSON");
AddBodyMethod(context, HttpMethod.Patch, "patchJSON");
AddBodyMethod(context, HttpMethod.Post, "postJSON");
AddBodyMethod(context, HttpMethod.Put, "putJSON");
AddMethod(context, HttpMethod.Delete, "deleteJSON");
AddMethod(context, HttpMethod.Get, "getJSON");
}
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "getJSON(url, callback, ?headers)",
describe(JsonType.Function, "getJSON(url, callback, headers?)",
Resources.ScriptingGetJSON);
describe(JsonType.Function, "postJSON(url, body, callback, ?headers)",
describe(JsonType.Function, "postJSON(url, body, callback, headers?)",
Resources.ScriptingPostJSON);
describe(JsonType.Function, "putJSON(url, body, callback, ?headers)",
describe(JsonType.Function, "putJSON(url, body, callback, headers?)",
Resources.ScriptingPutJson);
describe(JsonType.Function, "patchJSON(url, body, callback, headers)",
describe(JsonType.Function, "patchJSON(url, body, callback, headers?)",
Resources.ScriptingPatchJson);
describe(JsonType.Function, "deleteJSON(url, body, callback, headers)",
describe(JsonType.Function, "deleteJSON(url, body, callback, headers?)",
Resources.ScriptingDeleteJson);
}
@ -57,62 +56,51 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{
var action = new HttpJson((url, callback, headers) =>
{
RequestAsync(context, method, url, null, callback, headers).Forget();
Request(context, method, url, null, callback, headers);
});
context.Engine.SetValue(name, action);
}
private void AdBodyMethod(ScriptExecutionContext context, HttpMethod method, string name)
private void AddBodyMethod(ScriptExecutionContext context, HttpMethod method, string name)
{
var action = new HttpJsonWithBody((url, body, callback, headers) =>
{
RequestAsync(context, method, url, body, callback, headers).Forget();
Request(context, method, url, body, callback, headers);
});
context.Engine.SetValue(name, action);
}
private async Task RequestAsync(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action<JsValue> callback, JsValue? headers)
private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action<JsValue> callback, JsValue? headers)
{
if (callback == null)
context.Schedule(async (scheduler, ct) =>
{
context.Fail(new JavaScriptException("Callback cannot be null."));
return;
}
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
context.Fail(new JavaScriptException("URL is not valid."));
return;
}
if (callback == null)
{
throw new JavaScriptException("Callback cannot be null.");
}
context.MarkAsync();
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
throw new JavaScriptException("URL is not valid.");
}
try
{
using (var httpClient = httpClientFactory.CreateClient())
{
using (var request = CreateRequest(context, method, uri, body, headers))
{
using (var response = await httpClient.SendAsync(request, context.CancellationToken))
using (var response = await httpClient.SendAsync(request, ct))
{
response.EnsureSuccessStatusCode();
var responseObject = await ParseResponse(context, response);
var responseObject = await ParseResponseasync(context, response, ct);
// Reset the time contraints and other constraints so that our awaiting does not count as script time.
context.Engine.ResetConstraints();
callback(responseObject);
scheduler.Run(callback, responseObject);
}
}
}
}
catch (Exception ex)
{
context.Fail(ex);
}
});
}
private static HttpRequestMessage CreateRequest(ScriptExecutionContext context, HttpMethod method, Uri uri, JsValue? body, JsValue? headers)
@ -151,16 +139,17 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
return request;
}
private static async Task<JsValue> ParseResponse(ScriptExecutionContext context, HttpResponseMessage response)
private static async Task<JsValue> ParseResponseasync(ScriptExecutionContext context, HttpResponseMessage response,
CancellationToken ct)
{
var responseString = await response.Content.ReadAsStringAsync(context.CancellationToken);
var responseString = await response.Content.ReadAsStringAsync(ct);
context.CancellationToken.ThrowIfCancellationRequested();
ct.ThrowIfCancellationRequested();
var jsonParser = new JsonParser(context.Engine);
var jsonValue = jsonParser.Parse(responseString);
context.CancellationToken.ThrowIfCancellationRequested();
ct.ThrowIfCancellationRequested();
return jsonValue;
}

38
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public static class JintExtensions
{
public static List<DomainId> ToIds(this JsValue? value)
{
var ids = new List<DomainId>();
if (value?.IsString() == true)
{
ids.Add(DomainId.Create(value.ToString()));
}
else if (value?.IsArray() == true)
{
foreach (var item in value.AsArray())
{
if (item.IsString())
{
ids.Add(DomainId.Create(item.ToString()));
}
}
}
return ids;
}
}
}

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

@ -19,6 +19,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
using System.Diagnostics;
namespace Squidex.Domain.Apps.Core.Scripting
{
@ -49,30 +50,20 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct))
{
var tcs = new TaskCompletionSource<IJsonValue>();
var context =
CreateEngine<IJsonValue>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
await using (combined.Token.Register(() => tcs.TrySetCanceled(combined.Token)))
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions, tcs.TrySetException, combined.Token);
context.Complete(JsonMapper.Map(value));
}));
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
tcs.TrySetResult(JsonMapper.Map(value));
}));
var result = Execute(context.Engine, script);
if (!context.IsAsync)
{
tcs.TrySetResult(JsonMapper.Map(result));
}
var result = Execute(context.Engine, script);
return await tcs.Task;
}
return await context.CompleteAsync() ?? JsonMapper.Map(result);
}
}
}
@ -87,50 +78,33 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct))
{
var tcs = new TaskCompletionSource<ContentData>();
var context =
CreateEngine<ContentData>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
await using (combined.Token.Register(() => tcs.TrySetCanceled(combined.Token)))
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions, tcs.TrySetException, combined.Token);
context.Complete(vars.Data!);
}));
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
tcs.TrySetResult(vars.Data!);
}));
context.Engine.SetValue("replace", new Action(() =>
{
var dataInstance = context.Engine.GetValue("ctx").AsObject().Get("data");
context.Engine.SetValue("replace", new Action(() =>
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
var dataInstance = context.Engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
if (!context.IsCompleted && data.TryUpdate(out var modified))
{
if (!tcs.Task.IsCompleted)
{
if (data.TryUpdate(out var modified))
{
tcs.TrySetResult(modified);
}
else
{
tcs.TrySetResult(vars.Data!);
}
}
context.Complete(modified);
}
}));
Execute(context.Engine, script);
if (!context.IsAsync)
{
tcs.TrySetResult(vars.Data!);
}
}));
Execute(context.Engine, script);
return await tcs.Task;
}
return await context.CompleteAsync() ?? vars.Data!;
}
}
}
@ -141,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNullOrEmpty(script);
var context =
CreateEngine(options)
CreateEngine<object>(options, default)
.Extend(vars, options)
.Extend(extensions);
@ -150,14 +124,24 @@ namespace Squidex.Domain.Apps.Core.Scripting
return JsonMapper.Map(result);
}
private ScriptExecutionContext CreateEngine(ScriptOptions options)
private ScriptExecutionContext<T> CreateEngine<T>(ScriptOptions options, CancellationToken ct) where T : class
{
if (Debugger.IsAttached)
{
ct = default;
}
var engine = new Engine(engineOptions =>
{
engineOptions.AddObjectConverter(DefaultConverter.Instance);
engineOptions.SetReferencesResolver(NullPropagation.Instance);
engineOptions.Strict();
engineOptions.TimeoutInterval(timeoutScript);
if (!Debugger.IsAttached)
{
engineOptions.TimeoutInterval(timeoutScript);
engineOptions.CancellationToken(ct);
}
});
if (options.CanDisallow)
@ -175,9 +159,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
extension.Extend(engine);
}
var context = new ScriptExecutionContext(engine);
return context;
return new ScriptExecutionContext<T>(engine, ct);
}
private JsValue Execute(Engine engine, string script)
@ -212,7 +194,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
public void Describe(AddDescription describe, ScriptScope scope)
{
if (scope == ScriptScope.Transform)
if (scope.HasFlag(ScriptScope.Transform) || scope.HasFlag(ScriptScope.ContentScript))
{
describe(JsonType.Function, "replace()",
Resources.ScriptingReplace);

110
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs

@ -6,42 +6,96 @@
// ==========================================================================
using Jint;
using Squidex.Infrastructure.Tasks;
using Squidex.Text;
using System.Diagnostics;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptExecutionContext : ScriptContext
public abstract class ScriptExecutionContext : ScriptContext
{
private Func<Exception, bool>? completion;
public Engine Engine { get; }
public CancellationToken CancellationToken { get; private set; }
protected ScriptExecutionContext(Engine engine)
{
Engine = engine;
}
public abstract void Schedule(Func<IScheduler, CancellationToken, Task> action);
}
#pragma warning disable MA0048 // File name must match type name
public interface IScheduler
#pragma warning restore MA0048 // File name must match type name
{
void Run(Action? action);
void Run<T>(Action<T>? action, T argument);
}
public bool IsAsync { get; private set; }
public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, IScheduler where T : class
{
private readonly TaskCompletionSource<T?> tcs = new TaskCompletionSource<T?>();
private readonly CancellationToken cancellationToken;
private int pendingTasks;
internal ScriptExecutionContext(Engine engine)
public bool IsCompleted
{
Engine = engine;
get => tcs.Task.Status == TaskStatus.RanToCompletion || tcs.Task.Status == TaskStatus.Faulted;
}
public void MarkAsync()
internal ScriptExecutionContext(Engine engine, CancellationToken cancellationToken)
: base(engine)
{
IsAsync = true;
this.cancellationToken = cancellationToken;
}
public void Fail(Exception exception)
public Task<T?> CompleteAsync()
{
completion?.Invoke(exception);
if (pendingTasks <= 0)
{
tcs.TrySetResult(null);
}
return tcs.Task.WithCancellation(cancellationToken);
}
public void Complete(T value)
{
tcs.TrySetResult(value);
}
public ScriptExecutionContext ExtendAsync(IEnumerable<IJintExtension> extensions, Func<Exception, bool> completion,
CancellationToken ct)
public override void Schedule(Func<IScheduler, CancellationToken, Task> action)
{
CancellationToken = ct;
if (IsCompleted)
{
return;
}
async Task ScheduleAsync()
{
try
{
Interlocked.Increment(ref pendingTasks);
this.completion = completion;
await action(this, cancellationToken);
if (Interlocked.Decrement(ref pendingTasks) <= 0)
{
tcs.TrySetResult(null);
}
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
ScheduleAsync().Forget();
}
public ScriptExecutionContext<T> ExtendAsync(IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
{
extension.ExtendAsync(this);
@ -50,7 +104,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
return this;
}
public ScriptExecutionContext Extend(IEnumerable<IJintExtension> extensions)
public ScriptExecutionContext<T> Extend(IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
{
@ -60,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
return this;
}
public ScriptExecutionContext Extend(ScriptVars vars, ScriptOptions options)
public ScriptExecutionContext<T> Extend(ScriptVars vars, ScriptOptions options)
{
var engine = Engine;
@ -95,5 +149,27 @@ namespace Squidex.Domain.Apps.Core.Scripting
return this;
}
void IScheduler.Run(Action? action)
{
if (IsCompleted || action == null)
{
return;
}
Engine.ResetConstraints();
action();
}
void IScheduler.Run<TArg>(Action<TArg>? action, TArg argument)
{
if (IsCompleted || action == null)
{
return;
}
Engine.ResetConstraints();
action(argument);
}
}
}

22
backend/src/Squidex.Domain.Apps.Entities/AppTag.cs

@ -1,22 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Fluid.Tags;
using Microsoft.Extensions.DependencyInjection;
namespace Squidex.Domain.Apps.Entities
{
internal abstract class AppTag : ArgumentsTag
{
protected IAppProvider AppProvider { get; }
protected AppTag(IServiceProvider serviceProvider)
{
AppProvider = serviceProvider.GetRequiredService<IAppProvider>();
}
}
}

28
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs

@ -5,10 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text;
using Squidex.Infrastructure;
using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Domain.Apps.Entities.Assets
{
public static class AssetExtensions
@ -24,29 +20,5 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
return builder.WithBoolean(HeaderNoEnrichment, value);
}
public static async Task<string> GetTextAsync(this IAssetFileStore assetFileStore, DomainId appId, DomainId id, long fileVersion, string? encoding)
{
using (var stream = DefaultPools.MemoryStream.GetStream())
{
await assetFileStore.DownloadAsync(appId, id, fileVersion, null, stream);
stream.Position = 0;
var bytes = stream.ToArray();
switch (encoding?.ToLowerInvariant())
{
case "base64":
return Convert.ToBase64String(bytes);
case "ascii":
return Encoding.ASCII.GetString(bytes);
case "unicode":
return Encoding.Unicode.GetString(bytes);
default:
return Encoding.UTF8.GetString(bytes);
}
}
}
}
}

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

@ -8,8 +8,11 @@
using System.Text.Encodings.Web;
using Fluid;
using Fluid.Ast;
using Fluid.Tags;
using Fluid.Values;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.ValidateContent;
@ -21,32 +24,33 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
private static readonly FluidValue ErrorNullAsset = FluidValue.Create(null);
private static readonly FluidValue ErrorNoAsset = new StringValue("NoAsset");
private static readonly FluidValue ErrorNoImage = new StringValue("NoImage");
private static readonly FluidValue ErrorTooBig = new StringValue("ErrorTooBig");
private readonly IServiceProvider serviceProvider;
private sealed class AssetTag : AppTag
private sealed class AssetTag : ArgumentsTag
{
private readonly IAssetQueryService assetQuery;
private readonly IServiceProvider serviceProvider;
public AssetTag(IServiceProvider serviceProvider)
: base(serviceProvider)
{
assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
this.serviceProvider = serviceProvider;
}
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments)
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 id = await arguments[1].Expression.EvaluateAsync(context);
var content = await ResolveAssetAsync(AppProvider, assetQuery, enrichedEvent.AppId.Id, id);
var asset = await ResolveAssetAsync(serviceProvider, enrichedEvent.AppId.Id, id);
if (content != null)
if (asset != null)
{
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue();
context.SetValue(name, content);
context.SetValue(name, asset);
}
}
@ -75,15 +79,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
private void AddAssetFilter()
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
TemplateContext.GlobalFilters.AddAsyncFilter("asset", async (input, arguments, context) =>
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent)
{
var asset = await ResolveAssetAsync(appProvider, assetQuery, enrichedEvent.AppId.Id, input);
var asset = await ResolveAssetAsync(serviceProvider, enrichedEvent.AppId.Id, input);
if (asset == null)
{
@ -99,8 +99,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
private void AddAssetTextFilter()
{
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
TemplateContext.GlobalFilters.AddAsyncFilter("assetText", async (input, arguments, context) =>
{
if (input is not ObjectValue objectValue)
@ -108,15 +106,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
return ErrorNoAsset;
}
async Task<FluidValue> ResolveAssetText(DomainId appId, DomainId id, long fileSize, long fileVersion)
async Task<FluidValue> ResolveAssetTextAsync(AssetRef asset)
{
if (fileSize > 256_000)
if (asset.FileSize > 256_000)
{
return ErrorTooBig;
}
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
var encoding = arguments.At(0).ToStringValue()?.ToUpperInvariant();
var encoded = await assetFileStore.GetTextAsync(appId, id, fileVersion, encoding);
var encoded = await asset.GetTextAsync(encoding, assetFileStore, default);
return new StringValue(encoded);
}
@ -124,10 +124,64 @@ namespace Squidex.Domain.Apps.Entities.Assets
switch (objectValue.ToObjectValue())
{
case IAssetEntity asset:
return await ResolveAssetText(asset.AppId.Id, asset.Id, asset.FileSize, asset.FileVersion);
return await ResolveAssetTextAsync(asset.ToRef());
case EnrichedAssetEvent @event:
return await ResolveAssetTextAsync(@event.ToRef());
}
return ErrorNoAsset;
});
TemplateContext.GlobalFilters.AddAsyncFilter("assetBlurHash", async (input, arguments, context) =>
{
if (input is not ObjectValue objectValue)
{
return ErrorNoAsset;
}
async Task<FluidValue> ResolveAssetHashAsync(AssetRef asset)
{
if (asset.FileSize > 512_000)
{
return ErrorTooBig;
}
if (asset.Type != AssetType.Image)
{
return ErrorNoImage;
}
var options = new BlurOptions();
var arg0 = arguments.At(0);
var arg1 = arguments.At(1);
if (arg0.Type == FluidValues.Number)
{
options.ComponentX = (int)arg0.ToNumberValue();
}
if (arg1.Type == FluidValues.Number)
{
options.ComponentX = (int)arg1.ToNumberValue();
}
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>();
var blur = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, default);
return new StringValue(blur);
}
switch (objectValue.ToObjectValue())
{
case IAssetEntity asset:
return await ResolveAssetHashAsync(asset.ToRef());
case EnrichedAssetEvent @event:
return await ResolveAssetText(@event.AppId.Id, @event.Id, @event.FileSize, @event.FileVersion);
return await ResolveAssetHashAsync(@event.ToRef());
}
return ErrorNoAsset;
@ -139,8 +193,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
factory.RegisterTag("asset", new AssetTag(serviceProvider));
}
private static async Task<IAssetEntity?> ResolveAssetAsync(IAppProvider appProvider, IAssetQueryService assetQuery, DomainId appId, FluidValue id)
private static async Task<IAssetEntity?> ResolveAssetAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(appId);
if (app == null)
@ -150,6 +206,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
var domainId = DomainId.Create(id.ToStringValue());
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var requestContext =
Context.Admin(app).Clone(b => b
.WithoutTotal());

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

@ -11,19 +11,21 @@ using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Properties;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
{
private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback);
private delegate void GetAssetTextDelegate(JsValue references, Action<JsValue> callback, JsValue encoding);
private delegate void GetAssetTextDelegate(JsValue asset, Action<JsValue> callback, JsValue? encoding);
private delegate void GetBlurHashDelegate(JsValue asset, Action<JsValue> callback, JsValue? componentX, JsValue? componentY);
private readonly IServiceProvider serviceProvider;
public AssetsJintExtension(IServiceProvider serviceProvider)
@ -34,6 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public void ExtendAsync(ScriptExecutionContext context)
{
AddAssetText(context);
AddAssetBlurHash(context);
AddAsset(context);
}
@ -45,8 +48,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
describe(JsonType.Function, "getAsset(ids, callback)",
Resources.ScriptingGetAsset);
describe(JsonType.Function, "getAssetText(asset, callback, encoding)",
describe(JsonType.Function, "getAssetText(asset, callback, encoding?)",
Resources.ScriptingGetAssetText);
describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)",
Resources.ScriptingGetBlurHash);
}
private void AddAsset(ScriptExecutionContext context)
@ -61,138 +67,193 @@ namespace Squidex.Domain.Apps.Entities.Assets
return;
}
var action = new GetAssetsDelegate((references, callback) => GetAssets(context, appId, user, references, callback));
var getAssets = new GetAssetsDelegate((references, callback) =>
{
GetAssets(context, appId, user, references, callback);
});
context.Engine.SetValue("getAsset", action);
context.Engine.SetValue("getAssets", action);
context.Engine.SetValue("getAsset", getAssets);
context.Engine.SetValue("getAssets", getAssets);
}
private void AddAssetText(ScriptExecutionContext context)
{
var action = new GetAssetTextDelegate((references, callback, encoding) => GetText(context, references, callback, encoding));
var action = new GetAssetTextDelegate((references, callback, encoding) =>
{
GetText(context, references, callback, encoding);
});
context.Engine.SetValue("getAssetText", action);
}
private void GetText(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue encoding)
private void AddAssetBlurHash(ScriptExecutionContext context)
{
GetTextAsync(context, input, callback, encoding).Forget();
var getBlurHash = new GetBlurHashDelegate((input, callback, componentX, componentY) =>
{
GetBlurHash(context, input, callback, componentX, componentY);
});
context.Engine.SetValue("getAssetBlurHash", getBlurHash);
}
private async Task GetTextAsync(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue encoding)
private void GetText(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? encoding)
{
Guard.NotNull(callback);
if (input is not ObjectWrapper objectWrapper)
context.Schedule(async (scheduler, ct) =>
{
callback(JsValue.FromObject(context.Engine, "ErrorNoAsset"));
return;
}
async Task ResolveAssetText(DomainId appId, DomainId id, long fileSize, long fileVersion)
{
if (fileSize > 256_000)
if (input is not ObjectWrapper objectWrapper)
{
callback(JsValue.FromObject(context.Engine, "ErrorTooBig"));
scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset"));
return;
}
context.MarkAsync();
try
async Task ResolveAssetText(AssetRef asset)
{
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
var encoded = await assetFileStore.GetTextAsync(appId, id, fileVersion, encoding?.ToString());
if (asset.FileSize > 256_000)
{
scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorTooBig"));
return;
}
// Reset the time contraints and other constraints so that our awaiting does not count as script time.
context.Engine.ResetConstraints();
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
try
{
var text = await asset.GetTextAsync(encoding?.ToString(), assetFileStore, ct);
callback(JsValue.FromObject(context.Engine, encoded));
scheduler.Run(callback, JsValue.FromObject(context.Engine, text));
}
catch
{
scheduler.Run(callback, JsValue.Null);
}
}
catch (Exception ex)
switch (objectWrapper.Target)
{
context.Fail(ex);
case IAssetEntity asset:
await ResolveAssetText(asset.ToRef());
break;
case EnrichedAssetEvent e:
await ResolveAssetText(e.ToRef());
break;
default:
scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset"));
break;
}
}
});
}
private void GetBlurHash(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY)
{
Guard.NotNull(callback);
switch (objectWrapper.Target)
context.Schedule(async (scheduler, ct) =>
{
case IAssetEntity asset:
await ResolveAssetText(asset.AppId.Id, asset.Id, asset.FileSize, asset.FileVersion);
if (input is not ObjectWrapper objectWrapper)
{
scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset"));
return;
}
case EnrichedAssetEvent @event:
await ResolveAssetText(@event.AppId.Id, @event.Id, @event.FileSize, @event.FileVersion);
return;
}
async Task ResolveHashAsync(AssetRef asset)
{
if (asset.FileSize > 512_000 || asset.Type != AssetType.Image)
{
scheduler.Run(callback, JsValue.Null);
return;
}
callback(JsValue.FromObject(context.Engine, "ErrorNoAsset"));
}
var options = new BlurOptions();
private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
GetReferencesAsync(context, appId, user, references, callback).Forget();
if (componentX?.IsNumber() == true)
{
options.ComponentX = (int)componentX.AsNumber();
}
if (componentY?.IsNumber() == true)
{
options.ComponentX = (int)componentX.AsNumber();
}
var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>();
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
try
{
var hash = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, hash));
}
catch
{
scheduler.Run(callback, JsValue.Null);
}
}
switch (objectWrapper.Target)
{
case IAssetEntity asset:
await ResolveHashAsync(asset.ToRef());
break;
case EnrichedAssetEvent @event:
await ResolveHashAsync(@event.ToRef());
break;
default:
scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset"));
break;
}
});
}
private async Task GetReferencesAsync(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);
var ids = new List<DomainId>();
if (references.IsString())
context.Schedule(async (scheduler, ct) =>
{
ids.Add(DomainId.Create(references.ToString()));
}
else if (references.IsArray())
{
foreach (var value in references.AsArray())
var ids = references.ToIds();
if (ids.Count == 0)
{
if (value.IsString())
{
ids.Add(DomainId.Create(value.ToString()));
}
var emptyAssets = Array.Empty<IEnrichedAssetEntity>();
scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets));
return;
}
}
if (ids.Count == 0)
{
var emptyAssets = Array.Empty<IEnrichedAssetEntity>();
var app = await GetAppAsync(appId, ct);
callback(JsValue.FromObject(context.Engine, emptyAssets));
return;
}
if (app == null)
{
var emptyAssets = Array.Empty<IEnrichedAssetEntity>();
context.MarkAsync();
scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets));
return;
}
try
{
var app = await GetAppAsync(appId);
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var requestContext =
new Context(user, app).Clone(b => b
.WithoutTotal());
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var assetItems = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), context.CancellationToken);
// Reset the time contraints and other constraints so that our awaiting does not count as script time.
context.Engine.ResetConstraints();
var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct);
callback(JsValue.FromObject(context.Engine, assetItems.ToArray()));
}
catch (Exception ex)
{
context.Fail(ex);
}
scheduler.Run(callback, JsValue.FromObject(context.Engine, assets.ToArray()));
return;
});
}
private async Task<IAppEntity> GetAppAsync(DomainId appId)
private async Task<IAppEntity> GetAppAsync(DomainId appId,
CancellationToken ct)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(appId);
var app = await appProvider.GetAppAsync(appId, false, ct);
if (app == null)
{

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

@ -0,0 +1,92 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.ObjectPool;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Assets
{
public record struct AssetRef(
DomainId AppId,
DomainId Id,
long FileVersion,
long FileSize,
string MimeType,
AssetType Type);
public static class Transformations
{
public static AssetRef ToRef(this EnrichedAssetEvent @event)
{
return new AssetRef(
@event.AppId.Id,
@event.Id,
@event.FileVersion,
@event.FileSize,
@event.MimeType,
@event.AssetType);
}
public static AssetRef ToRef(this IAssetEntity asset)
{
return new AssetRef(
asset.AppId.Id,
asset.Id,
asset.FileVersion,
asset.FileSize,
asset.MimeType,
asset.Type);
}
public static async Task<string> GetTextAsync(this AssetRef asset, string? encoding,
IAssetFileStore assetFileStore,
CancellationToken ct = default)
{
using (var stream = DefaultPools.MemoryStream.GetStream())
{
await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct);
stream.Position = 0;
var bytes = stream.ToArray();
switch (encoding?.ToLowerInvariant())
{
case "base64":
return Convert.ToBase64String(bytes);
case "ascii":
return Encoding.ASCII.GetString(bytes);
case "unicode":
return Encoding.Unicode.GetString(bytes);
default:
return Encoding.UTF8.GetString(bytes);
}
}
}
public static async Task<string?> GetBlurHashAsync(this AssetRef asset, BlurOptions options,
IAssetFileStore assetFileStore,
IAssetThumbnailGenerator thumbnailGenerator, CancellationToken ct = default)
{
using (var stream = DefaultPools.MemoryStream.GetStream())
{
await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct);
stream.Position = 0;
return await thumbnailGenerator.ComputeBlurHashAsync(stream, asset.MimeType, options, ct);
}
}
}
}

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint.Native;
using Orleans;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Properties;
@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
{
private delegate long CounterReset(string name, long value = 0);
private delegate void CounterResetV2(string name, Action<JsValue>? callback = null, long value = 0);
private readonly IGrainFactory grainFactory;
public CounterJintExtension(IGrainFactory grainFactory)
@ -24,20 +27,46 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
public void Extend(ScriptExecutionContext context)
{
if (context.TryGetValue<DomainId>("appId", out var appId))
if (!context.TryGetValue<DomainId>("appId", out var appId))
{
var engine = context.Engine;
return;
}
engine.SetValue("incrementCounter", new Func<string, long>(name =>
{
return Increment(appId, name);
}));
var increment = new Func<string, long>(name =>
{
return Increment(appId, name);
});
engine.SetValue("resetCounter", new Func<string, long, long>((name, value) =>
{
return Reset(appId, name, value);
}));
context.Engine.SetValue("incrementCounter", increment);
var reset = new CounterReset((name, value) =>
{
return Reset(appId, name, value);
});
context.Engine.SetValue("resetCounter", reset);
}
public void ExtendAsync(ScriptExecutionContext context)
{
if (!context.TryGetValue<DomainId>("appId", out var appId))
{
return;
}
var increment = new Action<string, Action<JsValue>>((name, callback) =>
{
IncrementV2(context, appId, name, callback);
});
context.Engine.SetValue("incrementCounterV2", increment);
var reset = new CounterResetV2((name, callback, value) =>
{
ResetV2(context, appId, name, callback, value);
});
context.Engine.SetValue("resetCounterV2", reset);
}
public void Describe(AddDescription describe, ScriptScope scope)
@ -45,8 +74,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
describe(JsonType.Function, "incrementCounter(name)",
Resources.ScriptingIncrementCounter);
describe(JsonType.Function, "resetCounter(name, value)",
describe(JsonType.Function, "incrementCounterV2(name, callback?)",
Resources.ScriptingIncrementCounterV2);
describe(JsonType.Function, "resetCounter(name, value?)",
Resources.ScriptingResetCounter);
describe(JsonType.Function, "resetCounter(name, callback?, value?)",
Resources.ScriptingResetCounterV2);
}
private long Increment(DomainId appId, string name)
@ -56,11 +91,41 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
return AsyncHelper.Sync(() => grain.IncrementAsync(name));
}
private void IncrementV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue> callback)
{
context.Schedule(async (scheduler, ct) =>
{
var grain = grainFactory.GetGrain<ICounterGrain>(appId.ToString());
var result = await grain.IncrementAsync(name);
if (callback != null)
{
scheduler.Run(callback, JsValue.FromObject(context.Engine, result));
}
});
}
private long Reset(DomainId appId, string name, long value)
{
var grain = grainFactory.GetGrain<ICounterGrain>(appId.ToString());
return AsyncHelper.Sync(() => grain.ResetAsync(name, value));
}
private void ResetV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue>? callback, long value)
{
context.Schedule(async (scheduler, ct) =>
{
var grain = grainFactory.GetGrain<ICounterGrain>(appId.ToString());
var result = await grain.ResetAsync(name, value);
if (callback != null)
{
scheduler.Run(callback, JsValue.FromObject(context.Engine, result));
}
});
}
}
}

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

@ -8,6 +8,7 @@
using System.Text.Encodings.Web;
using Fluid;
using Fluid.Ast;
using Fluid.Tags;
using Fluid.Values;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
@ -23,23 +24,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
private static readonly FluidValue ErrorNullReference = FluidValue.Create(null);
private readonly IServiceProvider serviceProvider;
private sealed class ReferenceTag : AppTag
private sealed class ReferenceTag : ArgumentsTag
{
private readonly IContentQueryService contentQuery;
private readonly IServiceProvider serviceProvider;
public ReferenceTag(IServiceProvider serviceProvider)
: base(serviceProvider)
{
contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
this.serviceProvider = serviceProvider;
}
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments)
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 id = await arguments[1].Expression.EvaluateAsync(context);
var content = await ResolveContentAsync(AppProvider, contentQuery, enrichedEvent.AppId.Id, id);
var content = await ResolveContentAsync(serviceProvider, enrichedEvent.AppId.Id, id);
if (content != null)
{
@ -72,15 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
private void AddReferenceFilter()
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
TemplateContext.GlobalFilters.AddAsyncFilter("reference", async (input, arguments, context) =>
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent)
{
var content = await ResolveContentAsync(appProvider, contentQuery, enrichedEvent.AppId.Id, input);
var content = await ResolveContentAsync(serviceProvider, enrichedEvent.AppId.Id, input);
if (content == null)
{
@ -99,8 +96,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
factory.RegisterTag("reference", new ReferenceTag(serviceProvider));
}
private static async Task<IContentEntity?> ResolveContentAsync(IAppProvider appProvider, IContentQueryService contentQuery, DomainId appId, FluidValue id)
private static async Task<IContentEntity?> ResolveContentAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(appId);
if (app == null)
@ -109,17 +108,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
var domainId = DomainId.Create(id.ToStringValue());
var domainIds = new List<DomainId> { domainId };
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var requestContext =
Context.Admin(app).Clone(b => b
.WithoutContentEnrichment()
.WithUnpublished()
.WithoutTotal());
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds));
var content = contents.FirstOrDefault();
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainId));
return content;
return contents.FirstOrDefault();
}
}
}

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

@ -6,7 +6,6 @@
// ==========================================================================
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.Extensions.DependencyInjection;
@ -14,7 +13,6 @@ using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Properties;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
@ -40,7 +38,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
return;
}
var action = new GetReferencesDelegate((references, callback) => GetReferences(context, appId, user, references, callback));
var action = new GetReferencesDelegate((references, callback) =>
{
GetReferences(context, appId, user, references, callback);
});
context.Engine.SetValue("getReference", action);
context.Engine.SetValue("getReferences", action);
@ -56,44 +57,32 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
GetReferencesAsync(context, appId, user, references, callback).Forget();
}
private async Task GetReferencesAsync(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);
var ids = new List<DomainId>();
if (references.IsString())
{
ids.Add(DomainId.Create(references.ToString()));
}
else if (references.IsArray())
context.Schedule(async (scheduler, ct) =>
{
foreach (var value in references.AsArray())
var ids = references.ToIds();
if (ids.Count == 0)
{
if (value.IsString())
{
ids.Add(DomainId.Create(value.ToString()));
}
var emptyContents = Array.Empty<IEnrichedContentEntity>();
scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents));
return;
}
}
if (ids.Count == 0)
{
var emptyContents = Array.Empty<IEnrichedContentEntity>();
var app = await GetAppAsync(appId);
callback(JsValue.FromObject(context.Engine, emptyContents));
return;
}
if (app == null)
{
var emptyContents = Array.Empty<IEnrichedContentEntity>();
context.MarkAsync();
scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents));
return;
}
try
{
var app = await GetAppAsync(appId);
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var requestContext =
new Context(user, app).Clone(b => b
@ -101,19 +90,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
.WithUnpublished()
.WithoutTotal());
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), context.CancellationToken);
// Reset the time contraints and other constraints so that our awaiting does not count as script time.
context.Engine.ResetConstraints();
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct);
callback(JsValue.FromObject(context.Engine, contents.ToArray()));
}
catch (Exception ex)
{
context.Fail(ex);
}
scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.ToArray()));
});
}
private async Task<IAppEntity> GetAppAsync(DomainId appId)

33
backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Properties {
}
/// <summary>
/// Looks up a localized string similar to ueries the assets with the specified IDs and invokes the callback with an array of assets..
/// Looks up a localized string similar to Queries the assets with the specified IDs and invokes the callback with an array of assets..
/// </summary>
internal static string ScriptingGetAssets {
get {
@ -87,6 +87,15 @@ namespace Squidex.Domain.Apps.Entities.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Gets the blur hash of an asset if it is an image or null otherwise..
/// </summary>
internal static string ScriptingGetBlurHash {
get {
return ResourceManager.GetString("ScriptingGetBlurHash", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Queries the content item with the specified ID and invokes the callback with an array of contents..
/// </summary>
@ -106,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Properties {
}
/// <summary>
/// Looks up a localized string similar to Increments the counter with the given name and returns the value..
/// Looks up a localized string similar to Increments the counter with the given name and returns the value (OBSOLETE)..
/// </summary>
internal static string ScriptingIncrementCounter {
get {
@ -115,12 +124,30 @@ namespace Squidex.Domain.Apps.Entities.Properties {
}
/// <summary>
/// Looks up a localized string similar to Resets the counter with the given name to zero..
/// Looks up a localized string similar to Increments the counter with the given name and returns the value..
/// </summary>
internal static string ScriptingIncrementCounterV2 {
get {
return ResourceManager.GetString("ScriptingIncrementCounterV2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Resets the counter with the given name to zero (OBSOLETE)..
/// </summary>
internal static string ScriptingResetCounter {
get {
return ResourceManager.GetString("ScriptingResetCounter", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Resets the counter with the given name to zero..
/// </summary>
internal static string ScriptingResetCounterV2 {
get {
return ResourceManager.GetString("ScriptingResetCounterV2", resourceCulture);
}
}
}
}

11
backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx

@ -121,11 +121,14 @@
<value>Queries the asset with the specified ID and invokes the callback with an array of assets.</value>
</data>
<data name="ScriptingGetAssets" xml:space="preserve">
<value>ueries the assets with the specified IDs and invokes the callback with an array of assets.</value>
<value>Queries the assets with the specified IDs and invokes the callback with an array of assets.</value>
</data>
<data name="ScriptingGetAssetText" xml:space="preserve">
<value>Get the text of an asset. Encodings: base64,ascii,unicode,utf8</value>
</data>
<data name="ScriptingGetBlurHash" xml:space="preserve">
<value>Gets the blur hash of an asset if it is an image or null otherwise.</value>
</data>
<data name="ScriptingGetReference" xml:space="preserve">
<value>Queries the content item with the specified ID and invokes the callback with an array of contents.</value>
</data>
@ -133,9 +136,15 @@
<value>Queries the content items with the specified IDs and invokes the callback with an array of contents.</value>
</data>
<data name="ScriptingIncrementCounter" xml:space="preserve">
<value>Increments the counter with the given name and returns the value (OBSOLETE).</value>
</data>
<data name="ScriptingIncrementCounterV2" xml:space="preserve">
<value>Increments the counter with the given name and returns the value.</value>
</data>
<data name="ScriptingResetCounter" xml:space="preserve">
<value>Resets the counter with the given name to zero (OBSOLETE).</value>
</data>
<data name="ScriptingResetCounterV2" xml:space="preserve">
<value>Resets the counter with the given name to zero.</value>
</data>
</root>

2
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -31,7 +31,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OpenTelemetry.Api" Version="1.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="2.17.0" />
<PackageReference Include="Squidex.Assets" Version="2.18.0" />
<PackageReference Include="Squidex.Caching" Version="1.9.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="2.13.0" />
<PackageReference Include="Squidex.Log" Version="1.6.0" />

1
backend/src/Squidex.Web/ETagExtensions.cs

@ -9,7 +9,6 @@ using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;

18
backend/src/Squidex/Squidex.csproj

@ -74,16 +74,16 @@
<PackageReference Include="OrleansDashboard.EmbeddedAssets" Version="3.6.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.0.3" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.S3" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="2.17.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.S3" Version="2.18.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="2.18.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.9.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.6.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="8.8.0" />
<PackageReference Include="Squidex.Hosting" Version="2.13.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

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

@ -194,11 +194,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
};
const string script = @"
async = true;
var x = 0;
getJSON('http://squidex.io', function(result) {
getJSON('http://mockup.squidex.io', function(result) {
complete();
});
";
@ -258,11 +256,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
};
const string script = @"
async = true;
var data = ctx.data;
getJSON('http://squidex.io', function(result) {
getJSON('http://mockup.squidex.io', function(result) {
data.operation = { iv: result.key };
replace(data);
@ -288,7 +284,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
const string script = @"
var data = ctx.data;
getJSON('http://squidex.io', function(result) {
getJSON('http://mockup.squidex.io', function(result) {
data.operation = { iv: result.key };
replace(data);
@ -302,7 +298,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
}
[Fact]
public async Task TransformAsync_should_timeout_if_replace_never_called()
public async Task TransformAsync_should_not_timeout_if_replace_never_called()
{
var vars = new ScriptVars
{
@ -312,16 +308,14 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
};
const string script = @"
async = true;
var data = ctx.data;
getJSON('http://squidex.io', function(result) {
getJSON('http://cloud.squidex.io/healthz', function(result) {
data.operation = { iv: result.key };
});
";
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sut.TransformAsync(vars, script, contentOptions));
await sut.TransformAsync(vars, script, contentOptions);
}
[Fact]

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

@ -9,6 +9,7 @@ using System.Text;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
@ -21,9 +22,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetsFluidExtensionTests
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly FluidTemplateEngine sut;
@ -32,8 +34,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
var services =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.AddSingleton(assetFileStore)
.AddSingleton(assetQuery)
.AddSingleton(assetThumbnailGenerator)
.BuildServiceProvider();
var extensions = new IFluidExtension[]
@ -47,6 +50,29 @@ namespace Squidex.Domain.Apps.Entities.Assets
sut = new FluidTemplateEngine(extensions);
}
public static IEnumerable<object[]> Encodings()
{
yield return new object[] { "ascii" };
yield return new object[] { "unicode" };
yield return new object[] { "utf8" };
yield return new object[] { "base64" };
}
public static byte[] Encode(string encoding, string text)
{
switch (encoding)
{
case "base64":
return Convert.FromBase64String(text);
case "ascii":
return Encoding.ASCII.GetBytes(text);
case "unicode":
return Encoding.Unicode.GetBytes(text);
default:
return Encoding.UTF8.GetBytes(text);
}
}
[Fact]
public async Task Should_resolve_assets_in_loop()
{
@ -91,20 +117,21 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_resolve_asset_text()
[Theory]
[MemberData(nameof(Encodings))]
public async Task Should_resolve_text(string encoding)
{
var (vars, asset) = SetupAssetVars();
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
SetupText(asset.ToRef(), Encode(encoding, "hello+assets"));
var template = @"
{% assign ref = event.data.assets.iv[0] | asset %}
Text: {{ ref | assetText }}
var template = $@"
{{% assign ref = event.data.assets.iv[0] | asset %}}
Text: {{{{ ref | assetText: '{encoding}' }}}}
";
var expected = $@"
Text: Hello Asset
Text: hello+assets
";
var result = await sut.RenderAsync(template, vars);
@ -113,40 +140,52 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_with_utf8()
public async Task Should_not_resolve_text_if_too_big()
{
var (vars, asset) = SetupAssetVars();
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var (vars, _) = SetupAssetVars(1_000_000);
var template = @"
{% assign ref = event.data.assets.iv[0] | asset %}
Text: {{ ref | assetText: 'utf8' }}
Text: {{ ref | assetText }}
";
var expected = $@"
Text: Hello Asset
Text: ErrorTooBig
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_resolve_asset_text_with_unicode()
[Theory]
[MemberData(nameof(Encodings))]
public async Task Should_resolve_text_from_event(string encoding)
{
var (vars, asset) = SetupAssetVars();
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
FileVersion = 0,
FileSize = 100,
AppId = appId
};
SetupText(asset.Id, Encoding.Unicode.GetBytes("Hello Asset"));
SetupText(@event.ToRef(), Encode(encoding, "hello+assets"));
var template = @"
{% assign ref = event.data.assets.iv[0] | asset %}
Text: {{ ref | assetText: 'unicode' }}
var vars = new TemplateVars
{
["event"] = @event
};
var template = $@"
Text: {{{{ event | assetText: '{encoding}' }}}}
";
var expected = $@"
Text: Hello Asset
Text: hello+assets
";
var result = await sut.RenderAsync(template, vars);
@ -155,19 +194,19 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_with_ascii()
public async Task Should_resolve_blur_hash()
{
var (vars, asset) = SetupAssetVars();
SetupText(asset.Id, Encoding.ASCII.GetBytes("Hello Asset"));
SetupBlurHash(asset.ToRef(), "Hash");
var template = @"
{% assign ref = event.data.assets.iv[0] | asset %}
Text: {{ ref | assetText: 'ascii' }}
Text: {{ ref | assetBlurHash: 3,4 }}
";
var expected = $@"
Text: Hello Asset
Text: Hash
";
var result = await sut.RenderAsync(template, vars);
@ -176,38 +215,39 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_with_base64()
public async Task Should_not_resolve_blur_hash_if_too_big()
{
var (vars, asset) = SetupAssetVars();
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var (vars, _) = SetupAssetVars(1_000_000);
var template = @"
{% assign ref = event.data.assets.iv[0] | asset %}
Text: {{ ref | assetText: 'base64' }}
Text: {{ ref | assetBlurHash }}
";
var expected = $@"
Text: SGVsbG8gQXNzZXQ=
Text: ErrorTooBig
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_resolve_asset_text_if_too_big()
public async Task Should_not_resolve_blur_hash_if_not_an_image()
{
var (vars, _) = SetupAssetVars(1_000_000);
var (vars, _) = SetupAssetVars(type: AssetType.Unknown);
var template = @"
{% assign ref = event.data.assets.iv[0] | asset %}
Text: {{ ref | assetText }}
Text: {{ ref | assetBlurHash }}
";
var expected = $@"
Text: ErrorTooBig
Text: NoImage
";
var result = await sut.RenderAsync(template, vars);
@ -219,17 +259,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_from_event()
public async Task Should_resolve_blur_hash_from_event()
{
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
AssetType = AssetType.Image,
FileVersion = 0,
FileSize = 100,
AppId = appId
};
SetupText(@event.Id, Encoding.UTF8.GetBytes("Hello Asset"));
SetupBlurHash(@event.ToRef(), "Hash");
var vars = new TemplateVars
{
@ -237,11 +278,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
};
var template = @"
Text: {{ event | assetText }}
Text: {{ event | assetBlurHash }}
";
var expected = $@"
Text: Hello Asset
Text: Hash
";
var result = await sut.RenderAsync(template, vars);
@ -249,53 +290,22 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_not_resolve_asset_text_from_event_if_too_big()
private void SetupBlurHash(AssetRef asset, string hash)
{
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
FileVersion = 0,
FileSize = 1_000_000,
AppId = appId
};
var vars = new TemplateVars
{
["event"] = @event
};
var template = @"
Text: {{ event | assetText }}
";
var expected = $@"
Text: ErrorTooBig
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._))
.Returns(hash);
}
private void SetupText(DomainId id, byte[] bytes)
private void SetupText(AssetRef asset, byte[] bytes)
{
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.Invokes(x =>
{
var stream = x.GetArgument<Stream>(4)!;
stream.Write(bytes);
});
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.Invokes(x => x.GetArgument<Stream>(4)?.Write(bytes));
}
private (TemplateVars, IAssetEntity) SetupAssetVars(int fileSize = 100)
private (TemplateVars, IAssetEntity) SetupAssetVars(int fileSize = 100, AssetType type = AssetType.Image)
{
var assetId = DomainId.NewGuid();
var asset = CreateAsset(assetId, 1, fileSize);
var asset = CreateAsset(assetId, 1, fileSize, type);
var @event = new EnrichedContentEvent
{
@ -310,8 +320,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId, EtagVersion.Any, A<CancellationToken>._))
.Returns(asset);
SetupText(@event.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var vars = new TemplateVars
{
["event"] = @event
@ -320,12 +328,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
return (vars, asset);
}
private (TemplateVars, IAssetEntity[]) SetupAssetsVars(int fileSize = 100)
private (TemplateVars, IAssetEntity[]) SetupAssetsVars(int fileSize = 100, AssetType type = AssetType.Image)
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1, fileSize);
var asset1 = CreateAsset(assetId1, 1, fileSize, type);
var assetId2 = DomainId.NewGuid();
var asset2 = CreateAsset(assetId2, 2, fileSize);
var asset2 = CreateAsset(assetId2, 2, fileSize, type);
var @event = new EnrichedContentEvent
{
@ -351,7 +359,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return (vars, new[] { asset1, asset2 });
}
private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100)
private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100, AssetType type = AssetType.Unknown)
{
return new AssetEntity
{
@ -359,6 +367,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
Id = assetId,
FileSize = fileSize,
FileName = $"file{index}.jpg",
MimeType = "image/jpg",
Type = type
};
}

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

@ -12,6 +12,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
@ -25,9 +26,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetsJintExtensionTests : IClassFixture<TranslationsFixture>
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly JintScriptEngine sut;
@ -36,8 +38,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
var services =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.AddSingleton(assetFileStore)
.AddSingleton(assetQuery)
.AddSingleton(assetThumbnailGenerator)
.BuildServiceProvider();
var extensions = new IJintExtension[]
@ -45,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
new AssetsJintExtension(services)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default))
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, A<CancellationToken>._))
.Returns(Mocks.App(appId));
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
@ -57,13 +60,36 @@ namespace Squidex.Domain.Apps.Entities.Assets
extensions);
}
public static IEnumerable<object[]> Encodings()
{
yield return new object[] { "ascii" };
yield return new object[] { "unicode" };
yield return new object[] { "utf8" };
yield return new object[] { "base64" };
}
public static byte[] Encode(string encoding, string text)
{
switch (encoding)
{
case "base64":
return Convert.FromBase64String(text);
case "ascii":
return Encoding.ASCII.GetBytes(text);
case "unicode":
return Encoding.Unicode.GetBytes(text);
default:
return Encoding.UTF8.GetBytes(text);
}
}
[Fact]
public async Task Should_resolve_asset()
{
var (vars, asset) = SetupAssetVars();
var (vars, assets) = SetupAssetsVars(1);
var expected = $@"
Text: {asset.FileName} {asset.Id}
Text: {assets[0].FileName} {assets[0].Id}
";
var script = @"
@ -81,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact]
public async Task Should_resolve_assets()
{
var (vars, assets) = SetupAssetsVars();
var (vars, assets) = SetupAssetsVars(2);
var expected = $@"
Text: {assets[0].FileName} {assets[0].Id}
@ -101,25 +127,26 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_resolve_asset_text()
[Theory]
[MemberData(nameof(Encodings))]
public async Task Should_resolve_text(string encoding)
{
var (vars, asset) = SetupAssetVars();
var (vars, assets) = SetupAssetsVars(1);
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
SetupText(assets[0].ToRef(), Encode(encoding, "hello+assets"));
var expected = @"
Text: Hello Asset
Text: hello+assets
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
var result = `Text: ${text}`;
var script = $@"
getAssets(data.assets.iv, function (assets) {{
getAssetText(assets[0], function (text) {{
var result = `Text: ${{text}}`;
complete(result);
});
});";
}}, '{encoding}');
}});";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
@ -127,14 +154,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_with_utf8()
public async Task Should_not_resolve_text_if_too_big()
{
var (vars, asset) = SetupAssetVars();
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var (vars, _) = SetupAssetsVars(1, 1_000_000);
var expected = @"
Text: Hello Asset
Text: ErrorTooBig
";
var script = @"
@ -143,33 +168,46 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = `Text: ${text}`;
complete(result);
}, 'utf8');
});
});";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_resolve_asset_text_with_unicode()
[Theory]
[MemberData(nameof(Encodings))]
public async Task Should_resolve_text_from_event(string encoding)
{
var (vars, asset) = SetupAssetVars();
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
FileVersion = 0,
FileSize = 100,
AppId = appId
};
SetupText(@event.ToRef(), Encode(encoding, "hello+assets"));
SetupText(asset.Id, Encoding.Unicode.GetBytes("Hello Asset"));
var vars = new ScriptVars
{
["event"] = @event
};
var expected = @"
Text: Hello Asset
Text: hello+assets
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
var result = `Text: ${text}`;
var script = $@"
getAssetText(event, function (text) {{
var result = `Text: ${{text}}`;
complete(result);
}, 'unicode');
});";
complete(result);
}}, '{encoding}');";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
@ -177,23 +215,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_with_ascii()
public async Task Should_resolve_blur_hash()
{
var (vars, asset) = SetupAssetVars();
var (vars, assets) = SetupAssetsVars(1);
SetupText(asset.Id, Encoding.ASCII.GetBytes("Hello Asset"));
SetupBlurHash(assets[0].ToRef(), "Hash");
var expected = @"
Text: Hello Asset
Hash: Hash
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
var result = `Text: ${text}`;
getAssetBlurHash(assets[0], function (text) {
var result = `Hash: ${text}`;
complete(result);
}, 'ascii');
});
});";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
@ -202,23 +240,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_resolve_asset_text_with_base64()
public async Task Should_not_resolve_blur_hash_if_too_big()
{
var (vars, asset) = SetupAssetVars();
var (vars, assets) = SetupAssetsVars(1, 1_000_000);
SetupText(asset.Id, Encoding.UTF8.GetBytes("Hello Asset"));
SetupBlurHash(assets[0].ToRef(), "Hash");
var expected = @"
Text: SGVsbG8gQXNzZXQ=
Hash: null
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
var result = `Text: ${text}`;
getAssetBlurHash(assets[0], function (text) {
var result = `Hash: ${text}`;
complete(result);
}, 'base64');
});
});";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
@ -227,18 +265,20 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_not_resolve_asset_text_if_too_big()
public async Task Should_not_resolve_blue_hash_if_not_an_image()
{
var (vars, _) = SetupAssetVars(1_000_000);
var (vars, assets) = SetupAssetsVars(1, type: AssetType.Audio);
SetupBlurHash(assets[0].ToRef(), "Hash");
var expected = @"
Text: ErrorTooBig
Hash: null
";
var script = @"
getAssets(data.assets.iv, function (assets) {
getAssetText(assets[0], function (text) {
var result = `Text: ${text}`;
getAssetBlurHash(assets[0], function (text) {
var result = `Hash: ${text}`;
complete(result);
});
@ -247,55 +287,21 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_resolve_asset_text_from_event()
public async Task Should_resolve_blur_hash_from_event()
{
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
AssetType = AssetType.Image,
FileVersion = 0,
FileSize = 100,
AppId = appId
};
SetupText(@event.Id, Encoding.UTF8.GetBytes("Hello Asset"));
var vars = new ScriptVars
{
["event"] = @event
};
var expected = @"
Text: Hello Asset
";
var script = @"
getAssetText(event, function (text) {
var result = `Text: ${text}`;
complete(result);
});";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_not_resolve_asset_text_from_event_if_too_big()
{
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
FileVersion = 0,
FileSize = 1_000_000,
AppId = appId
};
SetupBlurHash(@event.ToRef(), "Hash");
var vars = new ScriptVars
{
@ -303,11 +309,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
};
var expected = @"
Text: ErrorTooBig
Text: Hash
";
var script = @"
getAssetText(event, function (text) {
getAssetBlurHash(event, function (text) {
var result = `Text: ${text}`;
complete(result);
@ -316,56 +322,24 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
private void SetupText(DomainId id, byte[] bytes)
private void SetupBlurHash(AssetRef asset, string hash)
{
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.Invokes(x =>
{
var stream = x.GetArgument<Stream>(4)!;
stream.Write(bytes);
});
A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._))
.Returns(hash);
}
private (ScriptVars, IAssetEntity) SetupAssetVars(int fileSize = 100)
private void SetupText(AssetRef asset, byte[] bytes)
{
var assetId = DomainId.NewGuid();
var asset = CreateAsset(assetId, 1, fileSize);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddInvariant(JsonValue.Array(assetId)));
A.CallTo(() => assetQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, asset));
var vars = new ScriptVars
{
["data"] = data,
["appId"] = appId.Id,
["appName"] = appId.Name,
["user"] = user
};
return (vars, asset);
A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._))
.Invokes(x => x.GetArgument<Stream>(4)?.Write(bytes));
}
private (ScriptVars, IAssetEntity[]) SetupAssetsVars(int fileSize = 100)
private (ScriptVars, IAssetEntity[]) SetupAssetsVars(int count, int fileSize = 100, AssetType type = AssetType.Image)
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1, fileSize);
var assetId2 = DomainId.NewGuid();
var asset2 = CreateAsset(assetId1, 2, fileSize);
var assets = Enumerable.Range(0, count).Select(x => CreateAsset(1, fileSize, type)).ToArray();
var assetIds = assets.Select(x => x.Id);
var user = new ClaimsPrincipal();
@ -373,11 +347,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddInvariant(JsonValue.Array(assetId1, assetId2)));
.AddInvariant(JsonValue.Array(assetIds)));
A.CallTo(() => assetQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1, assetId2), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, asset1, asset2));
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetIds), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, assets));
var vars = new ScriptVars
{
@ -387,17 +361,19 @@ namespace Squidex.Domain.Apps.Entities.Assets
["user"] = user
};
return (vars, new[] { asset1, asset2 });
return (vars, assets);
}
private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100)
private IEnrichedAssetEntity CreateAsset(int index, int fileSize = 100, AssetType type = AssetType.Image)
{
return new AssetEntity
{
AppId = appId,
Id = assetId,
Id = DomainId.NewGuid(),
FileSize = fileSize,
FileName = $"file{index}.jpg",
MimeType = "image/jpg",
Type = type
};
}

54
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs

@ -61,6 +61,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
Assert.Equal("3", result);
}
[Fact]
public async Task Should_reset_counter_with_callback()
{
var appId = DomainId.NewGuid();
A.CallTo(() => grainFactory.GetGrain<ICounterGrain>(appId.ToString(), null))
.Returns(counter);
A.CallTo(() => counter.ResetAsync("my", 4))
.Returns(3);
const string script = @"
resetCounterV2('my', function(result) {
complete(result);
}, 4);
";
var vars = new ScriptVars
{
["appId"] = appId
};
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal("3", result);
}
[Fact]
public void Should_increment_counter()
{
@ -85,5 +112,32 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
Assert.Equal("3", result);
}
[Fact]
public async Task Should_increment_counter_with_callback()
{
var appId = DomainId.NewGuid();
A.CallTo(() => grainFactory.GetGrain<ICounterGrain>(appId.ToString(), null))
.Returns(counter);
A.CallTo(() => counter.IncrementAsync("my"))
.Returns(3);
const string script = @"
incrementCounter('my', function (result) {
complete(result);
});
";
var vars = new ScriptVars
{
["appId"] = appId
};
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal("3", result);
}
}
}

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

@ -55,28 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[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()
.AddInvariant(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), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(1, reference1));
var vars = new ScriptVars
{
["appId"] = appId.Id,
["data"] = data,
["dataOld"] = null,
["user"] = user
};
var (vars, _) = SetupReferenceVars(1);
var expected = @"
Text: Hello 1 World 1
@ -97,10 +76,30 @@ namespace Squidex.Domain.Apps.Entities.Contents
[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 (vars, _) = SetupReferenceVars(2);
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
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 result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private (ScriptVars, IContentEntity[]) SetupReferenceVars(int count)
{
var references = Enumerable.Range(0, count).Select((x, i) => CreateReference(i + 1)).ToArray();
var referenceIds = references.Select(x => x.Id);
var user = new ClaimsPrincipal();
@ -108,11 +107,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
new ContentData()
.AddField("references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(referenceId1, referenceId2)));
.AddInvariant(JsonValue.Array(referenceIds)));
A.CallTo(() => contentQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), A<Q>.That.HasIds(referenceId1, referenceId2), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, reference1, reference2));
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), A<Q>.That.HasIds(referenceIds), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, references));
var vars = new ScriptVars
{
@ -122,25 +121,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
["user"] = user
};
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
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 result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
return (vars, references);
}
private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index)
private static IEnrichedContentEntity CreateReference(int index)
{
return new ContentEntity
{
@ -152,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
.AddField("field2",
new ContentFieldData()
.AddInvariant(JsonValue.Create($"World {index}"))),
Id = referenceId
Id = DomainId.NewGuid()
};
}

Loading…
Cancel
Save