orkestr 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +832 -0
- data/Rakefile +6 -0
- data/app/assets/builds/orkestr/orkestr-editor.js +72 -0
- data/app/assets/stylesheets/orkestr/application.css +15 -0
- data/app/assets/stylesheets/orkestr/theme.css +62 -0
- data/app/controllers/orkestr/api/base_controller.rb +45 -0
- data/app/controllers/orkestr/api/executions_controller.rb +50 -0
- data/app/controllers/orkestr/api/human_tasks_controller.rb +48 -0
- data/app/controllers/orkestr/api/registry_controller.rb +40 -0
- data/app/controllers/orkestr/api/workflows_controller.rb +53 -0
- data/app/controllers/orkestr/application_controller.rb +4 -0
- data/app/controllers/orkestr/human_tasks_controller.rb +30 -0
- data/app/controllers/orkestr/ui_controller.rb +8 -0
- data/app/controllers/orkestr/webhooks_controller.rb +35 -0
- data/app/helpers/orkestr/application_helper.rb +4 -0
- data/app/helpers/orkestr/ui_helper.rb +34 -0
- data/app/javascript/orkestr-ui/index.html +19 -0
- data/app/javascript/orkestr-ui/package-lock.json +2050 -0
- data/app/javascript/orkestr-ui/package.json +23 -0
- data/app/javascript/orkestr-ui/src/OrkestrApp.tsx +152 -0
- data/app/javascript/orkestr-ui/src/api/client.ts +59 -0
- data/app/javascript/orkestr-ui/src/api/executions.ts +23 -0
- data/app/javascript/orkestr-ui/src/api/humanTasks.ts +32 -0
- data/app/javascript/orkestr-ui/src/api/index.ts +5 -0
- data/app/javascript/orkestr-ui/src/api/registry.ts +10 -0
- data/app/javascript/orkestr-ui/src/api/workflows.ts +33 -0
- data/app/javascript/orkestr-ui/src/components/Editor/ActionsBuilder.tsx +213 -0
- data/app/javascript/orkestr-ui/src/components/Editor/CustomNode.tsx +31 -0
- data/app/javascript/orkestr-ui/src/components/Editor/EditorToolbar.tsx +153 -0
- data/app/javascript/orkestr-ui/src/components/Editor/FormSchemaBuilder.tsx +390 -0
- data/app/javascript/orkestr-ui/src/components/Editor/NodeConfigPanel.tsx +274 -0
- data/app/javascript/orkestr-ui/src/components/Editor/NodePalette.tsx +43 -0
- data/app/javascript/orkestr-ui/src/components/Editor/RunDialog.tsx +52 -0
- data/app/javascript/orkestr-ui/src/components/Editor/WorkflowEditor.tsx +299 -0
- data/app/javascript/orkestr-ui/src/components/Executions/ExecutionDetail.tsx +155 -0
- data/app/javascript/orkestr-ui/src/components/Executions/ExecutionList.tsx +74 -0
- data/app/javascript/orkestr-ui/src/components/HumanTasks/TaskForm.tsx +216 -0
- data/app/javascript/orkestr-ui/src/components/HumanTasks/TaskFormEmbed.tsx +117 -0
- data/app/javascript/orkestr-ui/src/components/HumanTasks/TaskList.tsx +110 -0
- data/app/javascript/orkestr-ui/src/components/Workflows/NewWorkflowDialog.tsx +64 -0
- data/app/javascript/orkestr-ui/src/components/Workflows/WorkflowList.tsx +94 -0
- data/app/javascript/orkestr-ui/src/components/shared/EntryConditionsEditor.tsx +138 -0
- data/app/javascript/orkestr-ui/src/components/shared/ExpressionEditor.tsx +206 -0
- data/app/javascript/orkestr-ui/src/components/shared/HumanTaskFormRenderer.tsx +321 -0
- data/app/javascript/orkestr-ui/src/components/shared/JsonSchemaForm.tsx +376 -0
- data/app/javascript/orkestr-ui/src/components/shared/Loading.tsx +5 -0
- data/app/javascript/orkestr-ui/src/components/shared/StatusBadge.tsx +9 -0
- data/app/javascript/orkestr-ui/src/fieldRegistry.ts +74 -0
- data/app/javascript/orkestr-ui/src/hooks/useApi.ts +30 -0
- data/app/javascript/orkestr-ui/src/hooks/useRegistry.ts +35 -0
- data/app/javascript/orkestr-ui/src/main.tsx +75 -0
- data/app/javascript/orkestr-ui/src/styles/editor.css +445 -0
- data/app/javascript/orkestr-ui/src/styles/index.css +478 -0
- data/app/javascript/orkestr-ui/src/types/execution.ts +37 -0
- data/app/javascript/orkestr-ui/src/types/humanTask.ts +30 -0
- data/app/javascript/orkestr-ui/src/types/index.ts +4 -0
- data/app/javascript/orkestr-ui/src/types/registry.ts +22 -0
- data/app/javascript/orkestr-ui/src/types/workflow.ts +64 -0
- data/app/javascript/orkestr-ui/src/vite-env.d.ts +6 -0
- data/app/javascript/orkestr-ui/tsconfig.json +21 -0
- data/app/javascript/orkestr-ui/tsconfig.tsbuildinfo +1 -0
- data/app/javascript/orkestr-ui/vite.config.ts +30 -0
- data/app/jobs/orkestr/application_job.rb +4 -0
- data/app/jobs/orkestr/execute_workflow_job.rb +10 -0
- data/app/jobs/orkestr/resume_execution_job.rb +15 -0
- data/app/mailers/orkestr/application_mailer.rb +6 -0
- data/app/models/concerns/orkestr/assignable.rb +28 -0
- data/app/models/concerns/orkestr/contextualizable.rb +9 -0
- data/app/models/orkestr/application_record.rb +6 -0
- data/app/models/orkestr/assignee.rb +40 -0
- data/app/models/orkestr/context.rb +42 -0
- data/app/models/orkestr/edge.rb +58 -0
- data/app/models/orkestr/execution.rb +45 -0
- data/app/models/orkestr/execution_log.rb +38 -0
- data/app/models/orkestr/human_task.rb +63 -0
- data/app/models/orkestr/node.rb +48 -0
- data/app/models/orkestr/node_execution.rb +59 -0
- data/app/models/orkestr/workflow.rb +39 -0
- data/app/orkestr_nodes/action/node.rb +77 -0
- data/app/orkestr_nodes/condition/node.rb +67 -0
- data/app/orkestr_nodes/http_request/node.rb +88 -0
- data/app/orkestr_nodes/human_action/node.rb +103 -0
- data/app/orkestr_nodes/transform/node.rb +48 -0
- data/app/orkestr_nodes/wait/node.rb +18 -0
- data/app/orkestr_triggers/manual/trigger.rb +12 -0
- data/app/orkestr_triggers/scheduled/trigger.rb +26 -0
- data/app/orkestr_triggers/webhook/trigger.rb +23 -0
- data/app/serializers/orkestr/assignee_serializer.rb +30 -0
- data/app/serializers/orkestr/context_serializer.rb +30 -0
- data/app/serializers/orkestr/edge_serializer.rb +33 -0
- data/app/serializers/orkestr/execution_collection_serializer.rb +7 -0
- data/app/serializers/orkestr/execution_log_serializer.rb +31 -0
- data/app/serializers/orkestr/execution_serializer.rb +37 -0
- data/app/serializers/orkestr/human_task_serializer.rb +46 -0
- data/app/serializers/orkestr/node_execution_serializer.rb +43 -0
- data/app/serializers/orkestr/node_serializer.rb +29 -0
- data/app/serializers/orkestr/workflow_collection_serializer.rb +11 -0
- data/app/serializers/orkestr/workflow_serializer.rb +30 -0
- data/app/services/orkestr/entry_condition_evaluator.rb +68 -0
- data/app/services/orkestr/execution_service/complete.rb +64 -0
- data/app/services/orkestr/execution_service/join_resolver.rb +56 -0
- data/app/services/orkestr/execution_service/node_runner.rb +56 -0
- data/app/services/orkestr/execution_service/runner.rb +162 -0
- data/app/services/orkestr/execution_service/start.rb +90 -0
- data/app/services/orkestr/expression_resolver.rb +72 -0
- data/app/services/orkestr/human_task_service/complete.rb +62 -0
- data/app/services/orkestr/workflow_service/duplicate.rb +30 -0
- data/app/services/orkestr/workflow_service/export.rb +26 -0
- data/app/services/orkestr/workflow_service/import.rb +29 -0
- data/app/services/orkestr/workflow_synchronizer.rb +102 -0
- data/app/views/layouts/orkestr/application.html.erb +17 -0
- data/app/views/layouts/orkestr/ui.html.erb +18 -0
- data/app/views/orkestr/human_tasks/show.html.erb +17 -0
- data/app/views/orkestr/ui/index.html.erb +8 -0
- data/config/routes.rb +27 -0
- data/db/migrate/20260308204133_enable_pgcrypto_extension.rb +5 -0
- data/db/migrate/20260308204558_create_orkestr_workflows.rb +12 -0
- data/db/migrate/20260308204703_create_orkestr_nodes.rb +12 -0
- data/db/migrate/20260308204807_create_orkestr_edges.rb +12 -0
- data/db/migrate/20260308204931_create_orkestr_executions.rb +13 -0
- data/db/migrate/20260308205023_create_orkestr_node_executions.rb +16 -0
- data/db/migrate/20260308205119_add_react_flow_id_to_nodes.rb +6 -0
- data/db/migrate/20260308205123_add_react_flow_id_to_edges.rb +6 -0
- data/db/migrate/20260308205745_add_workflow_to_executions.rb +5 -0
- data/db/migrate/20260308205940_make_executable_nullable_on_executions.rb +6 -0
- data/db/migrate/20260308220730_create_orkestr_human_tasks.rb +15 -0
- data/db/migrate/20260308220900_create_orkestr_assignees.rb +13 -0
- data/db/migrate/20260308234115_add_unique_index_to_node_executions.rb +7 -0
- data/db/migrate/20260309075336_create_orkestr_contexts.rb +10 -0
- data/db/migrate/20260309075343_replace_executable_with_context_on_executions.rb +11 -0
- data/db/migrate/20260309080416_add_status_key_deadline_to_orkestr_assignees.rb +7 -0
- data/db/migrate/20260309082815_add_status_and_workflow_to_orkestr_contexts.rb +7 -0
- data/db/migrate/20260309082816_add_status_default_context_and_reuse_context_to_orkestr_workflows.rb +7 -0
- data/db/migrate/20260309083328_create_orkestr_execution_logs.rb +14 -0
- data/db/migrate/20260310223204_replace_node_executions_unique_index_for_cycle_support.rb +18 -0
- data/lib/orkestr/configuration.rb +16 -0
- data/lib/orkestr/engine.rb +48 -0
- data/lib/orkestr/nodes/base.rb +74 -0
- data/lib/orkestr/nodes/loader.rb +32 -0
- data/lib/orkestr/nodes/registry.rb +34 -0
- data/lib/orkestr/nodes/schema_dsl.rb +73 -0
- data/lib/orkestr/triggers/base.rb +49 -0
- data/lib/orkestr/triggers/loader.rb +29 -0
- data/lib/orkestr/triggers/registry.rb +34 -0
- data/lib/orkestr/triggers/schema_dsl.rb +45 -0
- data/lib/orkestr/version.rb +3 -0
- data/lib/orkestr.rb +27 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/lib/tasks/orkestr_tasks.rake +19 -0
- metadata +251 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Orkestr
|
|
2
|
+
module WorkflowService
|
|
3
|
+
class Import
|
|
4
|
+
attr_reader :data
|
|
5
|
+
|
|
6
|
+
def initialize(data)
|
|
7
|
+
@data = data.deep_symbolize_keys
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
Workflow.transaction do
|
|
12
|
+
workflow = Workflow.create!(
|
|
13
|
+
name: data[:name] || "Imported workflow",
|
|
14
|
+
status: "draft",
|
|
15
|
+
trigger_type: data[:trigger_type],
|
|
16
|
+
trigger_config: data[:trigger_config] || {},
|
|
17
|
+
default_context: data[:default_context] || {},
|
|
18
|
+
reuse_context: data[:reuse_context] || false,
|
|
19
|
+
graph_json: data[:graph_json] || {}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
WorkflowSynchronizer.new(workflow).call if workflow.graph_json.present?
|
|
23
|
+
|
|
24
|
+
workflow
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module Orkestr
|
|
2
|
+
class WorkflowSynchronizer
|
|
3
|
+
attr_reader :workflow
|
|
4
|
+
|
|
5
|
+
def initialize(workflow)
|
|
6
|
+
@workflow = workflow
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
graph = workflow.graph
|
|
11
|
+
flow_nodes = graph[:nodes] || []
|
|
12
|
+
flow_edges = graph[:edges] || []
|
|
13
|
+
|
|
14
|
+
ActiveRecord::Base.transaction do
|
|
15
|
+
remove_stale_edges(flow_edges)
|
|
16
|
+
remove_stale_nodes(flow_nodes)
|
|
17
|
+
upsert_nodes(flow_nodes)
|
|
18
|
+
upsert_edges(flow_edges)
|
|
19
|
+
detect_cycles! unless Orkestr.configuration.allow_cycles
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def remove_stale_edges(flow_edges)
|
|
26
|
+
flow_ids = flow_edges.map { |e| e[:id].to_s }
|
|
27
|
+
workflow.edges.where.not(react_flow_id: flow_ids).destroy_all
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def remove_stale_nodes(flow_nodes)
|
|
31
|
+
flow_ids = flow_nodes.map { |n| n[:id].to_s }
|
|
32
|
+
workflow.nodes.where.not(react_flow_id: flow_ids).destroy_all
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fields that are internal to the React Flow UI and should not be persisted in config_json
|
|
36
|
+
UI_INTERNAL_KEYS = %i[type label executionStatus].freeze
|
|
37
|
+
|
|
38
|
+
def upsert_nodes(flow_nodes)
|
|
39
|
+
flow_nodes.each do |flow_node|
|
|
40
|
+
data = (flow_node[:data] || {}).deep_dup
|
|
41
|
+
conditions = data.delete(:conditions) || {}
|
|
42
|
+
UI_INTERNAL_KEYS.each { |key| data.delete(key) }
|
|
43
|
+
node = workflow.nodes.find_or_initialize_by(react_flow_id: flow_node[:id].to_s)
|
|
44
|
+
node.update!(
|
|
45
|
+
node_type: flow_node[:type] || flow_node.dig(:data, :type) || "default",
|
|
46
|
+
config_json: data,
|
|
47
|
+
conditions_json: conditions
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def upsert_edges(flow_edges)
|
|
53
|
+
flow_edges.each do |flow_edge|
|
|
54
|
+
source = workflow.nodes.find_by!(react_flow_id: flow_edge[:source].to_s)
|
|
55
|
+
target = workflow.nodes.find_by!(react_flow_id: flow_edge[:target].to_s)
|
|
56
|
+
|
|
57
|
+
edge = workflow.edges.find_or_initialize_by(react_flow_id: flow_edge[:id].to_s)
|
|
58
|
+
edge.update!(
|
|
59
|
+
source_node: source,
|
|
60
|
+
target_node: target,
|
|
61
|
+
source_handle: flow_edge[:sourceHandle]
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def detect_cycles!
|
|
67
|
+
nodes = workflow.nodes.reload.index_by(&:id)
|
|
68
|
+
adjacency = Hash.new { |h, k| h[k] = [] }
|
|
69
|
+
|
|
70
|
+
workflow.edges.reload.each do |edge|
|
|
71
|
+
adjacency[edge.source_node_id] << edge.target_node_id
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
visited = Set.new
|
|
75
|
+
in_stack = Set.new
|
|
76
|
+
|
|
77
|
+
nodes.each_key do |node_id|
|
|
78
|
+
next if visited.include?(node_id)
|
|
79
|
+
|
|
80
|
+
if dfs_has_cycle?(node_id, adjacency, visited, in_stack)
|
|
81
|
+
raise Orkestr::CycleDetectedError, "Workflow graph contains a cycle"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def dfs_has_cycle?(node_id, adjacency, visited, in_stack)
|
|
87
|
+
visited.add(node_id)
|
|
88
|
+
in_stack.add(node_id)
|
|
89
|
+
|
|
90
|
+
adjacency[node_id].each do |neighbor|
|
|
91
|
+
if in_stack.include?(neighbor)
|
|
92
|
+
return true
|
|
93
|
+
elsif !visited.include?(neighbor)
|
|
94
|
+
return true if dfs_has_cycle?(neighbor, adjacency, visited, in_stack)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
in_stack.delete(node_id)
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Orkestr</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "orkestr/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Orkestr</title>
|
|
7
|
+
<style>html, body { margin: 0; padding: 0; height: 100%; }</style>
|
|
8
|
+
<% Orkestr.configuration.custom_ui_styles.each do |style_path| %>
|
|
9
|
+
<%= stylesheet_link_tag style_path %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<%= yield %>
|
|
14
|
+
<% Orkestr.configuration.custom_ui_scripts.each do |script_path| %>
|
|
15
|
+
<%= javascript_include_tag script_path %>
|
|
16
|
+
<% end %>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# Standalone task form page — uses the web component.
|
|
2
|
+
Host apps can also embed <orkestr-editor mode="task" task-id="...">
|
|
3
|
+
inside their own containers (modals, panels, etc.) %>
|
|
4
|
+
<%= javascript_include_tag "orkestr/orkestr-editor", defer: true %>
|
|
5
|
+
|
|
6
|
+
<div style="max-width:40rem;margin:2rem auto;padding:0 1rem;">
|
|
7
|
+
<h2 style="font-size:1.25rem;font-weight:600;margin-bottom:1rem;">
|
|
8
|
+
<%= @task.node&.config_json&.dig("description") || @task.node&.config_json&.dig("label") || "Complete Task" %>
|
|
9
|
+
</h2>
|
|
10
|
+
|
|
11
|
+
<orkestr-editor
|
|
12
|
+
api-base-url="<%= request.path.sub(%r{/human_tasks.*$}, "") %>"
|
|
13
|
+
mode="task"
|
|
14
|
+
task-id="<%= @task.id %>"
|
|
15
|
+
style="display:block;min-height:200px;">
|
|
16
|
+
</orkestr-editor>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%= javascript_include_tag "orkestr/orkestr-editor", defer: true %>
|
|
2
|
+
|
|
3
|
+
<% api_base = request.path.sub(%r{/ui.*$}, "") %>
|
|
4
|
+
<orkestr-editor
|
|
5
|
+
api-base-url="<%= api_base %>"
|
|
6
|
+
mode="dashboard"
|
|
7
|
+
style="display: block; width: 100%; height: 100vh;">
|
|
8
|
+
</orkestr-editor>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Orkestr::Engine.routes.draw do
|
|
2
|
+
post "webhooks/:workflow_id", to: "webhooks#create", as: :webhook
|
|
3
|
+
|
|
4
|
+
namespace :api do
|
|
5
|
+
resources :workflows, only: %i[index show create update destroy] do
|
|
6
|
+
resources :executions, only: %i[index create]
|
|
7
|
+
end
|
|
8
|
+
resources :executions, only: %i[show]
|
|
9
|
+
resources :human_tasks, only: %i[index show] do
|
|
10
|
+
member do
|
|
11
|
+
put :complete
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
get "registry/nodes", to: "registry#nodes"
|
|
15
|
+
get "registry/triggers", to: "registry#triggers"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# HTML endpoints for embedding human task forms in host apps (turbo frame compatible)
|
|
19
|
+
resources :human_tasks, only: %i[show] do
|
|
20
|
+
member do
|
|
21
|
+
put :complete
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
get "ui", to: "ui#index"
|
|
26
|
+
get "ui/*path", to: "ui#index"
|
|
27
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateOrkestrWorkflows < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_workflows, id: :uuid do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.json :graph_json, null: false, default: {}
|
|
6
|
+
t.string :trigger_type
|
|
7
|
+
t.json :trigger_config, null: false, default: {}
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateOrkestrNodes < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_nodes, id: :uuid do |t|
|
|
4
|
+
t.references :workflow, null: false, foreign_key: { to_table: :orkestr_workflows }, type: :uuid
|
|
5
|
+
t.string :node_type, null: false
|
|
6
|
+
t.json :config_json, null: false, default: {}
|
|
7
|
+
t.json :conditions_json, null: false, default: {}
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateOrkestrEdges < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_edges, id: :uuid do |t|
|
|
4
|
+
t.references :workflow, null: false, foreign_key: { to_table: :orkestr_workflows }, type: :uuid
|
|
5
|
+
t.references :source_node, null: false, foreign_key: { to_table: :orkestr_nodes }, type: :uuid
|
|
6
|
+
t.references :target_node, null: false, foreign_key: { to_table: :orkestr_nodes }, type: :uuid
|
|
7
|
+
t.string :source_handle
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateOrkestrExecutions < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_executions, id: :uuid do |t|
|
|
4
|
+
t.references :executable, polymorphic: true, null: true, type: :uuid
|
|
5
|
+
t.json :context_json, null: false, default: {}
|
|
6
|
+
t.string :status, null: false, default: "pending"
|
|
7
|
+
t.datetime :started_at
|
|
8
|
+
t.datetime :finished_at
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateOrkestrNodeExecutions < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_node_executions, id: :uuid do |t|
|
|
4
|
+
t.references :execution, null: false, foreign_key: { to_table: :orkestr_executions }, type: :uuid
|
|
5
|
+
t.references :node, null: false, foreign_key: { to_table: :orkestr_nodes }, type: :uuid
|
|
6
|
+
t.string :status, null: false, default: "pending"
|
|
7
|
+
t.json :input_json, null: false, default: {}
|
|
8
|
+
t.json :output_json, null: false, default: {}
|
|
9
|
+
t.datetime :started_at
|
|
10
|
+
t.datetime :finished_at
|
|
11
|
+
t.integer :attempt, null: false, default: 1
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateOrkestrHumanTasks < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_human_tasks, id: :uuid do |t|
|
|
4
|
+
t.references :node_execution, null: false, foreign_key: { to_table: :orkestr_node_executions }, type: :uuid
|
|
5
|
+
t.references :execution, null: false, foreign_key: { to_table: :orkestr_executions }, type: :uuid
|
|
6
|
+
t.references :node, null: false, foreign_key: { to_table: :orkestr_nodes }, type: :uuid
|
|
7
|
+
t.json :schema_json, null: false, default: {}
|
|
8
|
+
t.json :response_json
|
|
9
|
+
t.string :status, null: false, default: "pending"
|
|
10
|
+
t.datetime :completed_at
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateOrkestrAssignees < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_assignees, id: :uuid do |t|
|
|
4
|
+
t.references :human_task, null: false, foreign_key: { to_table: :orkestr_human_tasks }, type: :uuid
|
|
5
|
+
t.string :assignable_type, null: false
|
|
6
|
+
t.string :assignable_id, null: false
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :orkestr_assignees, %i[assignable_type assignable_id]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class CreateOrkestrContexts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_contexts, id: :uuid do |t|
|
|
4
|
+
t.references :contextualizable, polymorphic: true, null: true, type: :uuid
|
|
5
|
+
t.jsonb :data, null: false, default: {}
|
|
6
|
+
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class ReplaceExecutableWithContextOnExecutions < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_reference :orkestr_executions, :context, null: true, type: :uuid,
|
|
4
|
+
foreign_key: { to_table: :orkestr_contexts }
|
|
5
|
+
|
|
6
|
+
remove_index :orkestr_executions, name: :index_orkestr_executions_on_executable
|
|
7
|
+
remove_column :orkestr_executions, :executable_type, :string
|
|
8
|
+
remove_column :orkestr_executions, :executable_id, :uuid
|
|
9
|
+
remove_column :orkestr_executions, :context_json, :json, null: false, default: {}
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddStatusKeyDeadlineToOrkestrAssignees < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :orkestr_assignees, :key, :string
|
|
4
|
+
add_column :orkestr_assignees, :status, :string, null: false, default: "pending"
|
|
5
|
+
add_column :orkestr_assignees, :deadline, :datetime
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddStatusAndWorkflowToOrkestrContexts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :orkestr_contexts, :status, :string, null: false, default: "pending"
|
|
4
|
+
add_reference :orkestr_contexts, :workflow, type: :uuid, null: true,
|
|
5
|
+
foreign_key: { to_table: :orkestr_workflows }
|
|
6
|
+
end
|
|
7
|
+
end
|
data/db/migrate/20260309082816_add_status_default_context_and_reuse_context_to_orkestr_workflows.rb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddStatusDefaultContextAndReuseContextToOrkestrWorkflows < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :orkestr_workflows, :status, :string, null: false, default: "draft"
|
|
4
|
+
add_column :orkestr_workflows, :default_context, :jsonb, null: false, default: {}
|
|
5
|
+
add_column :orkestr_workflows, :reuse_context, :boolean, null: false, default: false
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateOrkestrExecutionLogs < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :orkestr_execution_logs, id: :uuid do |t|
|
|
4
|
+
t.references :execution, null: false, type: :uuid,
|
|
5
|
+
foreign_key: { to_table: :orkestr_executions }
|
|
6
|
+
t.references :node_execution, null: true, type: :uuid,
|
|
7
|
+
foreign_key: { to_table: :orkestr_node_executions }
|
|
8
|
+
t.string :level, null: false, default: "info"
|
|
9
|
+
t.text :message, null: false
|
|
10
|
+
t.jsonb :metadata, null: false, default: {}
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class ReplaceNodeExecutionsUniqueIndexForCycleSupport < ActiveRecord::Migration[8.0]
|
|
2
|
+
def up
|
|
3
|
+
remove_index :orkestr_node_executions, name: "index_orkestr_node_executions_unique_per_execution"
|
|
4
|
+
|
|
5
|
+
add_index :orkestr_node_executions, [:execution_id, :node_id],
|
|
6
|
+
unique: true,
|
|
7
|
+
where: "status IN ('pending', 'running', 'waiting')",
|
|
8
|
+
name: "index_orkestr_node_executions_unique_active"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def down
|
|
12
|
+
remove_index :orkestr_node_executions, name: "index_orkestr_node_executions_unique_active"
|
|
13
|
+
|
|
14
|
+
add_index :orkestr_node_executions, [:execution_id, :node_id],
|
|
15
|
+
unique: true,
|
|
16
|
+
name: "index_orkestr_node_executions_unique_per_execution"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Orkestr
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :nodes_paths, :triggers_paths, :authenticator, :authorizer, :custom_ui_scripts, :custom_ui_styles,
|
|
4
|
+
:allow_cycles
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@nodes_paths = []
|
|
8
|
+
@triggers_paths = []
|
|
9
|
+
@authenticator = nil
|
|
10
|
+
@authorizer = nil
|
|
11
|
+
@custom_ui_scripts = []
|
|
12
|
+
@custom_ui_styles = []
|
|
13
|
+
@allow_cycles = false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Orkestr
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace Orkestr
|
|
4
|
+
|
|
5
|
+
config.generators do |g|
|
|
6
|
+
g.orm :active_record, primary_key_type: :uuid
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Register app/assets/builds so Propshaft finds orkestr/orkestr-editor.js
|
|
10
|
+
config.paths["app/assets"] << "app/assets/builds"
|
|
11
|
+
|
|
12
|
+
initializer "orkestr.assets", before: :append_assets_path do |app|
|
|
13
|
+
builds_path = root.join("app/assets/builds")
|
|
14
|
+
app.config.assets.paths.unshift(builds_path) if builds_path.exist?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer "orkestr.append_migrations" do |app|
|
|
18
|
+
unless app.root.to_s.match?(root.to_s)
|
|
19
|
+
config.paths["db/migrate"].expanded.each do |path|
|
|
20
|
+
app.config.paths["db/migrate"] << path
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Exclude plugin directories from Zeitwerk autoloading.
|
|
26
|
+
# They use a custom loader (Orkestr::Nodes::Loader / Orkestr::Triggers::Loader).
|
|
27
|
+
initializer "orkestr.ignore_plugin_paths", before: :set_autoload_paths do
|
|
28
|
+
Rails.autoloaders.main.ignore(
|
|
29
|
+
root.join("app/orkestr_nodes"),
|
|
30
|
+
root.join("app/orkestr_triggers")
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
initializer "orkestr.helpers" do
|
|
35
|
+
ActiveSupport.on_load(:action_view) do
|
|
36
|
+
require_relative "../../app/helpers/orkestr/ui_helper"
|
|
37
|
+
include Orkestr::UiHelper
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
initializer "orkestr.load_plugins" do
|
|
42
|
+
config.after_initialize do
|
|
43
|
+
Orkestr::Nodes::Loader.load_all!
|
|
44
|
+
Orkestr::Triggers::Loader.load_all!
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require_relative "schema_dsl"
|
|
2
|
+
require_relative "registry"
|
|
3
|
+
|
|
4
|
+
module Orkestr
|
|
5
|
+
module Nodes
|
|
6
|
+
class Base
|
|
7
|
+
include SchemaDsl
|
|
8
|
+
|
|
9
|
+
def self.inherited(subclass)
|
|
10
|
+
super
|
|
11
|
+
TracePoint.new(:end) do |tp|
|
|
12
|
+
if tp.self == subclass
|
|
13
|
+
tp.disable
|
|
14
|
+
Registry.register(subclass) if subclass.node_id
|
|
15
|
+
end
|
|
16
|
+
end.enable
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :node_execution
|
|
20
|
+
|
|
21
|
+
def initialize(node_execution, resolved_config: nil)
|
|
22
|
+
@node_execution = node_execution
|
|
23
|
+
@resolved_config = resolved_config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute(_context, _input)
|
|
27
|
+
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def config
|
|
31
|
+
@resolved_config || node_execution.node.config
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def execution
|
|
35
|
+
node_execution.execution
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def workflow_context
|
|
39
|
+
execution.context_data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def log(message, level: :info, metadata: {})
|
|
43
|
+
ExecutionLog.create!(
|
|
44
|
+
execution: execution,
|
|
45
|
+
node_execution: node_execution,
|
|
46
|
+
level: level.to_s,
|
|
47
|
+
message: message,
|
|
48
|
+
metadata: metadata
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
def dig_value(data, path)
|
|
55
|
+
return nil if path.blank?
|
|
56
|
+
|
|
57
|
+
keys = path.to_s.split(".")
|
|
58
|
+
keys.reduce(data) do |obj, key|
|
|
59
|
+
case obj
|
|
60
|
+
when Hash then obj[key.to_sym] || obj[key.to_s]
|
|
61
|
+
else nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def interpolate(template, data)
|
|
67
|
+
template.gsub(/\{\{(\s*[\w.]+\s*)\}\}/) do
|
|
68
|
+
path = ::Regexp.last_match(1).strip
|
|
69
|
+
dig_value(data, path).to_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Orkestr
|
|
2
|
+
module Nodes
|
|
3
|
+
module Loader
|
|
4
|
+
class << self
|
|
5
|
+
def load_all!
|
|
6
|
+
search_paths.each do |path|
|
|
7
|
+
load_from(path)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def search_paths
|
|
14
|
+
paths = []
|
|
15
|
+
# Engine built-in nodes
|
|
16
|
+
paths << Orkestr::Engine.root.join("app", "orkestr_nodes")
|
|
17
|
+
# Configured custom paths
|
|
18
|
+
Orkestr.configuration.nodes_paths.each { |p| paths << Pathname.new(p) }
|
|
19
|
+
# Host app nodes
|
|
20
|
+
paths << Rails.root.join("app", "orkestr_nodes") if defined?(Rails)
|
|
21
|
+
paths.select(&:exist?).uniq
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load_from(base_path)
|
|
25
|
+
Dir[base_path.join("**/node.rb")].sort.each do |file|
|
|
26
|
+
require file
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Orkestr
|
|
2
|
+
module Nodes
|
|
3
|
+
module Registry
|
|
4
|
+
class << self
|
|
5
|
+
def register(node_class)
|
|
6
|
+
node_class.validate_node_id!
|
|
7
|
+
registry[node_class.node_id] = node_class
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find(id)
|
|
11
|
+
registry[id]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all
|
|
15
|
+
registry.values
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ids
|
|
19
|
+
registry.keys
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset!
|
|
23
|
+
@registry = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def registry
|
|
29
|
+
@registry ||= {}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|