Browse Source

More progress

pull/1330/head
Sebastian Stehle 2 weeks ago
parent
commit
6373c0547d
  1. 17
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  2. 28
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EngineExtensions.cs
  3. 119
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  4. 59
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs
  6. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs
  7. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  8. 171
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  9. 198
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  10. 50
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsJintExtension.cs
  11. 47
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  12. 79
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  13. 25
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
@ -65,11 +66,13 @@ public sealed class EventJintExtension(IUrlGenerator urlGenerator) : IJintExtens
}
}
public void Extend(ScriptExecutionContext context)
public void Extend(Engine engine)
{
context.Engine.SetValue("console", FlowConsoleWrapper.Instance);
var context = engine.GetContext();
context.Engine.SetValue("contentAction", new EventDelegate(() =>
engine.SetValue("console", FlowConsoleWrapper.Instance);
engine.SetValue("contentAction", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent)
{
@ -79,7 +82,7 @@ public sealed class EventJintExtension(IUrlGenerator urlGenerator) : IJintExtens
return JsValue.Null;
}));
context.Engine.SetValue("contentUrl", new EventDelegate(() =>
engine.SetValue("contentUrl", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent)
{
@ -89,7 +92,7 @@ public sealed class EventJintExtension(IUrlGenerator urlGenerator) : IJintExtens
return JsValue.Null;
}));
context.Engine.SetValue("assetContentSlugUrl", new EventDelegate(() =>
engine.SetValue("assetContentSlugUrl", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent)
{
@ -109,8 +112,8 @@ public sealed class EventJintExtension(IUrlGenerator urlGenerator) : IJintExtens
return JsValue.Null;
});
context.Engine.SetValue("assetContentUrl", assetUrl);
context.Engine.SetValue("assetContentAppUrl", assetUrl);
engine.SetValue("assetContentUrl", assetUrl);
engine.SetValue("assetContentAppUrl", assetUrl);
}
public void Describe(AddDescription describe, ScriptScope scope)

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

@ -0,0 +1,28 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
namespace Squidex.Domain.Apps.Core.Scripting;
public static class EngineExtensions
{
public static ScriptExecutionContext GetContext(this Engine engine)
{
return ScriptExecutionContext.GetContext(engine);
}
public static void Schedule(this Engine engine, Func<CancellationToken, Task> action)
{
ScriptExecutionContext.GetContext(engine).Schedule(action);
}
public static void Schedule<TResult>(this Engine engine, Func<CancellationToken, Task<TResult>> action, Action<TResult>? callback)
{
ScriptExecutionContext.GetContext(engine).Schedule(action, callback);
}
}

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

@ -21,48 +21,48 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
private delegate void HttpJsonWithBodyDelegate(string url, JsValue body, Action<JsValue> callback, JsValue? headers = null, bool ignoreError = false);
private delegate void HttpRequestDelegate(JsValue requestInit, Action<JsValue> callback);
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
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");
AddMethod(context, "request");
AddBodyMethod(engine, HttpMethod.Patch, "patchJSON");
AddBodyMethod(engine, HttpMethod.Post, "postJSON");
AddBodyMethod(engine, HttpMethod.Put, "putJSON");
AddMethod(engine, HttpMethod.Delete, "deleteJSON");
AddMethod(engine, HttpMethod.Get, "getJSON");
AddMethod(engine, "request");
}
private void AddMethod(ScriptExecutionContext context, string name)
private void AddMethod(Engine engine, string name)
{
var action = new HttpRequestDelegate((requestInit, callback) =>
{
var httpRequest = ParseRequestInit(requestInit);
Request(context, httpRequest.Method, httpRequest.Url, httpRequest.Body, callback, httpRequest.Headers, true, true);
var (url, method, headers, body) = ParseRequestInit(requestInit);
Request(engine, method, url, body, callback, headers, true, true);
});
context.Engine.SetValue(name, action);
engine.SetValue(name, action);
}
private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name)
private void AddMethod(Engine engine, HttpMethod method, string name)
{
var action = new HttpJsonDelegate((url, callback, headers, ignoreError) =>
{
Request(context, method, url, null, callback, headers, ignoreError);
Request(engine, method, url, null, callback, headers, ignoreError);
});
context.Engine.SetValue(name, action);
engine.SetValue(name, action);
}
private void AddBodyMethod(ScriptExecutionContext context, HttpMethod method, string name)
private void AddBodyMethod(Engine engine, HttpMethod method, string name)
{
var action = new HttpJsonWithBodyDelegate((url, body, callback, headers, ignoreError) =>
{
Request(context, method, url, body, callback, headers, ignoreError);
Request(engine, method, url, body, callback, headers, ignoreError);
});
context.Engine.SetValue(name, action);
engine.SetValue(name, action);
}
private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action<JsValue> callback, JsValue? headers, bool ignoreError, bool forceRawResponse = false)
private void Request(Engine engine, HttpMethod method, string url, JsValue? body, Action<JsValue> callback, JsValue? headers, bool ignoreError, bool forceRawResponse = false)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
@ -74,53 +74,67 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
// The request reads javascript values and is therefore created while we are still inside the engine.
var request = CreateRequest(engine, method, uri, body, headers);
engine.Schedule(async ct =>
{
try
{
var httpClient = httpClientFactory.CreateClient("Jint");
var request = CreateRequest(context, method, uri, body, headers);
var response = await httpClient.SendAsync(request, ct);
if (!ignoreError)
using (request)
{
response.EnsureSuccessStatusCode();
}
var httpClient = httpClientFactory.CreateClient("Jint");
JsValue responseObject;
using var response = await httpClient.SendAsync(request, ct);
var responseString = await response.Content.ReadAsStringAsync(ct);
if (ignoreError && (forceRawResponse || !response.IsSuccessStatusCode || string.IsNullOrEmpty(responseString)))
{
responseObject = JsValue.FromObject(context.Engine, new Dictionary<string, object?>
if (!ignoreError)
{
["statusCode"] = (int)response.StatusCode,
["headers"] =
response.EnsureSuccessStatusCode();
}
var responseString = await response.Content.ReadAsStringAsync(ct);
return (
StatusCode: (int)response.StatusCode,
Headers:
response.Content.Headers
.Concat(response.Headers)
.Concat(response.TrailingHeaders)
.GroupBy(x => x.Key)
.ToDictionary(x => x.Key, x => x.Last().Value.First()),
["body"] = responseString,
});
}
else
{
responseObject = ParseResponse(context, responseString, ct);
Body: responseString,
IsRaw: ignoreError && (forceRawResponse || !response.IsSuccessStatusCode || string.IsNullOrEmpty(responseString))
);
}
scheduler.Run(callback, responseObject);
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
throw new JavaScriptException(ex.Message);
}
},
response =>
{
JsValue responseObject;
if (response.IsRaw)
{
responseObject = JsValue.FromObject(engine, new Dictionary<string, object?>
{
["statusCode"] = response.StatusCode,
["headers"] = response.Headers,
["body"] = response.Body,
});
}
else
{
responseObject = new JsonParser(engine).Parse(response.Body);
}
callback(responseObject);
});
}
private static HttpRequestMessage CreateRequest(ScriptExecutionContext context,
private static HttpRequestMessage CreateRequest(Engine engine,
HttpMethod method,
Uri uri,
JsValue? body,
@ -166,7 +180,7 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
}
else
{
var jsonWriter = new JsonSerializer(context.Engine);
var jsonWriter = new JsonSerializer(engine);
var jsonContent = jsonWriter.Serialize(body, JsValue.Undefined, JsValue.Undefined)?.ToString();
if (jsonContent != null)
@ -179,19 +193,6 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
return request;
}
private static JsValue ParseResponse(ScriptExecutionContext context, string responseString,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var jsonParser = new JsonParser(context.Engine);
var jsonValue = jsonParser.Parse(responseString);
ct.ThrowIfCancellationRequested();
return jsonValue;
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))

59
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Jint.Runtime;
using Squidex.AI;
@ -20,39 +21,40 @@ public sealed class StringAsyncJintExtension(ITranslator translator, IChatAgent
private delegate void TextGenerateDelegate(string prompt, Action<JsValue> callback);
private delegate void TextTranslateDelegate(string text, string language, Action<JsValue> callback, string sourceLanguage);
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
var generate = new TextGenerateDelegate((prompt, callback) =>
{
Generate(context, prompt, callback);
Generate(engine, prompt, callback);
});
var translate = new TextTranslateDelegate((text, language, callback, sourceLanguage) =>
{
Translate(context, text, language, callback, sourceLanguage);
Translate(engine, text, language, callback, sourceLanguage);
});
context.Engine.SetValue("generate", generate);
context.Engine.SetValue("translate", translate);
engine.SetValue("generate", generate);
engine.SetValue("translate", translate);
}
private void Generate(ScriptExecutionContext context, string prompt, Action<JsValue> callback)
private void Generate(Engine engine, string prompt, Action<JsValue> callback)
{
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
// We are still inside the engine here, therefore the callback can be invoked directly.
if (string.IsNullOrWhiteSpace(prompt))
{
callback(JsValue.Null);
return;
}
engine.Schedule(async ct =>
{
try
{
if (string.IsNullOrWhiteSpace(prompt))
{
scheduler.Run(callback, JsValue.Null);
return;
}
var request = new ChatRequest
{
Prompt = prompt,
@ -60,41 +62,44 @@ public sealed class StringAsyncJintExtension(ITranslator translator, IChatAgent
var result = await chatAgent.PromptAsync(request, ct: ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, result.Content));
return result.Content;
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
throw new JavaScriptException(ex.Message);
}
});
},
content => callback(JsValue.FromObject(engine, content)));
}
private void Translate(ScriptExecutionContext context, string text, string language, Action<JsValue> callback, string sourceLanguage)
private void Translate(Engine engine, string text, string language, Action<JsValue> callback, string sourceLanguage)
{
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
// We are still inside the engine here, therefore the callback can be invoked directly.
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(language))
{
callback(JsValue.Null);
return;
}
engine.Schedule(async ct =>
{
try
{
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(language))
{
scheduler.Run(callback, JsValue.Null);
return;
}
var translation = await translator.TranslateAsync(text, language, sourceLanguage, ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, translation.Text));
return translation.Text;
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
throw new JavaScriptException(ex.Message);
}
});
},
translated => callback(JsValue.FromObject(engine, translated)));
}
public void Describe(AddDescription describe, ScriptScope scope)

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

@ -15,11 +15,7 @@ public interface IJintExtension
{
}
void Extend(ScriptExecutionContext context)
{
}
void ExtendAsync(ScriptExecutionContext context)
void ExtendAsync(Engine engine)
{
}
}

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

@ -37,7 +37,7 @@ public static class JintExtensions
{
foreach (var extension in extensions)
{
extension.ExtendAsync(context);
extension.ExtendAsync(context.Engine);
}
return context;
@ -48,7 +48,7 @@ public static class JintExtensions
{
foreach (var extension in extensions)
{
extension.Extend(context);
extension.Extend(context.Engine);
}
return context;

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

@ -62,6 +62,11 @@ public sealed class JintScriptEngine(IMemoryCache cache, IOptions<JintScriptOpti
{
throw MapException(ex);
}
finally
{
// Stop pending tasks before the token source is disposed, they must not touch the engine anymore.
await combined.CancelAsync();
}
}
public async Task<ContentData> TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default,
@ -110,6 +115,11 @@ public sealed class JintScriptEngine(IMemoryCache cache, IOptions<JintScriptOpti
{
throw MapException(ex);
}
finally
{
// Stop pending tasks before the token source is disposed, they must not touch the engine anymore.
await combined.CancelAsync();
}
}
public JsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default)
@ -167,11 +177,6 @@ public sealed class JintScriptEngine(IMemoryCache cache, IOptions<JintScriptOpti
engine.AddReject();
}
foreach (var extension in extensions)
{
extension.Extend(engine);
}
return new ScriptExecutionContext<T>(engine, ct);
}

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

@ -5,27 +5,54 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Runtime.CompilerServices;
using Acornima.Ast;
using Jint;
using Jint.Native;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.Scripting;
public abstract class ScriptExecutionContext(Engine engine) : ScriptVars
public abstract class ScriptExecutionContext : ScriptVars
{
public Engine Engine { get; } = engine;
private static readonly ConditionalWeakTable<Engine, ScriptExecutionContext> Contexts = new ConditionalWeakTable<Engine, ScriptExecutionContext>();
public Engine Engine { get; }
protected ScriptExecutionContext(Engine engine)
{
Engine = engine;
// The extensions only get the engine and resolve the context from there.
Contexts.AddOrUpdate(engine, this);
}
public static ScriptExecutionContext GetContext(Engine engine)
{
if (!Contexts.TryGetValue(engine, out var context))
{
ThrowHelper.InvalidOperationException("Engine is not attached to a script context.");
return default!;
}
return context;
}
public abstract JsValue Evaluate(Prepared<Script> script);
public abstract Task<JsValue> EvaluateAsync(Prepared<Script> script);
public abstract void Schedule(Func<IScheduler, CancellationToken, Task> action);
public abstract void Schedule(Func<CancellationToken, Task> action);
public abstract void Schedule<TResult>(Func<CancellationToken, Task<TResult>> action, Action<TResult>? callback);
}
public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, IScheduler
public sealed class ScriptExecutionContext<T> : ScriptExecutionContext
{
private readonly TaskCompletionSource<CompletedValue?> tcs = new TaskCompletionSource<CompletedValue?>();
private readonly TaskCompletionSource<CompletedValue?> tcs = new TaskCompletionSource<CompletedValue?>(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly SemaphoreSlim engineLock = new SemaphoreSlim(1);
private readonly CancellationTokenRegistration cancellationRegistration;
private readonly CancellationToken cancellationToken;
private int pendingTasks = 1;
@ -34,28 +61,55 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
public T Value { get; init; }
}
private readonly struct Releaser(SemaphoreSlim semaphore) : IDisposable
{
public void Dispose()
{
semaphore.Release();
}
}
public bool IsCompleted
{
get => tcs.Task.Status is TaskStatus.RanToCompletion or TaskStatus.Faulted;
get => tcs.Task.IsCompleted;
}
internal ScriptExecutionContext(Engine engine, CancellationToken cancellationToken)
: base(engine)
{
this.cancellationToken = cancellationToken;
// Settle the source on cancellation, so that pending callbacks do not enter the engine anymore.
cancellationRegistration = cancellationToken.Register(static state =>
{
var self = (ScriptExecutionContext<T>)state!;
self.tcs.TrySetCanceled(self.cancellationToken);
},
this);
}
public async Task<T> WaitForCompletionAsync(Func<T> fallback)
{
TryComplete();
try
{
var result = await tcs.Task;
if (result != null)
{
return result.Value;
}
var result = await tcs.Task.WithCancellation(cancellationToken);
if (result == null)
// The fallback converts javascript values and therefore needs exclusive access to the engine.
using (await LockEngineAsync())
{
return fallback();
}
}
finally
{
return fallback();
await cancellationRegistration.DisposeAsync();
}
return result.Value;
}
public void Complete(T value)
@ -65,15 +119,36 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
public override JsValue Evaluate(Prepared<Script> script)
{
// The synchronous path cannot schedule tasks, therefore nothing else can enter the engine.
return Engine.Evaluate(script);
}
public override Task<JsValue> EvaluateAsync(Prepared<Script> script)
{
// The lock cannot be taken here, otherwise we would deadlock.
return Engine.EvaluateAsync(script, cancellationToken);
}
public override void Schedule(Func<IScheduler, CancellationToken, Task> action)
public override void Schedule(Func<CancellationToken, Task> action)
{
ScheduleCoreAsync(CallWithDummyResult(action), null);
}
public override void Schedule<TResult>(Func<CancellationToken, Task<TResult>> action, Action<TResult>? callback)
{
ScheduleCoreAsync(action, callback);
}
private static Func<CancellationToken, Task<bool>> CallWithDummyResult(Func<CancellationToken, Task> action)
{
return async ct =>
{
await action(ct);
return true;
};
}
private void ScheduleCoreAsync<TResult>(Func<CancellationToken, Task<TResult>> action, Action<TResult>? callback)
{
if (IsCompleted)
{
@ -85,7 +160,21 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
TryStart();
try
{
await action(this, cancellationToken);
// The action must not touch the engine, so that parallel tasks do not block each other.
var result = await action(cancellationToken);
// The callback converts javascript values and is therefore the only part that needs the lock.
using (await LockEngineAsync())
{
if (!IsCompleted)
{
// The task can take a while, therefore the callback gets a fresh timeout.
Engine.Constraints.Reset();
callback?.Invoke(result);
}
}
TryComplete();
}
catch (Exception ex)
@ -97,52 +186,11 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
ScheduleAsync().Forget();
}
void IScheduler.Run(Action? action)
private async Task<Releaser> LockEngineAsync()
{
if (IsCompleted || action == null)
{
return;
}
TryStart();
try
{
lock (Engine)
{
Engine.Constraints.Reset();
action();
}
await engineLock.WaitAsync(cancellationToken);
TryComplete();
}
catch (Exception ex)
{
TryFail(ex);
}
}
void IScheduler.Run<TArg>(Action<TArg>? action, TArg argument)
{
if (IsCompleted || action == null)
{
return;
}
TryStart();
try
{
lock (Engine)
{
Engine.Constraints.Reset();
action(argument);
}
TryComplete(default!);
}
catch (Exception ex)
{
TryFail(ex);
}
return new Releaser(engineLock);
}
private void TryFail(Exception exception)
@ -163,12 +211,3 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
}
}
}
#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);
}

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

@ -32,46 +32,47 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
private delegate void GetAssetTextDelegate(JsValue asset, Action<JsValue> callback, JsValue? encoding);
private delegate void GetBlurHashDelegate(JsValue asset, Action<JsValue> callback, JsValue? componentX, JsValue? componentY);
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
AddGetAssetText(context);
AddGetAssetBlurHash(context);
AddGetAssetObject(context);
AddUpdateAsset(context);
AddGetAssetText(engine);
AddGetAssetBlurHash(engine);
AddGetAssetObject(engine);
AddUpdateAsset(engine);
}
private void AddUpdateAsset(ScriptExecutionContext context)
private void AddUpdateAsset(Engine engine)
{
if (!context.TryGetValueIfExists<ClaimsPrincipal>("user", out var user))
if (!engine.GetContext().TryGetValueIfExists<ClaimsPrincipal>("user", out var user))
{
return;
}
var updateAsset = new UpdateAssetDelegate((asset, metadata) =>
{
UpdateAsset(context, user, asset, metadata);
UpdateAsset(engine, user, asset, metadata);
});
context.Engine.SetValue("updateAsset", updateAsset);
engine.SetValue("updateAsset", updateAsset);
}
private void UpdateAsset(ScriptExecutionContext context, ClaimsPrincipal user, JsValue input, JsValue metadata)
private void UpdateAsset(Engine engine, ClaimsPrincipal user, JsValue input, JsValue metadata)
{
context.Schedule(async (scheduler, ct) =>
// The javascript values are read while we are still inside the engine.
if (!TryGetAssetRef(engine, input, out var asset) || metadata is not ObjectInstance metadataObj)
{
if (!TryGetAssetRef(context, input, out var asset) || metadata is not ObjectInstance metadataObj)
{
return;
}
return;
}
var commandBus = serviceProvider.GetRequiredService<ICommandBus>();
var assetMetadata = new AssetMetadata();
var assetMetadata = new AssetMetadata();
foreach (var (key, value) in metadataObj.GetOwnProperties())
{
assetMetadata[key.AsString()] = JsonMapper.Map(value.Value);
}
foreach (var (key, value) in metadataObj.GetOwnProperties())
{
assetMetadata[key.AsString()] = JsonMapper.Map(value.Value);
}
engine.Schedule(async ct =>
{
var commandBus = serviceProvider.GetRequiredService<ICommandBus>();
var command = new AnnotateAsset
{
@ -87,8 +88,10 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
});
}
private void AddGetAssetObject(ScriptExecutionContext context)
private void AddGetAssetObject(Engine engine)
{
var context = engine.GetContext();
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
@ -101,40 +104,40 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
var getAssets = new GetAssetsDelegate((references, callback) =>
{
GetAssets(context, appId, user, references, callback);
GetAssets(engine, appId, user, references, callback);
});
var getAsset = new GetAssetsDelegate((references, callback) =>
{
GetAsset(context, appId, user, references, callback);
GetAsset(engine, appId, user, references, callback);
});
context.Engine.SetValue("getAsset", getAssets);
context.Engine.SetValue("getAssetV2", getAsset);
context.Engine.SetValue("getAssets", getAssets);
engine.SetValue("getAsset", getAssets);
engine.SetValue("getAssetV2", getAsset);
engine.SetValue("getAssets", getAssets);
}
private void AddGetAssetText(ScriptExecutionContext context)
private void AddGetAssetText(Engine engine)
{
var action = new GetAssetTextDelegate((references, callback, encoding) =>
{
GetText(context, references, callback, encoding);
GetText(engine, references, callback, encoding);
});
context.Engine.SetValue("getAssetText", action);
engine.SetValue("getAssetText", action);
}
private void AddGetAssetBlurHash(ScriptExecutionContext context)
private void AddGetAssetBlurHash(Engine engine)
{
var getBlurHash = new GetBlurHashDelegate((input, callback, componentX, componentY) =>
{
GetBlurHash(context, input, callback, componentX, componentY);
GetBlurHash(engine, input, callback, componentX, componentY);
});
context.Engine.SetValue("getAssetBlurHash", getBlurHash);
engine.SetValue("getAssetBlurHash", getBlurHash);
}
private void GetText(ScriptExecutionContext context,
private void GetText(Engine engine,
JsValue input, Action<JsValue> callback, JsValue? encoding)
{
if (callback == null)
@ -142,23 +145,26 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
// The javascript values are read while we are still inside the engine.
TryGetAssetRef(engine, input, out var asset);
var encodingName = encoding?.ToString();
engine.Schedule(async ct =>
{
TryGetAssetRef(context, input, out var asset);
try
{
var text = await asset.GetTextAsync(encoding?.ToString(), serviceProvider, ct);
scheduler.Run(callback, text);
return await asset.GetTextAsync(encodingName, serviceProvider, ct);
}
catch
catch (Exception ex) when (ex is not OperationCanceledException)
{
scheduler.Run(callback, JsValue.Null);
return null;
}
});
},
text => callback(JsValue.FromObject(engine, text)));
}
private void GetBlurHash(ScriptExecutionContext context,
private void GetBlurHash(Engine engine,
JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY)
{
if (callback == null)
@ -166,36 +172,36 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
TryGetAssetRef(context, input, out var asset);
// The javascript values are read while we are still inside the engine.
TryGetAssetRef(engine, input, out var asset);
var options = new BlurOptions();
var options = new BlurOptions();
if (componentX?.IsNumber() == true)
{
options.ComponentX = (int)componentX.AsNumber();
}
if (componentX?.IsNumber() == true)
{
options.ComponentX = (int)componentX.AsNumber();
}
if (componentY?.IsNumber() == true)
{
options.ComponentX = (int)componentY.AsNumber();
}
if (componentY?.IsNumber() == true)
{
options.ComponentX = (int)componentY.AsNumber();
}
engine.Schedule(async ct =>
{
try
{
var hash = await asset.GetBlurHashAsync(options, serviceProvider, ct);
scheduler.Run(callback, hash);
return await asset.GetBlurHashAsync(options, serviceProvider, ct);
}
catch
catch (Exception ex) when (ex is not OperationCanceledException)
{
scheduler.Run(callback, JsValue.Null);
return null;
}
});
},
hash => callback(JsValue.FromObject(engine, hash)));
}
private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
private void GetAssets(Engine engine, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
if (callback == null)
@ -203,74 +209,60 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
var ids = references.ToIds();
// The javascript values are read while we are still inside the engine.
var ids = references.ToIds();
if (ids.Count == 0)
{
scheduler.Run(callback, new JsArray(context.Engine));
return;
}
if (ids.Count == 0)
{
callback(new JsArray(engine));
return;
}
engine.Schedule(async ct =>
{
var app = await GetAppAsync(appId, ct);
if (app == null)
{
scheduler.Run(callback, new JsArray(context.Engine));
return;
}
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var requestContext =
new Context(user, app).Clone(b => b
.WithNoTotal());
var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, assets.ToArray()));
return;
});
return await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct);
},
assets => callback(JsValue.FromObject(engine, assets.ToArray())));
}
private void GetAsset(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
private void GetAsset(Engine engine, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);
context.Schedule(async (scheduler, ct) =>
{
var ids = references.ToIds();
// The javascript values are read while we are still inside the engine.
var ids = references.ToIds();
if (ids.Count == 0)
{
scheduler.Run(callback, JsValue.Null);
return;
}
if (ids.Count == 0)
{
callback(JsValue.Null);
return;
}
engine.Schedule(async ct =>
{
var app = await GetAppAsync(appId, ct);
if (app == null)
{
scheduler.Run(callback, JsValue.Null);
return;
}
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var requestContext =
new Context(user, app).Clone(b => b
.WithNoTotal());
var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, assets.FirstOrDefault()));
return;
});
return await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct);
},
assets => callback(JsValue.FromObject(engine, assets.FirstOrDefault())));
}
private static bool TryGetAssetRef(ScriptExecutionContext context, JsValue input, out AssetRef assetRef)
private static bool TryGetAssetRef(Engine engine, JsValue input, out AssetRef assetRef)
{
assetRef = default;
@ -290,6 +282,8 @@ public sealed class AssetsJintExtension(IServiceProvider serviceProvider) : IJin
return true;
case AssetEntityScriptVars vars:
var context = engine.GetContext();
if (!context.TryGetValueIfExists<string>(nameof(AssetScriptVars.AppName), out var appName) ||
!context.TryGetValueIfExists<DomainId>(nameof(AssetScriptVars.AppId), out var appId) ||
!context.TryGetValueIfExists<DomainId>(nameof(AssetScriptVars.AssetId), out var assetId))

50
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsJintExtension.cs

@ -22,8 +22,10 @@ public sealed class ContentsJintExtension(IServiceProvider serviceProvider) : IJ
{
private delegate void GetContentsDelegate(string schema, JsValue query, Action<JsValue> callback);
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
var context = engine.GetContext();
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
@ -36,13 +38,13 @@ public sealed class ContentsJintExtension(IServiceProvider serviceProvider) : IJ
var getContents = new GetContentsDelegate((schemas, query, callback) =>
{
GetContents(context, appId, user, schemas, query, callback);
GetContents(engine, appId, user, schemas, query, callback);
});
context.Engine.SetValue("getContents", getContents);
engine.SetValue("getContents", getContents);
}
private void GetContents(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
private void GetContents(Engine engine, DomainId appId, ClaimsPrincipal user,
string schema, JsValue query, Action<JsValue> callback)
{
if (callback == null)
@ -50,15 +52,23 @@ public sealed class ContentsJintExtension(IServiceProvider serviceProvider) : IJ
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
// The query is read while we are still inside the engine.
var q = Q.Empty;
if (query is ObjectInstance obj)
{
var app = await GetAppAsync(appId);
if (app == null)
if (obj.TryGetValue("query", out var t) && t is JsString oDataQuery)
{
scheduler.Run(callback, new JsArray(context.Engine));
return;
q = q.WithODataQuery(oDataQuery.AsString());
}
}
else if (query is JsString oDataQueryValue)
{
q = q.WithODataQuery(oDataQueryValue.AsString());
}
engine.Schedule(async ct =>
{
var app = await GetAppAsync(appId);
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
@ -69,23 +79,9 @@ public sealed class ContentsJintExtension(IServiceProvider serviceProvider) : IJ
.WithUnpublished()
.WithNoTotal());
var q = Q.Empty;
if (query is ObjectInstance obj)
{
if (obj.TryGetValue("query", out var t) && t is JsString oDataQuery)
{
q = q.WithODataQuery(oDataQuery.AsString());
}
}
else if (query is JsString oDataQuery)
{
q = q.WithODataQuery(oDataQuery.AsString());
}
var contents = await contentQuery.QueryAsync(requestContext, schema, q, ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.ToArray()));
});
return await contentQuery.QueryAsync(requestContext, schema, q, ct);
},
contents => callback(JsValue.FromObject(engine, contents.ToArray())));
}
private async Task<App> GetAppAsync(DomainId appId)

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Properties;
@ -18,9 +19,9 @@ public sealed class CounterJintExtension(ICounterService counterService) : IJint
private delegate long CounterResetDelegate(string name, long value = 0);
private delegate void CounterResetV2Delegate(string name, Action<JsValue>? callback = null, long value = 0);
public void Extend(ScriptExecutionContext context)
public void Extend(Engine engine)
{
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
if (!engine.GetContext().TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}
@ -30,36 +31,36 @@ public sealed class CounterJintExtension(ICounterService counterService) : IJint
return Increment(appId, name);
});
context.Engine.SetValue("incrementCounter", increment);
engine.SetValue("incrementCounter", increment);
var reset = new CounterResetDelegate((name, value) =>
{
return Reset(appId, name, value);
});
context.Engine.SetValue("resetCounter", reset);
engine.SetValue("resetCounter", reset);
}
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
if (!engine.GetContext().TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}
var increment = new Action<string, Action<JsValue>>((name, callback) =>
{
IncrementV2(context, appId, name, callback);
IncrementV2(engine, appId, name, callback);
});
context.Engine.SetValue("incrementCounterV2", increment);
engine.SetValue("incrementCounterV2", increment);
var reset = new CounterResetV2Delegate((name, callback, value) =>
{
ResetV2(context, appId, name, callback, value);
ResetV2(engine, appId, name, callback, value);
});
context.Engine.SetValue("resetCounterV2", reset);
engine.SetValue("resetCounterV2", reset);
}
private long Increment(DomainId appId, string name)
@ -67,17 +68,10 @@ public sealed class CounterJintExtension(ICounterService counterService) : IJint
return AsyncHelper.Sync(() => counterService.IncrementAsync(appId, name));
}
private void IncrementV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue> callback)
private void IncrementV2(Engine engine, DomainId appId, string name, Action<JsValue> callback)
{
context.Schedule(async (scheduler, ct) =>
{
var result = await counterService.IncrementAsync(appId, name, ct);
if (callback != null)
{
scheduler.Run(callback, JsValue.FromObject(context.Engine, result));
}
});
engine.Schedule(ct => counterService.IncrementAsync(appId, name, ct),
result => callback?.Invoke(JsValue.FromObject(engine, result)));
}
private long Reset(DomainId appId, string name, long value)
@ -85,17 +79,10 @@ public sealed class CounterJintExtension(ICounterService counterService) : IJint
return AsyncHelper.Sync(() => counterService.ResetAsync(appId, name, value));
}
private void ResetV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue>? callback, long value)
private void ResetV2(Engine engine, DomainId appId, string name, Action<JsValue>? callback, long value)
{
context.Schedule(async (scheduler, ct) =>
{
var result = await counterService.ResetAsync(appId, name, value, ct);
if (callback != null)
{
scheduler.Run(callback, JsValue.FromObject(context.Engine, result));
}
});
engine.Schedule(ct => counterService.ResetAsync(appId, name, value, ct),
result => callback?.Invoke(JsValue.FromObject(engine, result)));
}
public void Describe(AddDescription describe, ScriptScope scope)

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

@ -6,6 +6,7 @@
// ==========================================================================
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.Extensions.DependencyInjection;
@ -21,8 +22,10 @@ public sealed class ReferencesJintExtension(IServiceProvider serviceProvider) :
{
private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback);
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
var context = engine.GetContext();
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
@ -35,20 +38,20 @@ public sealed class ReferencesJintExtension(IServiceProvider serviceProvider) :
var getReference = new GetReferencesDelegate((references, callback) =>
{
GetReference(context, appId, user, references, callback);
GetReference(engine, appId, user, references, callback);
});
var getReferences = new GetReferencesDelegate((references, callback) =>
{
GetReferences(context, appId, user, references, callback);
GetReferences(engine, appId, user, references, callback);
});
context.Engine.SetValue("getReference", getReferences);
context.Engine.SetValue("getReferenceV2", getReference);
context.Engine.SetValue("getReferences", getReferences);
engine.SetValue("getReference", getReferences);
engine.SetValue("getReferenceV2", getReference);
engine.SetValue("getReferences", getReferences);
}
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
private void GetReferences(Engine engine, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
if (callback == null)
@ -56,24 +59,19 @@ public sealed class ReferencesJintExtension(IServiceProvider serviceProvider) :
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
var ids = references.ToIds();
// The javascript values are read while we are still inside the engine.
var ids = references.ToIds();
if (ids.Count == 0)
{
scheduler.Run(callback, new JsArray(context.Engine));
return;
}
if (ids.Count == 0)
{
callback(new JsArray(engine));
return;
}
engine.Schedule(async ct =>
{
var app = await GetAppAsync(appId);
if (app == null)
{
scheduler.Run(callback, new JsArray(context.Engine));
return;
}
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var requestContext =
@ -83,13 +81,12 @@ public sealed class ReferencesJintExtension(IServiceProvider serviceProvider) :
.WithUnpublished()
.WithNoTotal());
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.ToArray()));
});
return await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct);
},
contents => callback(JsValue.FromObject(engine, contents.ToArray())));
}
private void GetReference(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
private void GetReference(Engine engine, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
if (callback == null)
@ -97,24 +94,19 @@ public sealed class ReferencesJintExtension(IServiceProvider serviceProvider) :
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
var ids = references.ToIds();
// The javascript values are read while we are still inside the engine.
var ids = references.ToIds();
if (ids.Count == 0)
{
scheduler.Run(callback, JsValue.Null);
return;
}
if (ids.Count == 0)
{
callback(JsValue.Null);
return;
}
engine.Schedule(async ct =>
{
var app = await GetAppAsync(appId);
if (app == null)
{
scheduler.Run(callback, JsValue.Null);
return;
}
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var requestContext =
@ -124,10 +116,9 @@ public sealed class ReferencesJintExtension(IServiceProvider serviceProvider) :
.WithUnpublished()
.WithNoTotal());
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.FirstOrDefault()));
});
return await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct);
},
contents => callback(JsValue.FromObject(engine, contents.FirstOrDefault())));
}
private async Task<App> GetAppAsync(DomainId appId)

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

@ -18,6 +18,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation;
using Engine = Jint.Engine;
namespace Squidex.Domain.Apps.Core.Operations.Scripting;
@ -68,26 +69,20 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
{
private delegate void Delay(Action callback, int time);
public void ExtendAsync(ScriptExecutionContext context)
public void ExtendAsync(Engine engine)
{
context.Engine.SetValue("setTimeout", new Delay((callback, time) =>
engine.SetValue("setTimeout", new Delay((callback, time) =>
{
if (time == 0)
engine.Schedule(async ct =>
{
context.Schedule((scheduler, ct) =>
{
scheduler.Run(callback);
return Task.CompletedTask;
});
}
else
{
context.Schedule(async (scheduler, ct) =>
if (time > 0)
{
await Task.Delay(time, ct);
scheduler.Run(callback);
});
}
}
return true;
},
_ => callback());
}));
}
}

Loading…
Cancel
Save