Browse Source

Add script request method (#1253)

* Add script requestAsync

* Refactor return type and line conditions of the code

* Refactor request header type

* Fix remove unnecessary types from test cases

* Fix remove unnecessary types from exception test cases

* Fix ScriptingRequestAsync additional comments

* Fix incorrectly expressed code

* Refactor change the name of the function and modify some sections of the code.
pull/1260/head
Noah 11 months ago
committed by GitHub
parent
commit
379caaef5f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 132
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs
  2. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx
  3. 78
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  4. 118
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

132
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs

@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -12,46 +11,32 @@ namespace Squidex.Domain.Apps.Core.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
private static System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly);
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@ -60,265 +45,184 @@ namespace Squidex.Domain.Apps.Core.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset..
/// </summary>
internal static string ScriptingAssetContentAppUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID..
/// </summary>
internal static string ScriptingAssetContentSlugUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset without the app name (deprecated)..
/// </summary>
internal static string ScriptingAssetContentUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingCharacterCount {
get {
return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Completes the script when an async method is used..
/// </summary>
internal static string ScriptingComplete {
get {
return ResourceManager.GetString("ScriptingComplete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The status of the content..
/// </summary>
internal static string ScriptingContentAction {
get {
return ResourceManager.GetString("ScriptingContentAction", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The URL to the content in the UI..
/// </summary>
internal static string ScriptingContentUrl {
get {
return ResourceManager.GetString("ScriptingContentUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingDeleteJson {
get {
return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest)..
/// </summary>
internal static string ScriptingDisallow {
get {
return ResourceManager.GetString("ScriptingDisallow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatDate {
get {
return ResourceManager.GetString("ScriptingFormatDate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatTime {
get {
return ResourceManager.GetString("ScriptingFormatTime", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uses OpenAI or other machine learning platforms to generate content from a prompt..
/// </summary>
internal static string ScriptingGenerate {
get {
return ResourceManager.GetString("ScriptingGenerate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingGetJSON {
get {
return ResourceManager.GetString("ScriptingGetJSON", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Generates a guid..
/// </summary>
internal static string ScriptingGuid {
get {
return ResourceManager.GetString("ScriptingGuid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a HTML string to plain text..
/// </summary>
internal static string ScriptingHtml2Text {
get {
return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a markdown string to plain text..
/// </summary>
internal static string ScriptingMarkdown2Text {
get {
return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important..
/// </summary>
internal static string ScriptingMD5 {
get {
return ResourceManager.GetString("ScriptingMD5", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPatchJson {
get {
return ResourceManager.GetString("ScriptingPatchJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPostJSON {
get {
return ResourceManager.GetString("ScriptingPostJSON", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPutJson {
get {
return ResourceManager.GetString("ScriptingPutJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden)..
/// </summary>
internal static string ScriptingReject {
get {
return ResourceManager.GetString("ScriptingReject", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied..
/// </summary>
internal static string ScriptingReplace {
get {
return ResourceManager.GetString("ScriptingReplace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA256 {
get {
return ResourceManager.GetString("ScriptingSHA256", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA512 {
get {
return ResourceManager.GetString("ScriptingSHA512", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs..
/// </summary>
internal static string ScriptingSlugify {
get {
return ResourceManager.GetString("ScriptingSlugify", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a text to camelCase..
/// </summary>
internal static string ScriptingToCamelCase {
get {
return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Converts a text to PascalCase.
/// </summary>
internal static string ScriptingToPascalCase {
get {
return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Translate from the source text to the specified language..
/// </summary>
internal static string ScriptingTranslate {
get {
return ResourceManager.GetString("ScriptingTranslate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingWordCount {
get {
return ResourceManager.GetString("ScriptingWordCount", resourceCulture);
}
}
internal static string ScriptingRequest {
get {
return ResourceManager.GetString("ScriptingRequest", resourceCulture);
}
}
}
}

5
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx

@ -204,4 +204,7 @@
<data name="ScriptingWordCount" xml:space="preserve">
<value>Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.</value>
</data>
</root>
<data name="ScriptingRequest" xml:space="preserve">
<value>Makes a request to the defined URL. requestInit dictionary is used to configure the request call, allowing you to specify the url, method, headers, body. The response is received via a callback.</value>
</data>
</root>

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

@ -12,7 +12,6 @@ using Jint.Native.Json;
using Jint.Native.Object;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions;
@ -20,6 +19,7 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
{
private delegate void HttpJsonDelegate(string url, Action<JsValue> callback, JsValue? headers = null, bool ignoreError = false);
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)
{
@ -28,6 +28,18 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
AddBodyMethod(context, HttpMethod.Put, "putJSON");
AddMethod(context, HttpMethod.Delete, "deleteJSON");
AddMethod(context, HttpMethod.Get, "getJSON");
AddMethod(context, "request");
}
private void AddMethod(ScriptExecutionContext context, 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);
});
context.Engine.SetValue(name, action);
}
private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name)
@ -50,7 +62,7 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
context.Engine.SetValue(name, action);
}
private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action<JsValue> callback, JsValue? headers, bool ignoreError)
private void Request(ScriptExecutionContext context, 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))
{
@ -80,7 +92,7 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
var responseString = await response.Content.ReadAsStringAsync(ct);
if (ignoreError && (!response.IsSuccessStatusCode || string.IsNullOrEmpty(responseString)))
if (ignoreError && (forceRawResponse || !response.IsSuccessStatusCode || string.IsNullOrEmpty(responseString)))
{
responseObject = JsValue.FromObject(context.Engine, new Dictionary<string, object?>
{
@ -141,6 +153,17 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
request.Content = new FormUrlEncodedContent(formValues);
}
else if (string.Equals(contentType, "text/plain", StringComparison.OrdinalIgnoreCase) ||
string.Equals(contentType, "text/xml", StringComparison.OrdinalIgnoreCase) ||
string.Equals(contentType, "application/xml", StringComparison.OrdinalIgnoreCase))
{
if (!body.IsString())
{
throw new JavaScriptException("Body is not a string.");
}
request.Content = new StringContent(body.AsString(), Encoding.UTF8, contentType);
}
else
{
var jsonWriter = new JsonSerializer(context.Engine);
@ -190,6 +213,9 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
describe(JsonType.Function, "deleteJSON(url, callback, headers?, ignoreError?)",
Resources.ScriptingDeleteJson);
describe(JsonType.Function, "request(requestInit, callback)",
Resources.ScriptingRequest);
}
private static IEnumerable<(string, string)> GetNonEmptyProperties(JsValue? source)
@ -209,4 +235,50 @@ public sealed class HttpJintExtension(IHttpClientFactory httpClientFactory) : IJ
}
}
}
private static (string Url, HttpMethod Method, JsValue Headers, JsValue Body) ParseRequestInit(JsValue? requestInit)
{
if (requestInit?.IsObject() != true || requestInit.AsObject() is not ObjectInstance obj)
{
throw new JavaScriptException("RequestInit is not an object.");
}
if (!obj.TryGetValue("url", out var urlJsValue))
{
throw new JavaScriptException("Missing required property 'url'.");
}
if (!urlJsValue.IsString())
{
throw new JavaScriptException("URL property must be a string.");
}
if (!obj.TryGetValue("method", out var methodJsValue))
{
throw new JavaScriptException("Missing required property 'method'.");
}
if (!methodJsValue.IsString())
{
throw new JavaScriptException("Method property must be a string.");
}
var url = urlJsValue.AsString();
var method = methodJsValue.AsString().ToUpperInvariant() switch
{
"PATCH" => HttpMethod.Patch,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"GET" => HttpMethod.Get,
_ => throw new JavaScriptException($"Invalid HTTP method: '{methodJsValue.AsString()}'"),
};
return (
Url: url,
Method: method,
Headers: obj["headers"],
Body: obj["body"]
);
}
}

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

@ -861,6 +861,124 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
Assert.Equal(expectedResult, actual);
}
[Fact]
public async Task Should_make_request()
{
var httpHandler = SetupRequest();
var vars = new ScriptVars
{
};
const string script = @"
request({
url: 'http://squidex.io/',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: {
key: 42
}
}, function(actual) {
complete(actual);
});
";
var actual = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeUrl("http://squidex.io/");
httpHandler.ShouldBeMethod(HttpMethod.Post);
httpHandler.ShouldBeBody("{\"key\":42}", "application/json");
var expectedResult =
JsonValue.Object()
.Add("statusCode", 200)
.Add("headers",
JsonValue.Object()
.Add("Content-Type", "application/json; charset=utf-8")
.Add("Content-Length", "13"))
.Add("body", "{ \"key\": 42 }");
Assert.Equal(expectedResult, actual);
}
[Theory]
[InlineData("text/plain")]
[InlineData("text/xml")]
[InlineData("application/xml")]
public async Task Should_make_request_text_body(string input)
{
var (body, contentLength) = input switch
{
"text/plain" => ("test", "test".Length),
"text/xml" => ("<html></html>", "<html></html>".Length),
"application/xml" => ("<person gender=\"female\"></person>", "<person gender=\"female\"></person>".Length),
_ => (string.Empty, 0),
};
var httpHandler = SetupRequest(HttpStatusCode.OK, new StringContent(body, Encoding.UTF8, input));
var vars = new ScriptVars
{
};
var script = $@"
request({{
url: 'http://squidex.io/',
method: 'POST',
headers: {{
'Content-Type': '{input}'
}},
body: '{body}'
}}, function(actual) {{
complete(actual);
}});
";
var actual = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeUrl("http://squidex.io/");
httpHandler.ShouldBeMethod(HttpMethod.Post);
httpHandler.ShouldBeBody(body, input);
var expectedResult =
JsonValue.Object()
.Add("statusCode", 200)
.Add("headers", JsonValue.Object()
.Add("Content-Type", $"{input}; charset=utf-8")
.Add("Content-Length", $"{contentLength}"))
.Add("body", body);
Assert.Equal(expectedResult, actual);
}
[Theory]
[InlineData("text/plain")]
[InlineData("text/xml")]
[InlineData("application/xml")]
public async Task Should_throw_exception_if_request_body_is_not_string(string input)
{
var vars = new ScriptVars
{
};
var script = $@"
request({{
url: 'http://squidex.io/',
method: 'POST',
headers: {{
'Content-Type': '{input}'
}},
body: {{
test: 1
}}
}}, function(actual) {{
complete(actual);
}});
";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script));
}
private MockupHttpHandler SetupRequest(HttpStatusCode statusCode = HttpStatusCode.OK, StringContent? responseContent = null)
{
var httpResponse = new HttpResponseMessage(statusCode)

Loading…
Cancel
Save