Browse Source

Script improvements (#1136)

* Custom indexes.

* Adjust test.

* New script extensions.

* New script extensions.
pull/1137/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
060f43357b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs
  2. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JsonMapper.cs
  3. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  4. 118
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsJintExtension.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  6. 9
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs
  7. 3
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx
  8. 1
      backend/src/Squidex.Web/Pipeline/JsonStreamResult.cs
  9. 5
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  10. 31
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  11. 147
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsJintExtensionTests.cs
  12. 31
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs

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

@ -17,18 +17,15 @@ public static class JintExtensions
{
var ids = new List<DomainId>();
if (value?.IsString() == true)
if (value is JsString s)
{
ids.Add(DomainId.Create(value.ToString()));
ids.Add(DomainId.Create(s.AsString()));
}
else if (value?.IsArray() == true)
else if (value is JsArray a)
{
foreach (var item in value.AsArray())
foreach (var item in a.OfType<JsString>())
{
if (item.IsString())
{
ids.Add(DomainId.Create(item.ToString()));
}
ids.Add(DomainId.Create(item.AsString()));
}
}

14
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JsonMapper.cs

@ -113,15 +113,13 @@ public static class JsonMapper
return number;
}
if (value.IsArray())
if (value is JsArray a)
{
var arr = value.AsArray();
var result = new JsonArray((int)a.Length);
var result = new JsonArray((int)arr.Length);
for (var i = 0; i < arr.Length; i++)
for (var i = 0; i < a.Length; i++)
{
result.Add(Map(arr.Get(i.ToString(CultureInfo.InvariantCulture))));
result.Add(Map(a.Get(i.ToString(CultureInfo.InvariantCulture))));
}
return result;
@ -132,10 +130,8 @@ public static class JsonMapper
return JsonValue.Create(wrapper.Target);
}
if (value.IsObject())
if (value is ObjectInstance obj)
{
var obj = value.AsObject();
var result = new JsonObject();
foreach (var (key, propertyDescriptor) in obj.GetOwnProperties())

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

@ -140,7 +140,8 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("getAssetBlurHash", getBlurHash);
}
private void GetText(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? encoding)
private void GetText(ScriptExecutionContext context,
JsValue input, Action<JsValue> callback, JsValue? encoding)
{
if (callback == null)
{
@ -163,7 +164,8 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
});
}
private void GetBlurHash(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY)
private void GetBlurHash(ScriptExecutionContext context,
JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY)
{
if (callback == null)
{
@ -199,7 +201,8 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
});
}
private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
if (callback == null)
{
@ -237,7 +240,8 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
});
}
private void GetAsset(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
private void GetAsset(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);

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

@ -0,0 +1,118 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Domain.Apps.Entities.Properties;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed class ContentsJintExtension : IJintExtension, IScriptDescriptor
{
private delegate void GetContentsDelegate(string schema, JsValue query, Action<JsValue> callback);
private readonly IServiceProvider serviceProvider;
public ContentsJintExtension(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public void ExtendAsync(ScriptExecutionContext context)
{
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}
if (!context.TryGetValueIfExists<ClaimsPrincipal>("user", out var user))
{
return;
}
var getContents = new GetContentsDelegate((schemas, query, callback) =>
{
GetContents(context, appId, user, schemas, query, callback);
});
context.Engine.SetValue("getContents", getContents);
}
private void GetContents(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
string schema, JsValue query, Action<JsValue> callback)
{
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
var app = await GetAppAsync(appId);
if (app == null)
{
scheduler.Run(callback, new JsArray(context.Engine));
return;
}
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var requestContext =
new Context(user, app).Clone(b => b
.WithFields(null)
.WithNoEnrichment()
.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()));
});
}
private async Task<App> GetAppAsync(DomainId appId)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(appId) ??
throw new JavaScriptException("App does not exist.");
return app;
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getContents(schema, query, callback)",
Resources.ScriptingGetContents);
}
}

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

@ -54,7 +54,8 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("getReferences", getReferences);
}
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
if (callback == null)
{
@ -94,7 +95,8 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor
});
}
private void GetReference(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
private void GetReference(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user,
JsValue references, Action<JsValue> callback)
{
if (callback == null)
{

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

@ -114,6 +114,15 @@ namespace Squidex.Domain.Apps.Entities.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Queries contents by schema and query..
/// </summary>
internal static string ScriptingGetContents {
get {
return ResourceManager.GetString("ScriptingGetContents", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Queries the content item with the specified ID and invokes the callback with an array of contents..
/// </summary>

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

@ -135,6 +135,9 @@
<data name="ScriptingGetBlurHash" xml:space="preserve">
<value>Gets the blur hash of an asset if it is an image or null otherwise.</value>
</data>
<data name="ScriptingGetContents" xml:space="preserve">
<value>Queries contents by schema and query.</value>
</data>
<data name="ScriptingGetReference" xml:space="preserve">
<value>Queries the content item with the specified ID and invokes the callback with an array of contents.</value>
</data>

1
backend/src/Squidex.Web/Pipeline/JsonStreamResult.cs

@ -51,6 +51,7 @@ public sealed class JsonStreamResult<T> : ActionResult
// Write the separator after a every json object to simplify deserialization.
await body.WriteAsync(Separator, ct);
await body.FlushAsync(ct);
}
}

5
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -18,6 +18,7 @@ using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
using Squidex.Web.Pipeline;
using System.Diagnostics;
namespace Squidex.Areas.Api.Controllers.Contents;
@ -56,6 +57,10 @@ public sealed class ContentsController : ApiController
[OpenApiIgnore]
public IActionResult StreamContents(string app, string schema, [FromQuery] int skip = 0)
{
if (schema.Equals("de-studyprogram-details"))
{
Debugger.Break();
}
var contents = contentQuery.StreamAsync(Context, schema, skip, HttpContext.RequestAborted);
return new JsonStreamResult<EnrichedContent>(contents);

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

@ -21,6 +21,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Assets;
@ -75,6 +76,16 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
}
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null_on_getAsset()
{
var (vars, _) = SetupAssetsVars(1);
var script = @"getAsset('id')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_resolve_asset()
{
@ -96,6 +107,16 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null_on_getAssetV2()
{
var (vars, _) = SetupAssetsVars(1);
var script = @"getAssetV2('id')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_resolve_asset_v2()
{
@ -117,6 +138,16 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null_on_getAssets()
{
var (vars, _) = SetupAssetsVars(1);
var script = @"getAssetV2('id')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_resolve_assets()
{

147
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsJintExtensionTests.cs

@ -0,0 +1,147 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Contents;
public class ContentsJintExtensionTests : GivenContext, IClassFixture<TranslationsFixture>
{
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly JintScriptEngine sut;
public ContentsJintExtensionTests()
{
var serviceProvider =
new ServiceCollection()
.AddSingleton(AppProvider)
.AddSingleton(contentQuery)
.BuildServiceProvider();
var extensions = new IJintExtension[]
{
new ContentsJintExtension(serviceProvider)
};
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
Options.Create(new JintScriptOptions
{
TimeoutScript = TimeSpan.FromSeconds(2),
TimeoutExecution = TimeSpan.FromSeconds(10)
}),
extensions);
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null()
{
var (vars, _) = SetupQueryVars("my-schema", "$filter=data/field/iv eq 42", 2);
var script = @"getContents('my-schema', '$filter=data/field/iv eq 42')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_query_contents()
{
var (vars, _) = SetupQueryVars("my-schema", "$filter=data/field/iv eq 42", 2);
var expected = @"
Text: Hello 1 World 1
";
var script = @"
getContents('my-schema', { query: '$filter=data/field/iv eq 42' }, function (references) {
var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
complete(`${actual1}`);
})";
var actual = (await sut.ExecuteAsync(vars, script, ct: CancellationToken)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
[Fact]
public async Task Should_query_contents_with_string()
{
var (vars, _) = SetupQueryVars("my-schema", "$filter=data/field/iv eq 42", 2);
var expected = @"
Text: Hello 1 World 1
";
var script = @"
getContents('my-schema', '$filter=data/field/iv eq 42', function (references) {
var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
complete(`${actual1}`);
})";
var actual = (await sut.ExecuteAsync(vars, script, ct: CancellationToken)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
private (ScriptVars, EnrichedContent[]) SetupQueryVars(string schema, string filter, int count)
{
var references = Enumerable.Range(0, count).Select((x, i) => CreateContent(i + 1)).ToArray();
var referenceIds = references.Select(x => x.Id);
var user = new ClaimsPrincipal();
A.CallTo(() => contentQuery.QueryAsync(
A<Context>.That.Matches(x => x.App == App && x.UserPrincipal == user),
schema,
A<Q>.That.Matches(x => x.QueryAsOdata == filter),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, [CreateContent(1)]));
var vars = new ScriptVars
{
["appId"] = AppId.Id,
["appName"] = AppId.Name,
["user"] = user
};
return (vars, references);
}
private EnrichedContent CreateContent(int index)
{
return CreateContent() with
{
Data =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddInvariant(JsonValue.Create($"Hello {index}")))
.AddField("field2",
new ContentFieldData()
.AddInvariant(JsonValue.Create($"World {index}")))
};
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty, StringComparison.Ordinal)
.Replace("\n", string.Empty, StringComparison.Ordinal)
.Replace(" ", string.Empty, StringComparison.Ordinal);
}
}

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

@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Contents;
@ -45,6 +46,16 @@ public class ReferencesJintExtensionTests : GivenContext, IClassFixture<Translat
extensions);
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null_on_getReference()
{
var (vars, _) = SetupReferenceVars(1);
var script = @"getReference('id')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_resolve_reference()
{
@ -66,6 +77,16 @@ public class ReferencesJintExtensionTests : GivenContext, IClassFixture<Translat
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null_on_getReferenceV2()
{
var (vars, _) = SetupReferenceVars(1);
var script = @"getReferenceV2('id')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_resolve_reference_v2()
{
@ -87,6 +108,16 @@ public class ReferencesJintExtensionTests : GivenContext, IClassFixture<Translat
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
[Fact]
public async Task Should_throw_exception_if_callback_is_null_on_getReferences()
{
var (vars, _) = SetupReferenceVars(1);
var script = @"getReferences('id')";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, ct: CancellationToken));
}
[Fact]
public async Task Should_resolve_references()
{

Loading…
Cancel
Save