diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index e977c6009..98b0ed6fd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/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); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs index 2309f2149..5732c9b7f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs +++ b/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 callback, JsValue? headers) + private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action 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 ParseResponse(ScriptExecutionContext context, HttpResponseMessage response) + private static async Task 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; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs new file mode 100644 index 000000000..1a039c9f3 --- /dev/null +++ b/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 ToIds(this JsValue? value) + { + var ids = new List(); + + 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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 4c418bd05..b827e29bc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/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(); + var context = + CreateEngine(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(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(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(); + var context = + CreateEngine(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(_ => { - 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(_ => - { - 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(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 CreateEngine(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(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); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs index a5ec4889c..0e950e0fb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs +++ b/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? completion; - public Engine Engine { get; } - public CancellationToken CancellationToken { get; private set; } + protected ScriptExecutionContext(Engine engine) + { + Engine = engine; + } + + public abstract void Schedule(Func 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(Action? action, T argument); + } - public bool IsAsync { get; private set; } + public sealed class ScriptExecutionContext : ScriptExecutionContext, IScheduler where T : class + { + private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + 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 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 extensions, Func completion, - CancellationToken ct) + public override void Schedule(Func 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 ExtendAsync(IEnumerable 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 extensions) + public ScriptExecutionContext Extend(IEnumerable 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 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(Action? action, TArg argument) + { + if (IsCompleted || action == null) + { + return; + } + + Engine.ResetConstraints(); + action(argument); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppTag.cs b/backend/src/Squidex.Domain.Apps.Entities/AppTag.cs deleted file mode 100644 index cad116cfa..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/AppTag.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs index 93952dc91..44feae209 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs +++ b/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 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); - } - } - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs index 92d6ff48c..85b82e08e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs +++ b/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(); + this.serviceProvider = serviceProvider; } - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + public override async ValueTask 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(); - - var assetQuery = serviceProvider.GetRequiredService(); - 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(); - 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 ResolveAssetText(DomainId appId, DomainId id, long fileSize, long fileVersion) + async Task ResolveAssetTextAsync(AssetRef asset) { - if (fileSize > 256_000) + if (asset.FileSize > 256_000) { return ErrorTooBig; } + var assetFileStore = serviceProvider.GetRequiredService(); + 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 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(); + var assetThumbnailGenerator = serviceProvider.GetRequiredService(); + + 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 ResolveAssetAsync(IAppProvider appProvider, IAssetQueryService assetQuery, DomainId appId, FluidValue id) + private static async Task ResolveAssetAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) { + var appProvider = serviceProvider.GetRequiredService(); + 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(); + var requestContext = Context.Admin(app).Clone(b => b .WithoutTotal()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index 6df6b5bd7..afdcf6730 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/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 callback); - private delegate void GetAssetTextDelegate(JsValue references, Action callback, JsValue encoding); + private delegate void GetAssetTextDelegate(JsValue asset, Action callback, JsValue? encoding); + private delegate void GetBlurHashDelegate(JsValue asset, Action 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 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 callback, JsValue encoding) + private void GetText(ScriptExecutionContext context, JsValue input, Action 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(); - - 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(); + 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 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 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(); + var assetFileStore = serviceProvider.GetRequiredService(); + 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 callback) + private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) { Guard.NotNull(callback); - var ids = new List(); - - 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(); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets)); + return; } - } - if (ids.Count == 0) - { - var emptyAssets = Array.Empty(); + var app = await GetAppAsync(appId, ct); - callback(JsValue.FromObject(context.Engine, emptyAssets)); - return; - } + if (app == null) + { + var emptyAssets = Array.Empty(); - context.MarkAsync(); + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets)); + return; + } - try - { - var app = await GetAppAsync(appId); + var assetQuery = serviceProvider.GetRequiredService(); var requestContext = new Context(user, app).Clone(b => b .WithoutTotal()); - var assetQuery = serviceProvider.GetRequiredService(); - 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 GetAppAsync(DomainId appId) + private async Task GetAppAsync(DomainId appId, + CancellationToken ct) { var appProvider = serviceProvider.GetRequiredService(); - var app = await appProvider.GetAppAsync(appId); + var app = await appProvider.GetAppAsync(appId, false, ct); if (app == null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs new file mode 100644 index 000000000..187e3c8ee --- /dev/null +++ b/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 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 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); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index 4769f67eb..e0d6f158f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/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? 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("appId", out var appId)) + if (!context.TryGetValue("appId", out var appId)) { - var engine = context.Engine; + return; + } - engine.SetValue("incrementCounter", new Func(name => - { - return Increment(appId, name); - })); + var increment = new Func(name => + { + return Increment(appId, name); + }); - engine.SetValue("resetCounter", new Func((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("appId", out var appId)) + { + return; } + + var increment = new Action>((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 callback) + { + context.Schedule(async (scheduler, ct) => + { + var grain = grainFactory.GetGrain(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(appId.ToString()); return AsyncHelper.Sync(() => grain.ResetAsync(name, value)); } + + private void ResetV2(ScriptExecutionContext context, DomainId appId, string name, Action? callback, long value) + { + context.Schedule(async (scheduler, ct) => + { + var grain = grainFactory.GetGrain(appId.ToString()); + + var result = await grain.ResetAsync(name, value); + + if (callback != null) + { + scheduler.Run(callback, JsValue.FromObject(context.Engine, result)); + } + }); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index 519805826..c6e74be1c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/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(); + this.serviceProvider = serviceProvider; } - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + public override async ValueTask 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(); - - var contentQuery = serviceProvider.GetRequiredService(); - 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 ResolveContentAsync(IAppProvider appProvider, IContentQueryService contentQuery, DomainId appId, FluidValue id) + private static async Task ResolveContentAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) { + var appProvider = serviceProvider.GetRequiredService(); + 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 }; + + var contentQuery = serviceProvider.GetRequiredService(); 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(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs index b8a9bb798..fae5e94c8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs +++ b/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 callback) - { - GetReferencesAsync(context, appId, user, references, callback).Forget(); - } - - private async Task GetReferencesAsync(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) { Guard.NotNull(callback); - var ids = new List(); - - 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(); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents)); + return; } - } - if (ids.Count == 0) - { - var emptyContents = Array.Empty(); + var app = await GetAppAsync(appId); - callback(JsValue.FromObject(context.Engine, emptyContents)); - return; - } + if (app == null) + { + var emptyContents = Array.Empty(); - context.MarkAsync(); + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents)); + return; + } - try - { - var app = await GetAppAsync(appId); + var contentQuery = serviceProvider.GetRequiredService(); 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(); - - 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 GetAppAsync(DomainId appId) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs index 2fd2f8af4..5264ca68e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Properties { } /// - /// 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.. /// internal static string ScriptingGetAssets { get { @@ -87,6 +87,15 @@ namespace Squidex.Domain.Apps.Entities.Properties { } } + /// + /// Looks up a localized string similar to Gets the blur hash of an asset if it is an image or null otherwise.. + /// + internal static string ScriptingGetBlurHash { + get { + return ResourceManager.GetString("ScriptingGetBlurHash", resourceCulture); + } + } + /// /// Looks up a localized string similar to Queries the content item with the specified ID and invokes the callback with an array of contents.. /// @@ -106,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Properties { } /// - /// 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).. /// internal static string ScriptingIncrementCounter { get { @@ -115,12 +124,30 @@ namespace Squidex.Domain.Apps.Entities.Properties { } /// - /// 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.. + /// + internal static string ScriptingIncrementCounterV2 { + get { + return ResourceManager.GetString("ScriptingIncrementCounterV2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resets the counter with the given name to zero (OBSOLETE).. /// internal static string ScriptingResetCounter { get { return ResourceManager.GetString("ScriptingResetCounter", resourceCulture); } } + + /// + /// Looks up a localized string similar to Resets the counter with the given name to zero.. + /// + internal static string ScriptingResetCounterV2 { + get { + return ResourceManager.GetString("ScriptingResetCounterV2", resourceCulture); + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx index 4d8fb7afd..b348454f4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx +++ b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx @@ -121,11 +121,14 @@ Queries the asset with the specified ID and invokes the callback with an array of assets. - ueries the assets with the specified IDs and invokes the callback with an array of assets. + Queries the assets with the specified IDs and invokes the callback with an array of assets. Get the text of an asset. Encodings: base64,ascii,unicode,utf8 + + Gets the blur hash of an asset if it is an image or null otherwise. + Queries the content item with the specified ID and invokes the callback with an array of contents. @@ -133,9 +136,15 @@ Queries the content items with the specified IDs and invokes the callback with an array of contents. + Increments the counter with the given name and returns the value (OBSOLETE). + + Increments the counter with the given name and returns the value. + Resets the counter with the given name to zero (OBSOLETE). + + Resets the counter with the given name to zero. \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index c41301908..35f849641 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -31,7 +31,7 @@ - + diff --git a/backend/src/Squidex.Web/ETagExtensions.cs b/backend/src/Squidex.Web/ETagExtensions.cs index 8c6811e5b..061c79bb6 100644 --- a/backend/src/Squidex.Web/ETagExtensions.cs +++ b/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; diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 8214c7407..154da2fc2 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -74,16 +74,16 @@ - - - - - - - - + + + + + + + + - + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index ade5b240e..b0789dd57 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/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(() => sut.TransformAsync(vars, script, contentOptions)); + await sut.TransformAsync(vars, script, contentOptions); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index 6f1fdf438..7688aece7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/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(); - private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); + private readonly IAssetFileStore assetFileStore = A.Fake(); + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly NamedId 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 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._, A._, A._, null, A._, A._, A._)) + .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._, A._, A._, null, A._, A._, A._)) + .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._, A._, A._, null, A._, A._, A._)) - .MustNotHaveHappened(); + A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A._, asset.MimeType, A._, A._)) + .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._, A._, A._)) - .Invokes(x => - { - var stream = x.GetArgument(4)!; - - stream.Write(bytes); - }); + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A._, A._, A._)) + .Invokes(x => x.GetArgument(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._, assetId, EtagVersion.Any, A._)) .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 }; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index fb3692000..e8136491d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/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 { - private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); + private readonly IAssetFileStore assetFileStore = A.Fake(); + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly NamedId 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._)) .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 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._, A._, A._, null, A._, A._, A._)) + .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._, A._, A._, null, A._, A._, A._)) - .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._, A._, A._, null, A._, A._, A._)) - .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._, A._, A._)) - .Invokes(x => - { - var stream = x.GetArgument(4)!; - - stream.Write(bytes); - }); + A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A._, asset.MimeType, A._, A._)) + .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.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId), A._)) - .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._, A._, A._)) + .Invokes(x => x.GetArgument(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.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId1, assetId2), A._)) - .Returns(ResultList.CreateFrom(2, asset1, asset2)); + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetIds), A._)) + .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 }; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs index 0bcf4b9e4..5cc226e00 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs +++ b/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(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(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); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs index ab5143cbc..b056e806b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs +++ b/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.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceId1), A._)) - .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.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceId1, referenceId2), A._)) - .Returns(ResultList.CreateFrom(2, reference1, reference2)); + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceIds), A._)) + .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() }; }