Skip to content

Commit

Permalink
Implements content item created trigger #82
Browse files Browse the repository at this point in the history
  • Loading branch information
apexdodge committed Apr 27, 2024
1 parent de3b099 commit 5dc4d07
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public interface IRaythaFunctionScriptEngine
{
public void Initialize(string code);
public Task<object> EvaluateGet(string query, TimeSpan executeTimeout, CancellationToken cancellationToken);
public Task<object> EvaluatePost(string payload, string query, TimeSpan executeTimeout, CancellationToken cancellationToken);
public Task<object> EvaluateGet(string code, string query, TimeSpan executeTimeout, CancellationToken cancellationToken);
public Task<object> EvaluatePost(string code, string payload, string query, TimeSpan executeTimeout, CancellationToken cancellationToken);
public Task EvaluateRun(string code, string payload, TimeSpan executeTimeout, CancellationToken cancellationToken);
}
2 changes: 2 additions & 0 deletions src/Raytha.Application/ConfigureServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Raytha.Application.Common.Behaviors;
using Raytha.Application.ContentItems;
using Raytha.Application.ContentItems.Commands;
using Raytha.Application.ContentItems.EventHandlers;
using System.Reflection;

namespace Raytha.Application;
Expand All @@ -22,6 +23,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
});
services.AddScoped<BeginExportContentItemsToCsv.BackgroundTask>();
services.AddScoped<BeginImportContentItemsFromCsv.BackgroundTask>();
services.AddScoped<ContentItemCreatedEventHandler.BackgroundTask>();
services.AddScoped<FieldValueConverter>();
return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using MediatR;
using Raytha.Application.Common.Exceptions;
using Raytha.Application.Common.Interfaces;
using Raytha.Domain.Entities;
using Raytha.Domain.Events;
using Raytha.Domain.ValueObjects;
using System.Text.Json;

namespace Raytha.Application.ContentItems.EventHandlers;

public class ContentItemCreatedEventHandler : INotificationHandler<ContentItemCreatedEvent>
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly IRaythaDbContext _db;

public ContentItemCreatedEventHandler(
IBackgroundTaskQueue taskQueue,
IRaythaDbContext db)
{
_taskQueue = taskQueue;
_db = db;
}

public async Task Handle(ContentItemCreatedEvent notification, CancellationToken cancellationToken)
{
var activeFunctions = _db.RaythaFunctions.Where(p => p.IsActive && p.TriggerType == RaythaFunctionTriggerType.ContentItemCreated.DeveloperName);
if (activeFunctions.Any())
{
foreach (var activeFunction in activeFunctions)
{
await _taskQueue.EnqueueAsync<BackgroundTask>(new ContentItemAndActiveFunction
{
Event = ContentItemDto.GetProjection(notification.ContentItem),
RaythaFunction = activeFunction
}, cancellationToken);
}
}
}

public class ContentItemAndActiveFunction
{
public required ContentItemDto Event { get; init; }
public required RaythaFunction RaythaFunction { get; init; }
}

public class BackgroundTask : IExecuteBackgroundTask
{
private readonly IRaythaDbContext _entityFrameworkDb;
private readonly IRaythaFunctionConfiguration _raythaFunctionConfiguration;
private readonly IRaythaFunctionScriptEngine _raythaFunctionScriptEngine;
private readonly IRaythaFunctionSemaphore _raythaFunctionSemaphore;

public BackgroundTask(
IRaythaFunctionConfiguration raythaFunctionConfiguration,
IRaythaFunctionSemaphore raythaFunctionSemaphore,
IRaythaFunctionScriptEngine raythaFunctionScriptEngine,
IRaythaDbContext entityFrameworkDb)
{
_raythaFunctionConfiguration = raythaFunctionConfiguration;
_raythaFunctionSemaphore = raythaFunctionSemaphore;
_raythaFunctionScriptEngine = raythaFunctionScriptEngine;
_entityFrameworkDb = entityFrameworkDb;
}

public async Task Execute(Guid jobId, JsonElement args, CancellationToken cancellationToken)
{
string? raythaFunctionName = args.GetProperty("RaythaFunction").GetProperty("Name").GetString();
string? code = args.GetProperty("RaythaFunction").GetProperty("Code").GetString();

var job = _entityFrameworkDb.BackgroundTasks.First(p => p.Id == jobId);
job.TaskStep = 1;
job.StatusInfo = $"Running Raytha Function";
job.PercentComplete = 0;
_entityFrameworkDb.BackgroundTasks.Update(job);
await _entityFrameworkDb.SaveChangesAsync(cancellationToken);

if (await _raythaFunctionSemaphore.WaitAsync(_raythaFunctionConfiguration.QueueTimeout, cancellationToken))
{
try
{
string payload = JsonSerializer.Serialize(args.GetProperty("Event"));
await _raythaFunctionScriptEngine.EvaluateRun(code, payload, _raythaFunctionConfiguration.ExecuteTimeout, cancellationToken);
job.TaskStep = 2;
job.StatusInfo = $"Completed Raytha Function: {raythaFunctionName}";
job.PercentComplete = 100;
}
catch (Exception exception) when(exception is RaythaFunctionExecuteTimeoutException or RaythaFunctionScriptException)
{
job.StatusInfo = $"Error running Raytha Function {raythaFunctionName} - {exception.Message}";
}
finally
{
job.TaskStep = 2;
job.PercentComplete = 100;
_entityFrameworkDb.BackgroundTasks.Update(job);
await _entityFrameworkDb.SaveChangesAsync(cancellationToken);
_raythaFunctionSemaphore.Release();
}
}
else
{
job.TaskStep = 2;
job.StatusInfo = $"Raytha Function {raythaFunctionName} failed to run because too many background tasks are running";
job.PercentComplete = 100;
_entityFrameworkDb.BackgroundTasks.Update(job);
await _entityFrameworkDb.SaveChangesAsync(cancellationToken);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,10 @@ public async Task<CommandResponseDto<object>> Handle(Command request, Cancellati
{
try
{
_raythaFunctionScriptEngine.Initialize(code);

return request.RequestMethod switch
{
"GET" => new CommandResponseDto<object>(await _raythaFunctionScriptEngine.EvaluateGet(request.QueryJson, _raythaFunctionConfiguration.ExecuteTimeout, cancellationToken)),
"POST" => new CommandResponseDto<object>(await _raythaFunctionScriptEngine.EvaluatePost(request.PayloadJson, request.QueryJson, _raythaFunctionConfiguration.ExecuteTimeout, cancellationToken)),
"GET" => new CommandResponseDto<object>(await _raythaFunctionScriptEngine.EvaluateGet(code, request.QueryJson, _raythaFunctionConfiguration.ExecuteTimeout, cancellationToken)),
"POST" => new CommandResponseDto<object>(await _raythaFunctionScriptEngine.EvaluatePost(code, request.PayloadJson, request.QueryJson, _raythaFunctionConfiguration.ExecuteTimeout, cancellationToken)),
_ => throw new NotImplementedException(),
};
}
Expand Down
8 changes: 7 additions & 1 deletion src/Raytha.Domain/ValueObjects/RaythaFunctionTriggerType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ public static RaythaFunctionTriggerType From(string developerName)
return type;
}

public static RaythaFunctionTriggerType HttpRequest => new("Http Request", "http_request");
public static RaythaFunctionTriggerType HttpRequest => new("Http request", "http_request");
public static RaythaFunctionTriggerType ContentItemCreated => new("Content item created", "content_item_created");
public static RaythaFunctionTriggerType ContentItemUpdated => new("Content item updated", "content_item_updated");
public static RaythaFunctionTriggerType ContentItemDeleted => new("Content item deleted", "content_item_deleted");

public string Label { get; }
public string DeveloperName { get; }
Expand All @@ -40,6 +43,9 @@ public static IEnumerable<RaythaFunctionTriggerType> SupportedTypes
get
{
yield return HttpRequest;
yield return ContentItemCreated;
yield return ContentItemUpdated;
yield return ContentItemDeleted;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public class RaythaFunctionScriptEngine : IRaythaFunctionScriptEngine
private readonly ICurrentOrganization _currentOrganization;
private readonly ICurrentUser _currentUser;
private readonly IRaythaFunctionsHttpClient _httpClient;
private readonly V8ScriptEngine _engine;

public RaythaFunctionScriptEngine(IRaythaFunctionApi_V1 raythaFunctionApiV1,
IEmailer emailer,
Expand All @@ -28,102 +27,98 @@ public class RaythaFunctionScriptEngine : IRaythaFunctionScriptEngine
_currentOrganization = currentOrganization;
_currentUser = currentUser;
_httpClient = httpClient;
_engine = new V8ScriptEngine();
}

public void Initialize(string code)
public async Task<object> Evaluate(string code, string method, TimeSpan executeTimeout, CancellationToken cancellationToken)
{
_engine.AddHostObject("API_V1", _raythaFunctionApiV1);
_engine.AddHostObject("CurrentOrganization", _currentOrganization);
_engine.AddHostObject("CurrentUser", _currentUser);
_engine.AddHostObject("Emailer", _emailer);
_engine.AddHostObject("HttpClient", _httpClient);
_engine.AddHostObject("host", new HostFunctions());
_engine.AddHostObject("clr", new HostTypeCollection("mscorlib", "System", "System.Core", "System.Linq", "System.Collections"));
_engine.AddHostType(typeof(JavaScriptExtensions));
_engine.AddHostType(typeof(Enumerable));
_engine.AddHostType(typeof(ShortGuid));
_engine.AddHostType(typeof(Guid));
_engine.AddHostType(typeof(Convert));
_engine.AddHostType(typeof(EmailMessage));
_engine.Execute("var System = clr.System;");
_engine.Execute(@"
class JsonResult {
constructor(obj) {
this.body = obj;
this.contentType = 'application/json';
}
}
using (var _engine = new V8ScriptEngine())
{
_engine.AddHostObject("API_V1", _raythaFunctionApiV1);
_engine.AddHostObject("CurrentOrganization", _currentOrganization);
_engine.AddHostObject("CurrentUser", _currentUser);
_engine.AddHostObject("Emailer", _emailer);
_engine.AddHostObject("HttpClient", _httpClient);
_engine.AddHostObject("host", new HostFunctions());
_engine.AddHostObject("clr", new HostTypeCollection("mscorlib", "System", "System.Core", "System.Linq", "System.Collections"));
_engine.AddHostType(typeof(JavaScriptExtensions));
_engine.AddHostType(typeof(Enumerable));
_engine.AddHostType(typeof(ShortGuid));
_engine.AddHostType(typeof(Guid));
_engine.AddHostType(typeof(Convert));
_engine.AddHostType(typeof(EmailMessage));
_engine.Execute("var System = clr.System;");
_engine.Execute(@"
class JsonResult {
constructor(obj) {
this.body = obj;
this.contentType = 'application/json';
}
}
class HtmlResult {
constructor(html) {
this.body = html;
this.contentType = 'text/html';
}
}
class HtmlResult {
constructor(html) {
this.body = html;
this.contentType = 'text/html';
}
}
class RedirectResult {
constructor(url) {
this.body = url;
this.contentType = 'redirectToUrl';
}
}
class RedirectResult {
constructor(url) {
this.body = url;
this.contentType = 'redirectToUrl';
}
}
class StatusCodeResult {
constructor(statusCode, error) {
this.statusCode = statusCode;
this.body = error;
this.contentType = 'statusCode';
}
}");
class StatusCodeResult {
constructor(statusCode, error) {
this.statusCode = statusCode;
this.body = error;
this.contentType = 'statusCode';
}
}");

try
{
_engine.Execute(code);
}
catch (ScriptEngineException exception)
{
throw new RaythaFunctionScriptException(exception.ErrorDetails);
try
{
_engine.Execute(code);
return await Task.Run(async () =>
{
var result = _engine.Evaluate(method);
// The script can be synchronous or asynchronous, so this simple solution is used to convert the result
// Source: https://github.com/microsoft/ClearScript/issues/366
try
{
return await result.ToTask();
}
catch (ArgumentException)
{
return result;
}
}, cancellationToken).WaitAsync(executeTimeout, cancellationToken);
}
catch (TimeoutException)
{
throw new RaythaFunctionExecuteTimeoutException("The function execution time has exceeded the timeout");
}
catch (ScriptEngineException exception)
{
throw new RaythaFunctionScriptException(exception.ErrorDetails);
}
}
}

public async Task<object> EvaluateGet(string query, TimeSpan executeTimeout, CancellationToken cancellationToken)
public async Task<object> EvaluateGet(string code,string query, TimeSpan executeTimeout, CancellationToken cancellationToken)
{
return await Evaluate($"get({query})", executeTimeout, cancellationToken);
return await Evaluate(code, $"get({query})", executeTimeout, cancellationToken);
}

public async Task<object> EvaluatePost(string payload, string query, TimeSpan executeTimeout, CancellationToken cancellationToken)
public async Task<object> EvaluatePost(string code, string payload, string query, TimeSpan executeTimeout, CancellationToken cancellationToken)
{
return await Evaluate($"post({payload}, {query})", executeTimeout, cancellationToken);
return await Evaluate(code, $"post({payload}, {query})", executeTimeout, cancellationToken);
}

private async Task<object> Evaluate(string method, TimeSpan executeTimeout, CancellationToken cancellationToken)
public async Task EvaluateRun(string code, string payload, TimeSpan executeTimeout, CancellationToken cancellationToken)
{
try
{
return await Task.Run(async () =>
{
var result = _engine.Evaluate(method);
// The script can be synchronous or asynchronous, so this simple solution is used to convert the result
// Source: https://github.com/microsoft/ClearScript/issues/366
try
{
return await result.ToTask();
}
catch (ArgumentException)
{
return result;
}
}, cancellationToken).WaitAsync(executeTimeout, cancellationToken);
}
catch (TimeoutException)
{
throw new RaythaFunctionExecuteTimeoutException("The function execution time has exceeded the timeout");
}
catch (ScriptEngineException exception)
{
throw new RaythaFunctionScriptException(exception.Message);
}
await Evaluate(code, $"run({payload})", executeTimeout, cancellationToken);
}
}

0 comments on commit 5dc4d07

Please sign in to comment.