Add support for array variables and fix serialization issue with proto
This commit is contained in:
parent
0584dc0bdf
commit
98d0920cfc
|
|
@ -112,6 +112,7 @@ export namespace Components {
|
|||
"addActivity": (args: AddActivityArgs) => Promise<Activity>;
|
||||
"autoLayout": (direction: LayoutDirection) => Promise<void>;
|
||||
"export": () => Promise<Activity>;
|
||||
"getActivity": (id: string) => Promise<Activity>;
|
||||
"getGraph": () => Promise<Graph>;
|
||||
"interactiveMode": boolean;
|
||||
"newRoot": () => Promise<Activity>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {Component, h, Prop, Event, EventEmitter, Method} from "@stencil/core";
|
||||
import {groupBy} from 'lodash';
|
||||
import {StorageDriverDescriptor, Variable} from "../../../models";
|
||||
import {FormEntry} from "../../shared/forms/form-entry";
|
||||
import {CheckboxFormEntry, FormEntry} from "../../shared/forms/form-entry";
|
||||
import {isNullOrWhitespace} from "../../../utils";
|
||||
import descriptorsStore from '../../../data/descriptors-store';
|
||||
import {VariableDescriptor} from "../../../services/api-client/variable-descriptors-api";
|
||||
|
|
@ -22,7 +22,7 @@ export class VariableEditorDialogContent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const variable: Variable = this.variable ?? {name: '', typeName: 'Object'};
|
||||
const variable: Variable = this.variable ?? {name: '', typeName: 'Object', isArray: false};
|
||||
const variableTypeName = variable.typeName;
|
||||
const availableTypes: Array<VariableDescriptor> = descriptorsStore.variableDescriptors;
|
||||
const groupedVariableTypes = groupBy(availableTypes, x => x.category);
|
||||
|
|
@ -50,6 +50,10 @@ export class VariableEditorDialogContent {
|
|||
</select>
|
||||
</FormEntry>
|
||||
|
||||
<CheckboxFormEntry fieldId="variableIsArray" label="This variable is an array" hint="Check if the variable holds an array of the selected type.">
|
||||
<input type="checkbox" name="variableIsArray" id="variableIsArray" value="true" checked={variable.isArray}/>
|
||||
</CheckboxFormEntry>
|
||||
|
||||
<FormEntry fieldId="variableValue" label="Value" hint="The value of the variable.">
|
||||
<input type="text" name="variableValue" id="variableValue" value={variable.value}/>
|
||||
</FormEntry>
|
||||
|
|
@ -84,12 +88,14 @@ export class VariableEditorDialogContent {
|
|||
const name = formData.get('variableName') as string;
|
||||
const value = formData.get('variableValue') as string;
|
||||
const type = formData.get('variableTypeName') as string;
|
||||
const isArray = formData.get('variableIsArray') as string == 'true';
|
||||
const driverTypeName = formData.get('variableStorageDriverTypeName') as string;
|
||||
const variable = this.variable;
|
||||
|
||||
variable.name = name;
|
||||
variable.typeName = type;
|
||||
variable.value = value;
|
||||
variable.isArray = isArray;
|
||||
variable.storageDriverTypeName = isNullOrWhitespace(driverTypeName) ? null : driverTypeName;
|
||||
|
||||
return variable;
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export class VariablesEditor {
|
|||
|
||||
private onAddVariableClick = async () => {
|
||||
const newVariableName = this.generateNewVariableName();
|
||||
const variable = {name: newVariableName, typeName: 'Object', value: null};
|
||||
const variable: Variable = {name: newVariableName, typeName: 'Object', value: null, isArray: false};
|
||||
|
||||
this.modalDialogInstance = this.modalDialogService.show(() => <elsa-variable-editor-dialog-content variable={variable}/>, {actions: [this.saveAction]})
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import 'reflect-metadata';
|
||||
import {Service} from "typedi";
|
||||
import state from "./state";
|
||||
import {ModalActionDefinition, ModalDialogInstance, ShowModalDialogOptions} from "./models";
|
||||
import {ModalType} from "../../../components/shared/modal-dialog";
|
||||
import {ModalDialogInstance, ShowModalDialogOptions} from "./models";
|
||||
|
||||
@Service()
|
||||
export class ModalDialogService {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ export interface Workflow extends Activity {
|
|||
export interface Variable {
|
||||
name: string;
|
||||
typeName: string;
|
||||
|
||||
isArray: boolean;
|
||||
value?: any;
|
||||
storageDriverTypeName?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,6 +209,11 @@ export class FlowchartComponent {
|
|||
this.updateLookups();
|
||||
}
|
||||
|
||||
@Method()
|
||||
async getActivity(id: string): Promise<Activity> {
|
||||
return this.activityLookup[id];
|
||||
}
|
||||
|
||||
@Method()
|
||||
public async renameActivity(args: RenameActivityArgs) {
|
||||
const nodeId = args.originalId;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Dynamic;
|
||||
using Elsa.Expressions.Extensions;
|
||||
using Elsa.Expressions.Services;
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ public class WellKnownTypeRegistry : IWellKnownTypeRegistry
|
|||
this.RegisterType<TimeSpan>("TimeSpan");
|
||||
this.RegisterType<DateOnly>("DateOnly");
|
||||
this.RegisterType<TimeOnly>("TimeOnly");
|
||||
this.RegisterType<ExpandoObject>("ExpandoObject");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -74,30 +74,42 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
// If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark.
|
||||
if (!context.TryGetInput<HttpContext>(HttpContextInputKey, out var httpContext))
|
||||
if (!context.TryGetInput<bool>(HttpContextInputKey, out var isHttpContext))
|
||||
{
|
||||
// Create bookmarks for when we receive the expected HTTP request.
|
||||
context.CreateBookmarks(GetBookmarkPayloads(context.ExpressionExecutionContext));
|
||||
return;
|
||||
}
|
||||
|
||||
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
|
||||
// Provide the received HTTP request as output.
|
||||
var request = httpContext.Request;
|
||||
context.Set(Result, request);
|
||||
|
||||
// Read route data, if any.
|
||||
var path = context.GetInput<PathString>(RequestPathInputKey);
|
||||
var routeData = GetRouteData(httpContext, path);
|
||||
context.Set(RouteData, routeData);
|
||||
|
||||
// Read content, if any.
|
||||
var content = await ParseContentAsync(context, request);
|
||||
context.Set(ParsedContent, content);
|
||||
|
||||
// Complete.
|
||||
await context.CompleteActivityAsync();
|
||||
if (httpContext == null)
|
||||
{
|
||||
// We're executing in a non-HTTP context (e.g. in a virtual actor).
|
||||
// Create a bookmark to allow the invoker to export the state and resume execution from there.
|
||||
|
||||
context.CreateBookmark(OnResumeAsync);
|
||||
return;
|
||||
}
|
||||
|
||||
await HandleRequestAsync(context, httpContext);
|
||||
}
|
||||
|
||||
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
|
||||
if (httpContext == null)
|
||||
{
|
||||
// We're not in an HTTP context, so let's fail.
|
||||
throw new Exception("Cannot execute in a non-HTTP context");
|
||||
}
|
||||
|
||||
await HandleRequestAsync(context, httpContext);
|
||||
}
|
||||
|
||||
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpRequest httpRequest)
|
||||
{
|
||||
if (!HasContent(httpRequest))
|
||||
|
|
@ -121,6 +133,25 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
return methods!.Select(x => new HttpEndpointBookmarkPayload(path!, x.ToLowerInvariant())).Cast<object>().ToArray();
|
||||
}
|
||||
|
||||
private async Task HandleRequestAsync(ActivityExecutionContext context, HttpContext httpContext)
|
||||
{
|
||||
// Provide the received HTTP request as output.
|
||||
var request = httpContext.Request;
|
||||
context.Set(Result, request);
|
||||
|
||||
// Read route data, if any.
|
||||
var path = context.GetInput<PathString>(RequestPathInputKey);
|
||||
var routeData = GetRouteData(httpContext, path);
|
||||
context.Set(RouteData, routeData);
|
||||
|
||||
// Read content, if any.
|
||||
var content = await ParseContentAsync(context, request);
|
||||
context.Set(ParsedContent, content);
|
||||
|
||||
// Complete.
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
private static RouteData GetRouteData(HttpContext httpContext, string path)
|
||||
{
|
||||
var routeData = httpContext.GetRouteData();
|
||||
|
|
|
|||
|
|
@ -59,17 +59,7 @@ public class WriteHttpResponse : CodeActivity
|
|||
return;
|
||||
}
|
||||
|
||||
var response = httpContext.Response;
|
||||
response.StatusCode = (int)context.Get(StatusCode);
|
||||
|
||||
var headers = ResponseHeaders.TryGet(context) ?? new HttpResponseHeaders();
|
||||
foreach (var header in headers)
|
||||
response.Headers.Add(header.Key, header.Value);
|
||||
|
||||
response.ContentType = ContentType.TryGet(context);
|
||||
var content = context.Get(Content);
|
||||
if (content != null)
|
||||
await response.WriteAsync(content, context.CancellationToken);
|
||||
await WriteResponseAsync(context, httpContext.Response);
|
||||
}
|
||||
|
||||
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
|
||||
|
|
@ -83,10 +73,17 @@ public class WriteHttpResponse : CodeActivity
|
|||
throw new Exception("Cannot execute in a non-HTTP context");
|
||||
}
|
||||
|
||||
var response = httpContext.Response;
|
||||
await WriteResponseAsync(context, httpContext.Response);
|
||||
}
|
||||
|
||||
private async Task WriteResponseAsync(ActivityExecutionContext context, HttpResponse response)
|
||||
{
|
||||
response.StatusCode = (int)context.Get(StatusCode);
|
||||
|
||||
var headers = ResponseHeaders.TryGet(context) ?? new HttpResponseHeaders();
|
||||
foreach (var header in headers)
|
||||
response.Headers.Add(header.Key, header.Value);
|
||||
|
||||
var content = context.Get(Content);
|
||||
|
||||
if (content != null)
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ public static class RouteTableExtensions
|
|||
routeTable.RemoveRange(paths);
|
||||
}
|
||||
|
||||
private static IEnumerable<StoredTrigger> Filter(IEnumerable<StoredTrigger> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>());
|
||||
private static IEnumerable<Bookmark> Filter(IEnumerable<Bookmark> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>());
|
||||
private static IEnumerable<StoredTrigger> Filter(IEnumerable<StoredTrigger> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>() && x.Data != null);
|
||||
private static IEnumerable<Bookmark> Filter(IEnumerable<Bookmark> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>() && x.Data != null);
|
||||
private static HttpEndpointBookmarkPayload Deserialize(StoredTrigger trigger) => Deserialize(trigger.Data!);
|
||||
private static HttpEndpointBookmarkPayload Deserialize(Bookmark bookmark) => Deserialize(bookmark.Data!);
|
||||
private static HttpEndpointBookmarkPayload Deserialize(string model) => JsonSerializer.Deserialize<HttpEndpointBookmarkPayload>(model, SerializerOptions)!;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public class WorkflowsMiddleware
|
|||
|
||||
var input = new Dictionary<string, object>
|
||||
{
|
||||
[HttpEndpoint.HttpContextInputKey] = httpContext,
|
||||
[HttpEndpoint.HttpContextInputKey] = true,
|
||||
[HttpEndpoint.RequestPathInputKey] = path
|
||||
};
|
||||
|
||||
|
|
@ -86,14 +86,15 @@ public class WorkflowsMiddleware
|
|||
cancellationToken);
|
||||
|
||||
// We must assume that the workflow executed in a different process (when e.g. using Proto.Actor)
|
||||
// and check if we received any `WriteHttpResponse` activity bookmarks.
|
||||
// and check if we received any `HttpEndpoint` or `WriteHttpResponse` activity bookmarks.
|
||||
// If we did, acquire a lock on the workflow instance and resume it from here within an actual HTTP context so that the activity can complete its HTTP response.
|
||||
var httpEndpointTypeName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
|
||||
var writeHttpResponseTypeName = ActivityTypeNameHelper.GenerateTypeName<WriteHttpResponse>();
|
||||
|
||||
var query =
|
||||
from triggeredWorkflow in triggerResult.TriggeredWorkflows
|
||||
from bookmark in triggeredWorkflow.Bookmarks
|
||||
where bookmark.Name == writeHttpResponseTypeName
|
||||
where bookmark.Name == writeHttpResponseTypeName || bookmark.Name == httpEndpointTypeName
|
||||
select (triggeredWorkflow.InstanceId, bookmark.Id);
|
||||
|
||||
var workflowExecutionResults = new Stack<(string InstanceId, string BookmarkId)>(query);
|
||||
|
|
@ -127,7 +128,7 @@ public class WorkflowsMiddleware
|
|||
cancellationToken);
|
||||
|
||||
var workflowHost = await _workflowHostFactory.CreateAsync(workflow, workflowState, cancellationToken);
|
||||
var options = new ResumeWorkflowHostOptions(correlationId, result.BookmarkId);
|
||||
var options = new ResumeWorkflowHostOptions(correlationId, result.BookmarkId, Input: input);
|
||||
await workflowHost.ResumeWorkflowAsync(options, cancellationToken);
|
||||
|
||||
// Import the updated workflow state into the runtime.
|
||||
|
|
|
|||
|
|
@ -44,7 +44,13 @@ public class LiquidTemplateManager : ILiquidTemplateManager
|
|||
e =>
|
||||
{
|
||||
if (!TryParse(source, out var parsed, out var error))
|
||||
{
|
||||
error = "{% raw %}\n" + error + "\n{% endraw %}";
|
||||
TryParse(error, out parsed, out error);
|
||||
|
||||
e.SetSlidingExpiration(TimeSpan.FromMilliseconds(100));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// TODO: add signal based cache invalidation.
|
||||
e.SetSlidingExpiration(TimeSpan.FromSeconds(30));
|
||||
|
|
|
|||
|
|
@ -31,11 +31,12 @@ public class VariableDefinitionMapper
|
|||
if (!_wellKnownTypeRegistry.TryGetTypeOrDefault(source.TypeName, out var type))
|
||||
return null;
|
||||
|
||||
var variableGenericType = typeof(Variable<>).MakeGenericType(type);
|
||||
var valueType = source.IsArray ? typeof(ICollection<>).MakeGenericType(type) : type;
|
||||
var variableGenericType = typeof(Variable<>).MakeGenericType(valueType);
|
||||
var variable = (Variable)Activator.CreateInstance(variableGenericType)!;
|
||||
|
||||
variable.Name = source.Name;
|
||||
variable.Value = source.Value.ConvertTo(type);
|
||||
variable.Value = source.Value.ConvertTo(valueType);
|
||||
variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : default;
|
||||
|
||||
return variable;
|
||||
|
|
@ -57,13 +58,16 @@ public class VariableDefinitionMapper
|
|||
public VariableDefinition Map(Variable source)
|
||||
{
|
||||
var variableType = source.GetType();
|
||||
var value = source.Value;
|
||||
var valueType = variableType.IsConstructedGenericType ? variableType.GetGenericArguments().FirstOrDefault() ?? typeof(object) : typeof(object);
|
||||
var valueTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(valueType);
|
||||
var isArray = valueType.IsGenericType && typeof(ICollection<>).IsAssignableFrom(valueType.GetGenericTypeDefinition());
|
||||
var elementValueType = isArray ? valueType.GenericTypeArguments[0] : valueType;
|
||||
var value = source.Value;
|
||||
|
||||
var valueTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(elementValueType);
|
||||
var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName();
|
||||
var serializedValue = value.Format();
|
||||
|
||||
return new VariableDefinition(source.Name, valueTypeAlias, serializedValue, storageDriverTypeName);
|
||||
return new VariableDefinition(source.Name, valueTypeAlias, isArray, serializedValue, storageDriverTypeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ namespace Elsa.Workflows.Management.Models;
|
|||
/// <summary>
|
||||
/// Stores information about a workflow variable.
|
||||
/// </summary>
|
||||
public record VariableDefinition(string Name, string TypeName, string? Value, string? StorageDriverTypeName);
|
||||
public record VariableDefinition(string Name, string TypeName, bool IsArray, string? Value, string? StorageDriverTypeName);
|
||||
Loading…
Reference in a new issue