alembic 0.1.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 +105 -0
- data/Rakefile +6 -0
- data/app/assets/builds/alembic/canvas.js +1 -0
- data/app/assets/builds/alembic/page_builder.css +1 -0
- data/app/assets/builds/alembic/page_builder.js +5 -0
- data/app/assets/stylesheets/alembic/application.css +15 -0
- data/app/controllers/alembic/application_controller.rb +32 -0
- data/app/controllers/alembic/flows_controller.rb +104 -0
- data/app/controllers/alembic/manage/base_controller.rb +16 -0
- data/app/controllers/alembic/manage/canvas_controller.rb +183 -0
- data/app/controllers/alembic/manage/definitions_controller.rb +15 -0
- data/app/controllers/alembic/manage/flows_controller.rb +55 -0
- data/app/controllers/alembic/manage/page_blocks_controller.rb +19 -0
- data/app/controllers/alembic/manage/pages_controller.rb +38 -0
- data/app/controllers/alembic/manage/previews_controller.rb +38 -0
- data/app/controllers/alembic/manage/versions_controller.rb +22 -0
- data/app/controllers/concerns/alembic/manage/authenticates_admin.rb +19 -0
- data/app/controllers/concerns/alembic/manage/draws_canvas.rb +35 -0
- data/app/helpers/alembic/application_helper.rb +11 -0
- data/app/javascript/alembic/canvas/Canvas.jsx +197 -0
- data/app/javascript/alembic/canvas/Connector.jsx +30 -0
- data/app/javascript/alembic/canvas/ConnectorLayer.jsx +31 -0
- data/app/javascript/alembic/canvas/Control.jsx +42 -0
- data/app/javascript/alembic/canvas/Inspector.jsx +44 -0
- data/app/javascript/alembic/canvas/Panel.jsx +67 -0
- data/app/javascript/alembic/canvas/Placeholder.jsx +24 -0
- data/app/javascript/alembic/canvas/Port.jsx +16 -0
- data/app/javascript/alembic/canvas/Records.jsx +34 -0
- data/app/javascript/alembic/canvas/StepCard.jsx +57 -0
- data/app/javascript/alembic/canvas/Toolbar.jsx +19 -0
- data/app/javascript/alembic/canvas/TypePicker.jsx +21 -0
- data/app/javascript/alembic/canvas/changes.js +5 -0
- data/app/javascript/alembic/canvas/choices.js +4 -0
- data/app/javascript/alembic/canvas/flow.js +7 -0
- data/app/javascript/alembic/canvas/flowSender.js +22 -0
- data/app/javascript/alembic/canvas/ids.js +7 -0
- data/app/javascript/alembic/canvas/rows.js +2 -0
- data/app/javascript/alembic/canvas/styles.js +3 -0
- data/app/javascript/alembic/canvas/useConnectors.js +75 -0
- data/app/javascript/alembic/canvas/useFlow.js +17 -0
- data/app/javascript/alembic/canvas.jsx +13 -0
- data/app/javascript/alembic/page_builder/PageBuilder.jsx +18 -0
- data/app/javascript/alembic/page_builder.jsx +10 -0
- data/app/jobs/alembic/application_job.rb +4 -0
- data/app/mailers/alembic/application_mailer.rb +6 -0
- data/app/models/alembic/application_record.rb +5 -0
- data/app/models/alembic/asked.rb +3 -0
- data/app/models/alembic/choice.rb +7 -0
- data/app/models/alembic/flow/admission.rb +19 -0
- data/app/models/alembic/flow/canvas.rb +167 -0
- data/app/models/alembic/flow/change.rb +18 -0
- data/app/models/alembic/flow/condition.rb +24 -0
- data/app/models/alembic/flow/definition.rb +132 -0
- data/app/models/alembic/flow/digest.rb +116 -0
- data/app/models/alembic/flow/document.rb +131 -0
- data/app/models/alembic/flow/drawing.rb +15 -0
- data/app/models/alembic/flow/edge.rb +9 -0
- data/app/models/alembic/flow/edit_history.rb +75 -0
- data/app/models/alembic/flow/layout.rb +88 -0
- data/app/models/alembic/flow/name.rb +15 -0
- data/app/models/alembic/flow/node.rb +5 -0
- data/app/models/alembic/flow/output.rb +23 -0
- data/app/models/alembic/flow/progress/kept.rb +38 -0
- data/app/models/alembic/flow/progress/loose.rb +43 -0
- data/app/models/alembic/flow/progress.rb +11 -0
- data/app/models/alembic/flow/registry.rb +25 -0
- data/app/models/alembic/flow/run.rb +54 -0
- data/app/models/alembic/flow/runner.rb +65 -0
- data/app/models/alembic/flow/settings.rb +80 -0
- data/app/models/alembic/flow/start.rb +13 -0
- data/app/models/alembic/flow/step.rb +42 -0
- data/app/models/alembic/flow/step_type/declaration.rb +118 -0
- data/app/models/alembic/flow/step_type.rb +69 -0
- data/app/models/alembic/flow/summaries.rb +44 -0
- data/app/models/alembic/flow/summary_version.rb +13 -0
- data/app/models/alembic/flow/switch.rb +17 -0
- data/app/models/alembic/flow/terminal.rb +13 -0
- data/app/models/alembic/flow/validator.rb +171 -0
- data/app/models/alembic/flow/version.rb +26 -0
- data/app/models/alembic/flow/violation.rb +9 -0
- data/app/models/alembic/flow.rb +44 -0
- data/app/models/alembic/outputs/band.rb +22 -0
- data/app/models/alembic/outputs/grouped.rb +26 -0
- data/app/models/alembic/outputs/lowest.rb +20 -0
- data/app/models/alembic/outputs/percentage.rb +31 -0
- data/app/models/alembic/outputs/tally.rb +23 -0
- data/app/models/alembic/outputs/weighted_sum.rb +26 -0
- data/app/models/alembic/page.rb +11 -0
- data/app/models/alembic/runner.rb +13 -0
- data/app/models/alembic/steps/question.rb +52 -0
- data/app/models/alembic/summary/output_type.rb +40 -0
- data/app/models/alembic/summary/registry.rb +25 -0
- data/app/models/alembic/summary/report.rb +31 -0
- data/app/models/alembic/summary/result.rb +5 -0
- data/app/models/alembic/summary/run.rb +13 -0
- data/app/models/alembic/summary.rb +15 -0
- data/app/views/alembic/flow/steps/_choosing.html.erb +9 -0
- data/app/views/alembic/flows/complete.html.erb +34 -0
- data/app/views/alembic/flows/show.html.erb +17 -0
- data/app/views/alembic/flows/step.html.erb +27 -0
- data/app/views/alembic/manage/definitions/edit.html.erb +13 -0
- data/app/views/alembic/manage/flows/edit.html.erb +29 -0
- data/app/views/alembic/manage/flows/index.html.erb +32 -0
- data/app/views/alembic/manage/flows/show.html.erb +26 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -0
- data/app/views/alembic/manage/pages/index.html.erb +20 -0
- data/app/views/alembic/manage/pages/show.html.erb +11 -0
- data/app/views/alembic/manage/versions/index.html.erb +41 -0
- data/app/views/layouts/alembic/application.html.erb +17 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20260604215031_create_alembic_diagnostics.rb +14 -0
- data/db/migrate/20260605015051_create_alembic_questions.rb +12 -0
- data/db/migrate/20260605021526_create_alembic_options.rb +14 -0
- data/db/migrate/20260605023308_create_alembic_conditions.rb +11 -0
- data/db/migrate/20260605025623_create_alembic_bands.rb +12 -0
- data/db/migrate/20260605032859_create_alembic_condition_options.rb +10 -0
- data/db/migrate/20260605032900_reshape_conditions_for_placement.rb +11 -0
- data/db/migrate/20260605125119_create_alembic_results.rb +14 -0
- data/db/migrate/20260605130338_create_alembic_rules.rb +10 -0
- data/db/migrate/20260605130339_create_alembic_rule_results.rb +10 -0
- data/db/migrate/20260605210714_add_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260606000855_add_guide_copy_to_alembic_diagnostics.rb +9 -0
- data/db/migrate/20260606005430_create_alembic_nodes.rb +22 -0
- data/db/migrate/20260606010312_create_alembic_build_steps.rb +12 -0
- data/db/migrate/20260606011110_create_alembic_warnings.rb +11 -0
- data/db/migrate/20260804180000_create_alembic_domains.rb +14 -0
- data/db/migrate/20260804180100_add_domain_to_alembic_questions.rb +5 -0
- data/db/migrate/20260822120000_create_alembic_definition_versions.rb +14 -0
- data/db/migrate/20260822120100_backfill_alembic_definition_versions.rb +17 -0
- data/db/migrate/20260822130000_remove_definition_from_alembic_diagnostics.rb +28 -0
- data/db/migrate/20260822140000_create_alembic_responses.rb +14 -0
- data/db/migrate/20260824120000_create_alembic_transitions.rb +10 -0
- data/db/migrate/20260824130000_drop_alembic_builder_tables.rb +17 -0
- data/db/migrate/20260824140000_add_definition_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824160000_add_summary_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824170000_drop_alembic_guide_copy_columns.rb +8 -0
- data/db/migrate/20260825120000_create_alembic_summary_versions.rb +14 -0
- data/db/migrate/20260825130000_add_summary_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825140000_move_summary_definitions_into_versions.rb +26 -0
- data/db/migrate/20260825150000_drop_summary_definition_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825160000_add_summary_version_to_alembic_responses.rb +6 -0
- data/db/migrate/20260825170000_rename_question_settings_in_current_definitions.rb +51 -0
- data/db/migrate/20260825180000_add_document_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825190000_seed_documents_from_current_definitions.rb +29 -0
- data/db/migrate/20260825200000_add_published_version_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825210000_publish_current_definitions.rb +29 -0
- data/db/migrate/20260825220000_add_undone_changes_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825223322_add_status_to_alembic_definition_versions.rb +8 -0
- data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825225015_change_alembic_diagnostic_status_to_availability.rb +16 -0
- data/db/migrate/20260825230000_add_undo_history_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825230500_add_changes_to_alembic_definition_versions.rb +5 -0
- data/db/migrate/20260826230000_rename_alembic_responses_to_flow_runs.rb +5 -0
- data/db/migrate/20260826234500_rename_answers_to_recorded_on_flow_runs.rb +5 -0
- data/db/migrate/20260826235000_rename_definition_versions_to_flow_versions.rb +5 -0
- data/db/migrate/20260826235500_rename_summary_versions_to_flow_summaries.rb +5 -0
- data/db/migrate/20260827000000_rename_diagnostics_to_flows.rb +8 -0
- data/db/migrate/20260831000000_add_persists_to_alembic_flows.rb +5 -0
- data/db/migrate/20260914120000_create_alembic_pages.rb +9 -0
- data/db/migrate/20260915120000_add_blocks_to_alembic_pages.rb +5 -0
- data/lib/alembic/engine.rb +43 -0
- data/lib/alembic/leftover_stylesheet.rb +14 -0
- data/lib/alembic/version.rb +3 -0
- data/lib/alembic.rb +34 -0
- data/lib/tasks/alembic_tasks.rake +4 -0
- metadata +252 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Canvas
|
|
4
|
+
def initialize(document, registry: Flow.registry)
|
|
5
|
+
@document = document
|
|
6
|
+
@registry = registry
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to_h
|
|
10
|
+
{ "nodes" => nodes, "edges" => edges, "palette" => palette, "violations" => violations }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def nodes
|
|
16
|
+
@document.nodes.map { |node| drawn_step(node) } + waiting.map { |gap| drawn_placeholder(gap) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def drawn_step(node)
|
|
20
|
+
{ "id" => node.id, "type" => node.type, "label" => label_for(node),
|
|
21
|
+
"config" => node.config, "ports" => [], "ends_here" => ends_here?(node), "begins_here" => begins_here?(node),
|
|
22
|
+
"loose" => loose?(node), "choices" => choices_for(node), "placeholder" => false, **placed[node.id] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def drawn_placeholder(gap)
|
|
26
|
+
{ "id" => gap[:id], "type" => "placeholder", "label" => gap[:on], "config" => {}, "ports" => [],
|
|
27
|
+
"ends_here" => false, "begins_here" => false, "loose" => false, "choices" => {},
|
|
28
|
+
"placeholder" => true, "from" => gap[:from], "on" => gap[:on], **placed[gap[:id]] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def edges
|
|
32
|
+
drawn.edges.each_with_index.map do |edge, index|
|
|
33
|
+
{ "id" => "#{edge.from}-#{edge.to}-#{index}", "source" => edge.from, "target" => edge.to, "label" => edge.on&.to_s,
|
|
34
|
+
"placeholder" => waiting.any? { |gap| gap[:id] == edge.to } }.merge(routing(edge, placed))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def placed
|
|
39
|
+
@placed ||= Layout.new(drawn).positions
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def drawn
|
|
43
|
+
@drawn ||= Document.new(@document.to_h.merge(
|
|
44
|
+
"nodes" => Array(@document.to_h["nodes"]) + waiting.map { |gap| { "id" => gap[:id], "type" => "placeholder" } },
|
|
45
|
+
"edges" => Array(@document.to_h["edges"]) + waiting.map { |gap| { "from" => gap[:from], "to" => gap[:id], "on" => gap[:on] } }
|
|
46
|
+
), registry: @registry)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def waiting
|
|
50
|
+
@waiting ||= @document.nodes.flat_map { |node| gaps_after(node) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def gaps_after(node)
|
|
54
|
+
wired = @document.edges_from(node.id).map { |edge| edge.on.to_s }
|
|
55
|
+
results = digest.routing_values(node.id)
|
|
56
|
+
return [ { id: "#{node.id}--", from: node.id, on: nil } ] if results.empty? && leads_nowhere?(node)
|
|
57
|
+
|
|
58
|
+
results.reject { |value| wired.include?(value) }
|
|
59
|
+
.map { |value| { id: "#{node.id}--#{value}", from: node.id, on: value } }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def leads_nowhere?(node)
|
|
63
|
+
!ends_here?(node) && !loose?(node) && @document.edges_from(node.id).none?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def routing(edge, placed)
|
|
67
|
+
from = placed[edge.from]
|
|
68
|
+
to = placed[edge.to]
|
|
69
|
+
return { "leaves" => "bottom", "enters" => "top", "route" => "straight" } unless from && to
|
|
70
|
+
|
|
71
|
+
return alongside_routing(from, to) unless to["row"] > from["row"]
|
|
72
|
+
|
|
73
|
+
{ "leaves" => "bottom", "enters" => "top",
|
|
74
|
+
"route" => to["column"] == from["column"] ? "straight" : "lane" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def alongside_routing(from, to)
|
|
78
|
+
leftward = to["column"] < from["column"]
|
|
79
|
+
|
|
80
|
+
{ "leaves" => leftward ? "left" : "right", "enters" => leftward ? "right" : "left",
|
|
81
|
+
"route" => to["row"] == from["row"] ? "straight" : "detour" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def palette
|
|
85
|
+
@registry.step_types.reject(&:begins_here?).map do |step_type|
|
|
86
|
+
{ "type" => step_type.id.to_s, "label" => step_type.step_name,
|
|
87
|
+
"fields" => step_type.settings.fields.transform_keys(&:to_s).transform_values(&:to_s),
|
|
88
|
+
"labels" => step_type.settings.labels.transform_keys(&:to_s),
|
|
89
|
+
"choices" => step_type.settings.choices.transform_keys(&:to_s),
|
|
90
|
+
"records" => holdings_of(step_type),
|
|
91
|
+
"record_labels" => step_type.settings.record_labels.to_h { |name, held| [ name.to_s, held.transform_keys(&:to_s) ] },
|
|
92
|
+
"awaits_input" => step_type.awaits_input? }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def holdings_of(step_type)
|
|
97
|
+
step_type.settings.record_fields.to_h do |name, holds|
|
|
98
|
+
[ name.to_s, holds.transform_keys(&:to_s).transform_values(&:to_s) ]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def violations
|
|
103
|
+
Validator.new(@document, registry: @registry).violations.map do |violation|
|
|
104
|
+
{ "node" => violation.node, "problem" => violation.problem.to_s, "detail" => violation.detail }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def label_for(node)
|
|
109
|
+
Name.of(node, @registry)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def choices_for(node)
|
|
113
|
+
naming_steps(node).index_with { earlier_than(node) }.merge(drawn_by(node)).merge(named_outputs_by(node))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def named_outputs_by(node)
|
|
117
|
+
step_type_for(node)&.settings&.outputs_of.to_h.to_h do |name, source|
|
|
118
|
+
[ name.to_s, digest.outputs_of(node.config[source.to_s]) ]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def drawn_by(node)
|
|
123
|
+
step_type_for(node)&.settings&.drawn_from.to_h.to_h do |name, source|
|
|
124
|
+
[ name.to_s, digest.values_of(step_named_by(node, source), node.config[source.to_s]) ]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def step_named_by(node, source)
|
|
129
|
+
node.config[step_type_for(node).settings.outputs_of[source].to_s]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def digest
|
|
133
|
+
Digest.new(@document, registry: @registry)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def naming_steps(node)
|
|
137
|
+
step_type_for(node)&.settings&.naming_steps.to_a.map(&:to_s)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def earlier_than(node)
|
|
141
|
+
digest.preceding(node.id).map do |id|
|
|
142
|
+
{ "value" => id, "label" => label_for(@document.node(id)) }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ends_here?(node)
|
|
147
|
+
step_type_for(node)&.ends_here? || false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def loose?(node)
|
|
151
|
+
@document.entry.present? && @document.reachable.exclude?(node.id)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def begins_here?(node)
|
|
155
|
+
step_type_for(node)&.begins_here? || false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ports_for(node)
|
|
159
|
+
digest.routing_values(node.id)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def step_type_for(node)
|
|
163
|
+
@registry.fetch(node.type) if @registry.registered?(node.type)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module Change
|
|
4
|
+
PHRASING = { "added" => "Added", "updated" => "Updated", "removed" => "Removed",
|
|
5
|
+
"moved" => "Moved", "connected" => "Connected", "disconnected" => "Disconnected", "returned" => "Returned to" }.freeze
|
|
6
|
+
|
|
7
|
+
def self.phrase(change)
|
|
8
|
+
named = Array(change["named"]).compact_blank.map { |name| "“#{name}”" }
|
|
9
|
+
verb = PHRASING.fetch(change["action"], change["action"])
|
|
10
|
+
|
|
11
|
+
return "#{verb} #{named.join(' → ')}" if named.any?
|
|
12
|
+
return "#{verb} #{change['detail']}" if change["detail"].present?
|
|
13
|
+
|
|
14
|
+
verb
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Condition
|
|
4
|
+
include Step
|
|
5
|
+
|
|
6
|
+
step_name "Condition"
|
|
7
|
+
|
|
8
|
+
setting :step, type: :previous_step
|
|
9
|
+
setting :output, outputs_of: :step, required: true
|
|
10
|
+
setting :comparison, type: :select, options: [ "is", "is not" ], required: true
|
|
11
|
+
setting :answer, from: :output, required: true
|
|
12
|
+
|
|
13
|
+
output :result, type: :boolean, values: [ true, false ]
|
|
14
|
+
|
|
15
|
+
names_by { |node| node.config.values_at("step", "comparison", "answer").compact_blank.join(" ").presence }
|
|
16
|
+
|
|
17
|
+
def route(node, state)
|
|
18
|
+
matched = state[node.config["step"]] == node.config["answer"]
|
|
19
|
+
|
|
20
|
+
node.config["comparison"] == "is not" ? !matched : matched
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Definition < ApplicationRecord
|
|
4
|
+
self.table_name = "alembic_flows"
|
|
5
|
+
|
|
6
|
+
enum :status, { active: "active", hidden: "hidden", inactive: "inactive" }
|
|
7
|
+
enum :kind, { scored: "scored", guide: "guide" }
|
|
8
|
+
enum :persists, { unsaved: "unsaved", each_step: "each_step", on_finish: "on_finish" }
|
|
9
|
+
|
|
10
|
+
validates :slug, presence: true
|
|
11
|
+
|
|
12
|
+
after_initialize :begin_the_flow, if: :new_record?
|
|
13
|
+
|
|
14
|
+
scope :listable, -> { active }
|
|
15
|
+
|
|
16
|
+
# Declared before :definition_versions so responses clear first, otherwise
|
|
17
|
+
# destroying a diagnostic trips the responses -> definition_versions FK.
|
|
18
|
+
has_many :runs, class_name: "Alembic::Flow::Run", foreign_key: :flow_id, dependent: :destroy
|
|
19
|
+
has_many :definition_versions, class_name: "Alembic::Flow::Version", foreign_key: :flow_id, dependent: :destroy
|
|
20
|
+
has_many :summary_versions, class_name: "Alembic::Flow::SummaryVersion", foreign_key: :flow_id, dependent: :destroy
|
|
21
|
+
|
|
22
|
+
def self.upsert_definition(definition)
|
|
23
|
+
find_or_initialize_by(slug: definition["slug"]).tap do |diagnostic|
|
|
24
|
+
diagnostic.save!
|
|
25
|
+
diagnostic.record_definition(definition) unless diagnostic.definition == definition
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def definition
|
|
30
|
+
current_definition_version&.definition
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def current_definition_version
|
|
34
|
+
definition_versions.find_by(number: cursor)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def edit_history
|
|
38
|
+
@edit_history ||= EditHistory.new(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def summaries
|
|
42
|
+
@summaries ||= Summaries.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def publish
|
|
46
|
+
create_version
|
|
47
|
+
|
|
48
|
+
publish_version(current_definition_version)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def return_to(version)
|
|
52
|
+
raise ActiveRecord::RecordNotFound unless definition_versions.exists?(version.id)
|
|
53
|
+
raise OutOfService if version.out_of_service?
|
|
54
|
+
|
|
55
|
+
update!(document: version.definition, undone_changes: [],
|
|
56
|
+
changes_since_version: changes_since_version.to_a + [ returning_to(version) ])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create_version
|
|
60
|
+
record_definition(document, changes_since_version.to_a) unless versioned?
|
|
61
|
+
|
|
62
|
+
update!(undo_history: edit_history.undoable, changes_since_version: [], undone_changes: [])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def returning_to(version)
|
|
66
|
+
{ "action" => "returned", "steps" => [], "named" => [],
|
|
67
|
+
"detail" => "version #{version.number}", "before" => document }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def versioned?
|
|
71
|
+
document == definition
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def record_definition(payload, captured = [])
|
|
75
|
+
definition_versions.create!(number: next_definition_number, definition: payload,
|
|
76
|
+
changes_captured: captured.map { |change| change.except("before") })
|
|
77
|
+
.tap { |version| update!(definition_cursor: version.number, document: payload) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def publish_version(version)
|
|
81
|
+
raise OutOfService if version.out_of_service?
|
|
82
|
+
return if live_version == version
|
|
83
|
+
|
|
84
|
+
live_version&.update!(status: :superseded)
|
|
85
|
+
version.update!(status: :live)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def available?
|
|
89
|
+
!inactive?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def retire_version(version)
|
|
93
|
+
version.update!(status: :retired)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def withdraw_version(version)
|
|
97
|
+
version.update!(status: :withdrawn)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def live_version
|
|
101
|
+
definition_versions.find_by(status: :live)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def live_definition
|
|
105
|
+
live_version&.definition
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def runner
|
|
109
|
+
Runner.new(live_definition)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def begin_the_flow
|
|
115
|
+
self.document ||= { "nodes" => [ { "id" => "start", "type" => "start" }, { "id" => "end", "type" => "terminal" } ],
|
|
116
|
+
"edges" => [ { "from" => "start", "to" => "end" } ] }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cursor
|
|
120
|
+
definition_cursor || recorded_numbers.max
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def recorded_numbers
|
|
124
|
+
definition_versions.pluck(:number)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def next_definition_number
|
|
128
|
+
(definition_versions.maximum(:number) || 0) + 1
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Digest
|
|
4
|
+
def initialize(document, registry: Flow.registry)
|
|
5
|
+
@document = document
|
|
6
|
+
@registry = registry
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def entry
|
|
10
|
+
@document.node(@document.entry)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def step(id)
|
|
14
|
+
@document.node(id)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def steps
|
|
18
|
+
@document.nodes
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def preceding(id)
|
|
22
|
+
return [] unless @document.reachable.include?(id)
|
|
23
|
+
|
|
24
|
+
@document.nodes.map(&:id).uniq
|
|
25
|
+
.reject { |other| other == id || @document.reachable(without: other).include?(id) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def routing_values(id)
|
|
29
|
+
named = step(id)
|
|
30
|
+
return [] unless named && step_type(named)&.routes?
|
|
31
|
+
|
|
32
|
+
step_type(named).outputs.flat_map { |output| values_taken(output, named) }.map { |value| value["value"].to_s }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def values_taken(output, node)
|
|
36
|
+
return output.values_for(node) unless output.from
|
|
37
|
+
|
|
38
|
+
values_out_of(node.config[output.from.to_s])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def values_of(id, output_name)
|
|
42
|
+
named = step(id)
|
|
43
|
+
return [] unless named && output_name.present?
|
|
44
|
+
|
|
45
|
+
step_type(named)&.values_of(output_name, named).to_a
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def outputs_of(id)
|
|
49
|
+
named = step(id)
|
|
50
|
+
return [] unless named
|
|
51
|
+
|
|
52
|
+
step_type(named)&.outputs.to_a.map { |output| { "value" => output.name.to_s, "label" => output.label } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def values_out_of(id)
|
|
56
|
+
named = step(id)
|
|
57
|
+
return [] unless named
|
|
58
|
+
|
|
59
|
+
step_type(named)&.outputs.to_a.flat_map { |output| output.values_for(named) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def requirements(id)
|
|
63
|
+
node = step(id)
|
|
64
|
+
step_type(node)&.settings&.requirements_for(node.config).to_a
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def next_step(state)
|
|
68
|
+
walk(state).last
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def state_on_path(state)
|
|
72
|
+
state.slice(*walk(state).first)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def walk(state)
|
|
78
|
+
recorded = []
|
|
79
|
+
visited = []
|
|
80
|
+
cursor = entry
|
|
81
|
+
|
|
82
|
+
while cursor && !visited.include?(cursor.id)
|
|
83
|
+
return [ recorded, cursor ] if pending?(cursor, state)
|
|
84
|
+
|
|
85
|
+
recorded << cursor.id if state.key?(cursor.id)
|
|
86
|
+
visited << cursor.id
|
|
87
|
+
cursor = successor(cursor, state)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
[ recorded, nil ]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def pending?(node, state)
|
|
94
|
+
return false if state.key?(node.id)
|
|
95
|
+
|
|
96
|
+
step_type(node)&.awaits_input? || acts?(node)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def acts?(node)
|
|
100
|
+
step_type(node)&.acts? || false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def successor(node, state)
|
|
104
|
+
leaving = @document.edges_from(node.id)
|
|
105
|
+
port = step_type(node)&.route(node, state)
|
|
106
|
+
taken = port.nil? ? leaving.first : leaving.find { |edge| edge.on.to_s == port.to_s }
|
|
107
|
+
|
|
108
|
+
taken && @document.node(taken.to)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def step_type(node)
|
|
112
|
+
@registry.fetch(node.type) if node && @registry.registered?(node.type)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Document
|
|
4
|
+
def initialize(document, registry: Flow.registry)
|
|
5
|
+
@document = document
|
|
6
|
+
@registry = registry
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def nodes
|
|
10
|
+
Array(@document["nodes"]).map { |node| Node.new(id: node["id"], type: node["type"], config: node.except("id", "type")) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def edges
|
|
14
|
+
Array(@document["edges"]).map { |edge| Edge.new(from: edge["from"], to: edge["to"], on: edge["on"]) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def entry
|
|
18
|
+
beginnings.first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def beginnings
|
|
22
|
+
nodes.select { |node| begins_here?(node) }.map(&:id)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def begins_here?(node)
|
|
26
|
+
@registry.registered?(node.type) && @registry.fetch(node.type).begins_here?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def node(id)
|
|
30
|
+
nodes.find { |node| node.id == id }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def edges_from(id)
|
|
34
|
+
edges.select { |edge| edge.from == id }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reachable(without: nil)
|
|
38
|
+
found = []
|
|
39
|
+
frontier = [ entry ]
|
|
40
|
+
|
|
41
|
+
while (id = frontier.shift)
|
|
42
|
+
next if found.include?(id) || id == without
|
|
43
|
+
|
|
44
|
+
found << id
|
|
45
|
+
frontier.concat(edges_from(id).map(&:to))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
found
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
@document
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def insert(node, on:, leaving: nil)
|
|
56
|
+
source, target = on
|
|
57
|
+
replaced = raw_edges.find { |edge| edge["from"] == source && edge["to"] == target }
|
|
58
|
+
bridged = [ { "from" => source, "to" => node["id"], "on" => replaced&.fetch("on", nil) }.compact,
|
|
59
|
+
{ "from" => node["id"], "to" => target, "on" => leaving }.compact ]
|
|
60
|
+
|
|
61
|
+
with(nodes: raw_nodes + [ node ], edges: (raw_edges - [ replaced ]) + bridged)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add(node)
|
|
65
|
+
with(nodes: raw_nodes + [ node ], edges: raw_edges)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def connect(from:, to:, on: nil)
|
|
69
|
+
with(nodes: raw_nodes, edges: raw_edges + [ { "from" => from, "to" => to, "on" => on }.compact ])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def disconnect(from:, to:)
|
|
73
|
+
with(nodes: raw_nodes, edges: raw_edges.reject { |edge| edge["from"] == from && edge["to"] == to })
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def move(id, on:, leaving: nil)
|
|
77
|
+
step = raw_nodes.find { |node| node["id"] == id }
|
|
78
|
+
return self unless step
|
|
79
|
+
|
|
80
|
+
remove(id).insert(step, on: on, leaving: leaving)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def configure(id, config)
|
|
84
|
+
reconfigured = raw_nodes.map do |node|
|
|
85
|
+
node["id"] == id ? node.slice("id", "type").merge(config) : node
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
with(nodes: reconfigured, edges: raw_edges)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def rewire(from:, to:, target:)
|
|
92
|
+
rewritten = raw_edges.map do |edge|
|
|
93
|
+
edge["from"] == from && edge["to"] == to ? edge.merge("to" => target) : edge
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
with(nodes: raw_nodes, edges: rewritten)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def remove(id)
|
|
100
|
+
incoming = raw_edges.select { |edge| edge["to"] == id }
|
|
101
|
+
outgoing = raw_edges.select { |edge| edge["from"] == id }
|
|
102
|
+
|
|
103
|
+
with(nodes: raw_nodes.reject { |node| node["id"] == id },
|
|
104
|
+
edges: (raw_edges - incoming - outgoing) + bridges(incoming, outgoing))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def bridges(incoming, outgoing)
|
|
110
|
+
incoming.product(outgoing).map do |arriving, leaving|
|
|
111
|
+
{ "from" => arriving["from"], "to" => leaving["to"], "on" => arriving["on"] }.compact
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def raw_nodes
|
|
116
|
+
Array(@document["nodes"])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def raw_edges
|
|
120
|
+
Array(@document["edges"])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def with(nodes:, edges:)
|
|
124
|
+
Document.new(@document.merge("nodes" => nodes, "edges" => edges), registry: @registry).tap do |edited|
|
|
125
|
+
broken = Validator.new(edited, registry: @registry).malformations
|
|
126
|
+
raise InvalidEdit, broken.map { |violation| "#{violation.node}: #{violation.problem}" }.join(", ") if broken.any?
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module Drawing
|
|
4
|
+
def self.of(node, registry = Flow.registry)
|
|
5
|
+
named(node, registry).presence || Flow.drawing
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.named(node, registry)
|
|
9
|
+
return unless node && registry.registered?(node.type)
|
|
10
|
+
|
|
11
|
+
registry.fetch(node.type).drawn_by
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class EditHistory
|
|
4
|
+
def initialize(flow)
|
|
5
|
+
@flow = flow
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def undoable?
|
|
9
|
+
undoable.any?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def redoable?
|
|
13
|
+
undone.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def undoable
|
|
17
|
+
@flow.undo_history.to_a + @flow.changes_since_version.to_a
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def undo_change
|
|
21
|
+
undone_change = undoable.last
|
|
22
|
+
return unless undone_change
|
|
23
|
+
|
|
24
|
+
@flow.update!(document: undone_change["before"],
|
|
25
|
+
undone_changes: undone + [ undone_change.merge("after" => @flow.document) ],
|
|
26
|
+
undo_history: @flow.undo_history.to_a[0...-1],
|
|
27
|
+
changes_since_version: @flow.changes_since_version.to_a[0...-1])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def redo_change
|
|
31
|
+
redone = undone.last
|
|
32
|
+
return unless redone
|
|
33
|
+
|
|
34
|
+
@flow.update!(document: redone["after"], undone_changes: undone[0...-1],
|
|
35
|
+
changes_since_version: @flow.changes_since_version.to_a + [ redone.except("after") ])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def record(edited, action, steps, before)
|
|
39
|
+
@flow.update!(document: edited.to_h, undone_changes: [],
|
|
40
|
+
changes_since_version: with(edited, action, steps, before))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def edit_document(payload)
|
|
44
|
+
@flow.update!(document: payload, undone_changes: [],
|
|
45
|
+
changes_since_version: @flow.changes_since_version.to_a + [ edited_by_hand ])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def with(edited, action, steps, before)
|
|
51
|
+
return @flow.changes_since_version.to_a unless action
|
|
52
|
+
|
|
53
|
+
@flow.changes_since_version.to_a +
|
|
54
|
+
[ { "action" => action.to_s, "steps" => steps.map(&:to_s),
|
|
55
|
+
"named" => steps.map { |id| named(edited, id) }, "before" => before } ]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def named(edited, id)
|
|
59
|
+
Name.of(edited.node(id.to_s) || document.node(id.to_s))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def document
|
|
63
|
+
Document.new(@flow.document || @flow.definition || {})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def undone
|
|
67
|
+
@flow.undone_changes.to_a
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def edited_by_hand
|
|
71
|
+
{ "action" => "edited", "steps" => [], "named" => [ "the definition" ], "before" => @flow.document }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|