diff --git a/app/graphql/types/flow_setting_type.rb b/app/graphql/types/flow_setting_type.rb index 24c9c83c..2ec38f88 100644 --- a/app/graphql/types/flow_setting_type.rb +++ b/app/graphql/types/flow_setting_type.rb @@ -6,6 +6,10 @@ class FlowSettingType < Types::BaseObject authorize :read_flow + field :cast, String, + null: true, + description: 'The cast applied to the flow setting' + field :flow_setting_identifier, String, null: false, method: :flow_setting_id, diff --git a/app/graphql/types/flow_sub_flow_setting_type.rb b/app/graphql/types/flow_sub_flow_setting_type.rb new file mode 100644 index 00000000..bc80d8f9 --- /dev/null +++ b/app/graphql/types/flow_sub_flow_setting_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + class FlowSubFlowSettingType < Types::BaseObject + description 'Represents a sub-flow setting.' + + field :default_value, GraphQL::Types::JSON, + null: true, + description: 'The default value of the sub-flow setting.' + field :hidden, Boolean, + null: true, + description: 'Whether the sub-flow setting is hidden.' + field :identifier, String, + null: false, + description: 'The identifier of the sub-flow setting.' + field :optional, Boolean, + null: true, + description: 'Whether the sub-flow setting is optional.' + end +end diff --git a/app/graphql/types/flow_sub_flow_type.rb b/app/graphql/types/flow_sub_flow_type.rb new file mode 100644 index 00000000..d0a8c016 --- /dev/null +++ b/app/graphql/types/flow_sub_flow_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + class FlowSubFlowType < Types::BaseObject + description 'Represents a sub-flow parameter value.' + + field :function_definition, Types::FunctionDefinitionType, + null: true, + description: 'The resolved function definition to execute.' + field :settings, [Types::FlowSubFlowSettingType], + method: :sub_flow_settings, + null: false, + description: 'The sub-flow settings.' + field :signature, String, + null: false, + description: 'The sub-flow signature.' + field :starting_node_id, GlobalIdType[::NodeFunction], + null: true, + description: 'The starting node to execute.' + + def starting_node_id + object.starting_node&.to_global_id + end + end +end diff --git a/app/graphql/types/input/flow_setting_input_type.rb b/app/graphql/types/input/flow_setting_input_type.rb index 259fc0d0..8f743df1 100644 --- a/app/graphql/types/input/flow_setting_input_type.rb +++ b/app/graphql/types/input/flow_setting_input_type.rb @@ -5,6 +5,9 @@ module Input class FlowSettingInputType < Types::BaseInputObject description 'Input type for flow settings' + argument :cast, String, + required: false, + description: 'The cast applied to the flow setting' argument :value, GraphQL::Types::JSON, required: true, description: 'The value of the flow setting' end diff --git a/app/graphql/types/input/flow_sub_flow_input_type.rb b/app/graphql/types/input/flow_sub_flow_input_type.rb new file mode 100644 index 00000000..43a82670 --- /dev/null +++ b/app/graphql/types/input/flow_sub_flow_input_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + module Input + class FlowSubFlowInputType < Types::BaseInputObject + description 'Input type for sub-flow parameter values' + + argument :function_identifier, String, + required: false, + description: 'The function identifier to execute' + argument :settings, [Types::Input::FlowSubFlowSettingInputType], + required: false, + description: 'The sub-flow settings' + argument :signature, String, + required: true, + description: 'The sub-flow signature' + argument :starting_node_id, Types::GlobalIdType[::NodeFunction], + required: false, + description: 'The starting node to execute' + + require_one_of %i[starting_node_id function_identifier] + end + end +end diff --git a/app/graphql/types/input/flow_sub_flow_setting_input_type.rb b/app/graphql/types/input/flow_sub_flow_setting_input_type.rb new file mode 100644 index 00000000..14cbe64d --- /dev/null +++ b/app/graphql/types/input/flow_sub_flow_setting_input_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module Input + class FlowSubFlowSettingInputType < Types::BaseInputObject + description 'Input type for sub-flow settings' + + argument :default_value, GraphQL::Types::JSON, + required: false, + description: 'The default value of the sub-flow setting' + argument :hidden, Boolean, + required: false, + description: 'Whether the sub-flow setting is hidden' + argument :identifier, String, + required: true, + description: 'The identifier of the sub-flow setting' + argument :optional, Boolean, + required: false, + description: 'Whether the sub-flow setting is optional' + end + end +end diff --git a/app/graphql/types/input/node_parameter_input_type.rb b/app/graphql/types/input/node_parameter_input_type.rb index 003b24b6..f12ed698 100644 --- a/app/graphql/types/input/node_parameter_input_type.rb +++ b/app/graphql/types/input/node_parameter_input_type.rb @@ -5,6 +5,9 @@ module Input class NodeParameterInputType < Types::BaseInputObject description 'Input type for Node parameter' + argument :cast, String, + required: false, + description: 'The cast applied to the parameter' argument :value, Types::Input::NodeParameterValueInputType, required: true, description: 'The value of the parameter' end diff --git a/app/graphql/types/input/node_parameter_value_input_type.rb b/app/graphql/types/input/node_parameter_value_input_type.rb index 027f5f1d..59cd1157 100644 --- a/app/graphql/types/input/node_parameter_value_input_type.rb +++ b/app/graphql/types/input/node_parameter_value_input_type.rb @@ -7,12 +7,12 @@ class NodeParameterValueInputType < Types::BaseInputObject argument :literal_value, GraphQL::Types::JSON, required: false, description: 'The literal value of the parameter' - argument :node_function_id, Types::GlobalIdType[::NodeFunction], - required: false, description: 'The function value of the parameter as an id' argument :reference_value, Types::Input::ReferenceValueInputType, required: false, description: 'The reference value of the parameter' + argument :sub_flow, Types::Input::FlowSubFlowInputType, + required: false, description: 'The sub-flow value of the parameter' - require_one_of %i[node_function_id literal_value reference_value] + require_one_of %i[literal_value reference_value sub_flow] end end end diff --git a/app/graphql/types/node_function_id_wrapper_type.rb b/app/graphql/types/node_function_id_wrapper_type.rb deleted file mode 100644 index 5db7a54a..00000000 --- a/app/graphql/types/node_function_id_wrapper_type.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Types - class NodeFunctionIdWrapperType < Types::BaseObject - description 'Represents a Node Function id wrapper.' - - id_field NodeFunction - end -end diff --git a/app/graphql/types/node_parameter_type.rb b/app/graphql/types/node_parameter_type.rb index 8159daf4..fb22e3ee 100644 --- a/app/graphql/types/node_parameter_type.rb +++ b/app/graphql/types/node_parameter_type.rb @@ -6,6 +6,7 @@ class NodeParameterType < Types::BaseObject authorize :read_flow + field :cast, String, null: true, description: 'The cast applied to the parameter' field :parameter_definition, Types::ParameterDefinitionType, null: false, description: 'The definition of the parameter' field :value, Types::NodeParameterValueType, null: true, description: 'The value of the parameter' @@ -13,8 +14,8 @@ class NodeParameterType < Types::BaseObject def value if object.reference_value.present? object.reference_value - elsif object.function_value.present? - object.function_value + elsif object.sub_flow.present? + object.sub_flow else object.literal_value end diff --git a/app/graphql/types/node_parameter_value_type.rb b/app/graphql/types/node_parameter_value_type.rb index 33b28cb2..aa6f83da 100644 --- a/app/graphql/types/node_parameter_value_type.rb +++ b/app/graphql/types/node_parameter_value_type.rb @@ -4,15 +4,15 @@ module Types class NodeParameterValueType < Types::BaseUnion description 'Represents a parameter value for a node.' - possible_types Types::LiteralValueType, Types::ReferenceValueType, Types::NodeFunctionIdWrapperType, - description: 'The value can be a literal, a reference, or a node function id.' + possible_types Types::FlowSubFlowType, Types::LiteralValueType, Types::ReferenceValueType, + description: 'The value can be a literal, a reference, or a sub-flow.' def self.resolve_type(object, _context) case object when ReferenceValue Types::ReferenceValueType - when NodeFunction - Types::NodeFunctionIdWrapperType + when SubFlow + Types::FlowSubFlowType else Types::LiteralValueType end diff --git a/app/models/flow_setting.rb b/app/models/flow_setting.rb index e543fb1a..2c9556d7 100644 --- a/app/models/flow_setting.rb +++ b/app/models/flow_setting.rb @@ -9,7 +9,8 @@ def to_grpc Tucana::Shared::FlowSetting.new( database_id: id, flow_setting_id: flow_setting_id, - value: Tucana::Shared::Value.from_ruby(object) + value: Tucana::Shared::Value.from_ruby(object), + cast: cast ) end end diff --git a/app/models/node_function.rb b/app/models/node_function.rb index 4b2ccda8..01452ff0 100644 --- a/app/models/node_function.rb +++ b/app/models/node_function.rb @@ -5,11 +5,6 @@ class NodeFunction < ApplicationRecord belongs_to :next_node, class_name: 'NodeFunction', optional: true belongs_to :flow, class_name: 'Flow' - belongs_to :value_of_node_parameter, - class_name: 'NodeParameter', - inverse_of: :function_value, - optional: true - has_one :previous_node, class_name: 'NodeFunction', foreign_key: :next_node_id, diff --git a/app/models/node_parameter.rb b/app/models/node_parameter.rb index 48a873a0..2ea75c13 100644 --- a/app/models/node_parameter.rb +++ b/app/models/node_parameter.rb @@ -5,22 +5,23 @@ class NodeParameter < ApplicationRecord belongs_to :node_function, class_name: 'NodeFunction', inverse_of: :node_parameters has_one :reference_value, autosave: true - has_one :function_value, class_name: 'NodeFunction', inverse_of: :value_of_node_parameter + has_one :sub_flow, autosave: true validate :only_one_value_present def to_grpc param = Tucana::Shared::NodeParameter.new( database_id: id, - runtime_parameter_id: parameter_definition.runtime_parameter_definition.runtime_name + runtime_parameter_id: parameter_definition.runtime_parameter_definition.runtime_name, + cast: cast ) param.value = Tucana::Shared::NodeValue.new(literal_value: Tucana::Shared::Value.from_ruby({})) if reference_value.present? param.value.reference_value = reference_value.to_grpc - elsif function_value.present? - param.value.node_function_id = function_value.id + elsif sub_flow.present? + param.value.sub_flow = sub_flow.to_grpc else param.value.literal_value = Tucana::Shared::Value.from_ruby(literal_value) end @@ -31,9 +32,9 @@ def to_grpc private def only_one_value_present - values = [!literal_value.nil?, reference_value.present?, function_value.present?] + values = [!literal_value.nil?, reference_value.present?, sub_flow.present?] return if values.count(true) <= 1 - errors.add(:value, 'Only one of literal_value, reference_value, or function_value must be present') + errors.add(:value, 'Only one of literal_value, reference_value, or sub_flow must be present') end end diff --git a/app/models/sub_flow.rb b/app/models/sub_flow.rb new file mode 100644 index 00000000..12d4b974 --- /dev/null +++ b/app/models/sub_flow.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class SubFlow < ApplicationRecord + belongs_to :node_parameter, inverse_of: :sub_flow + belongs_to :starting_node, class_name: 'NodeFunction', optional: true + belongs_to :function_definition, optional: true + + has_many :sub_flow_settings, inverse_of: :sub_flow, autosave: true + + validate :validate_execution_reference + + def function_identifier + function_definition&.identifier + end + + def to_grpc + Tucana::Shared::SubFlow.new( + starting_node_id: starting_node_id, + function_identifier: function_identifier, + signature: signature, + settings: sub_flow_settings.map(&:to_grpc) + ) + end + + private + + def validate_execution_reference + return if [starting_node.present?, function_definition.present?].count(true) == 1 + + errors.add(:base, 'Exactly one of starting_node or function_definition must be present') + end +end diff --git a/app/models/sub_flow_setting.rb b/app/models/sub_flow_setting.rb new file mode 100644 index 00000000..15659a27 --- /dev/null +++ b/app/models/sub_flow_setting.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class SubFlowSetting < ApplicationRecord + belongs_to :sub_flow, inverse_of: :sub_flow_settings + + validates :identifier, presence: true + + def to_grpc + Tucana::Shared::SubFlowSetting.new( + identifier: identifier, + default_value: Tucana::Shared::Value.from_ruby(default_value), + optional: optional, + hidden: hidden + ) + end +end diff --git a/app/services/error_code.rb b/app/services/error_code.rb index c94cd5db..d86a0987 100644 --- a/app/services/error_code.rb +++ b/app/services/error_code.rb @@ -88,7 +88,6 @@ def self.error_codes cyclic_data_type_reference: { description: 'A data type dependency cycle was detected' }, invalid_data_type_link: { description: 'The data type link is invalid because of active model errors' }, node_not_found: { description: 'The node with this id does not exist' }, - function_value_not_found: { description: 'The id for the function value node does not exist' }, invalid_node_parameter: { description: 'The node parameter is invalid' }, invalid_node_function: { description: 'The node function is invalid' }, invalid_runtime_status: { description: 'The runtime status is invalid because of active model errors' }, diff --git a/app/services/namespaces/projects/flows/update_service.rb b/app/services/namespaces/projects/flows/update_service.rb index 0c1f021c..bed2c683 100644 --- a/app/services/namespaces/projects/flows/update_service.rb +++ b/app/services/namespaces/projects/flows/update_service.rb @@ -53,13 +53,15 @@ def update_flow_attributes end def update_settings(t) - db_settings = flow.flow_settings.first(flow_input.settings.length) + settings_input = Array(flow_input.settings) + db_settings = flow.flow_settings.first(settings_input.length) flow_type_settings = flow.flow_type.flow_type_settings - flow_input.settings.each_with_index do |setting, index| + settings_input.each_with_index do |setting, index| db_settings[index] ||= flow.flow_settings.build db_settings[index].flow_setting_id = flow_type_settings[index]&.identifier db_settings[index].object = setting.value + db_settings[index].cast = setting.try(:cast) next if db_settings[index].save @@ -75,8 +77,6 @@ def update_settings(t) def update_nodes(t) all_nodes = flow.node_functions - - flow_input.starting_node_id node_index = 0 updated_nodes = [] @@ -181,22 +181,15 @@ def update_node_parameters(t, current_node, current_node_input, all_nodes) end db_parameters[index].parameter_definition = parameter_definition + db_parameters[index].cast = parameter.try(:cast) db_parameters[index].literal_value = parameter.value.literal_value - if parameter.value.node_function_id.present? - node = all_nodes.find { |n| n[:input].id == parameter.value.node_function_id } - - if node.nil? - t.rollback_and_return! ServiceResponse.error( - message: 'Invalid function value for parameter', - error_code: :function_value_not_found - ) - end - - db_parameters[index].function_value = node[:node] + if parameter.value.try(:sub_flow).present? + update_sub_flow(t, db_parameters[index], parameter.value.sub_flow, all_nodes) else - db_parameters[index].function_value = nil + db_parameters[index].sub_flow&.destroy + db_parameters[index].sub_flow = nil end if parameter.value.reference_value.present? @@ -246,13 +239,6 @@ def update_node_parameters(t, current_node, current_node_input, all_nodes) ) end - removed_parameters = current_node.node_parameters - db_parameters - # rubocop:disable Rails/SkipsModelValidations -- must nullify FK before parameter destruction to prevent cascade - flow.node_functions - .where(value_of_node_parameter: removed_parameters) - .update_all(value_of_node_parameter_id: nil) - # rubocop:enable Rails/SkipsModelValidations - current_node.node_parameters = db_parameters end @@ -267,6 +253,57 @@ def create_audit_event } ) end + + def update_sub_flow(t, node_parameter, sub_flow_input, all_nodes) + starting_node = nil + function_definition = nil + + if sub_flow_input.starting_node_id.present? + starting_node_reference = all_nodes.find { |n| n[:input].id == sub_flow_input.starting_node_id } + + if starting_node_reference.nil? + t.rollback_and_return! ServiceResponse.error( + message: 'Sub-flow starting node not found', + error_code: :node_not_found + ) + end + + starting_node = starting_node_reference[:node] + elsif sub_flow_input.function_identifier.present? + function_definition = flow.project.primary_runtime.function_definitions.find_by( + identifier: sub_flow_input.function_identifier + ) + + if function_definition.nil? + t.rollback_and_return! ServiceResponse.error( + message: 'Sub-flow function not found', + error_code: :invalid_function_id + ) + end + end + + sub_flow = node_parameter.sub_flow || node_parameter.build_sub_flow + sub_flow.assign_attributes( + starting_node: starting_node, + function_definition: function_definition, + signature: sub_flow_input.signature + ) + + sub_flow_settings_input = Array(sub_flow_input.try(:settings)) + sub_flow_settings = sub_flow.sub_flow_settings.first(sub_flow_settings_input.length) + + sub_flow_settings_input.each_with_index do |setting, index| + sub_flow_settings[index] ||= sub_flow.sub_flow_settings.build + sub_flow_settings[index].assign_attributes( + identifier: setting.identifier, + default_value: setting.try(:default_value), + optional: setting.try(:optional) || false, + hidden: setting.try(:hidden) || false + ) + end + + (sub_flow.sub_flow_settings - sub_flow_settings).each(&:destroy) + end end end end diff --git a/db/migrate/20260520120000_add_tucana_shared_flow_sub_flows.rb b/db/migrate/20260520120000_add_tucana_shared_flow_sub_flows.rb new file mode 100644 index 00000000..6f89f26a --- /dev/null +++ b/db/migrate/20260520120000_add_tucana_shared_flow_sub_flows.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class AddTucanaSharedFlowSubFlows < Code0::ZeroTrack::Database::Migration[1.0] + def change + add_column :flow_settings, :cast, :text, limit: 500 + + add_column :node_parameters, :cast, :text, limit: 500 + + create_table :sub_flows do |t| + t.references :node_parameter, null: false, index: { unique: true }, + foreign_key: { to_table: :node_parameters, on_delete: :cascade } + t.references :starting_node, null: true, foreign_key: { to_table: :node_functions, on_delete: :restrict } + t.references :function_definition, null: true, foreign_key: { on_delete: :restrict } + t.text :signature, null: false, limit: 500 + + t.check_constraint 'num_nonnulls(starting_node_id, function_definition_id) = 1', + name: check_constraint_name(:sub_flows, :execution_reference, :one_of) + + t.timestamps_with_timezone + end + + create_table :sub_flow_settings do |t| + t.references :sub_flow, null: false, foreign_key: { to_table: :sub_flows, on_delete: :cascade } + t.text :identifier, null: false + t.jsonb :default_value + t.boolean :optional, null: false, default: false + t.boolean :hidden, null: false, default: false + + t.timestamps_with_timezone + end + + remove_reference :node_functions, :value_of_node_parameter, + null: true, + foreign_key: { to_table: :node_parameters, on_delete: :cascade } + end +end diff --git a/db/schema_migrations/20260520120000 b/db/schema_migrations/20260520120000 new file mode 100644 index 00000000..2c8b40a7 --- /dev/null +++ b/db/schema_migrations/20260520120000 @@ -0,0 +1 @@ +e8ff4064cc4b6989a987836826ce7686d62a62059e30f7fddc8466d3ec893d8f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0375537b..911d5dd1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -205,7 +205,9 @@ CREATE TABLE flow_settings ( flow_setting_id text NOT NULL, object jsonb NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + "cast" text, + CONSTRAINT check_65f98666ae CHECK ((char_length("cast") <= 500)) ); CREATE SEQUENCE flow_settings_id_seq @@ -614,8 +616,7 @@ CREATE TABLE node_functions ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, flow_id bigint NOT NULL, - function_definition_id bigint NOT NULL, - value_of_node_parameter_id bigint + function_definition_id bigint NOT NULL ); CREATE SEQUENCE node_functions_id_seq @@ -633,7 +634,9 @@ CREATE TABLE node_parameters ( literal_value jsonb, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - parameter_definition_id bigint NOT NULL + parameter_definition_id bigint NOT NULL, + "cast" text, + CONSTRAINT check_6439c80497 CHECK ((char_length("cast") <= 500)) ); CREATE SEQUENCE node_parameters_id_seq @@ -945,6 +948,47 @@ CREATE TABLE schema_migrations ( version character varying NOT NULL ); +CREATE TABLE sub_flow_settings ( + id bigint NOT NULL, + sub_flow_id bigint NOT NULL, + identifier text NOT NULL, + default_value jsonb, + optional boolean DEFAULT false NOT NULL, + hidden boolean DEFAULT false NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE sub_flow_settings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE sub_flow_settings_id_seq OWNED BY sub_flow_settings.id; + +CREATE TABLE sub_flows ( + id bigint NOT NULL, + node_parameter_id bigint NOT NULL, + starting_node_id bigint, + function_definition_id bigint, + signature text NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_53a99b1dd3 CHECK ((num_nonnulls(starting_node_id, function_definition_id) = 1)), + CONSTRAINT check_943d01babb CHECK ((char_length(signature) <= 500)) +); + +CREATE SEQUENCE sub_flows_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE sub_flows_id_seq OWNED BY sub_flows.id; + CREATE TABLE translations ( id bigint NOT NULL, code text NOT NULL, @@ -1116,6 +1160,10 @@ ALTER TABLE ONLY runtime_statuses ALTER COLUMN id SET DEFAULT nextval('runtime_s ALTER TABLE ONLY runtimes ALTER COLUMN id SET DEFAULT nextval('runtimes_id_seq'::regclass); +ALTER TABLE ONLY sub_flow_settings ALTER COLUMN id SET DEFAULT nextval('sub_flow_settings_id_seq'::regclass); + +ALTER TABLE ONLY sub_flows ALTER COLUMN id SET DEFAULT nextval('sub_flows_id_seq'::regclass); + ALTER TABLE ONLY translations ALTER COLUMN id SET DEFAULT nextval('translations_id_seq'::regclass); ALTER TABLE ONLY user_identities ALTER COLUMN id SET DEFAULT nextval('user_identities_id_seq'::regclass); @@ -1274,6 +1322,12 @@ ALTER TABLE ONLY runtimes ALTER TABLE ONLY schema_migrations ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); +ALTER TABLE ONLY sub_flow_settings + ADD CONSTRAINT sub_flow_settings_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY sub_flows + ADD CONSTRAINT sub_flows_pkey PRIMARY KEY (id); + ALTER TABLE ONLY translations ADD CONSTRAINT translations_pkey PRIMARY KEY (id); @@ -1442,8 +1496,6 @@ CREATE INDEX index_node_functions_on_function_definition_id ON node_functions US CREATE INDEX index_node_functions_on_next_node_id ON node_functions USING btree (next_node_id); -CREATE INDEX index_node_functions_on_value_of_node_parameter_id ON node_functions USING btree (value_of_node_parameter_id); - CREATE INDEX index_node_parameters_on_node_function_id ON node_parameters USING btree (node_function_id); CREATE INDEX index_node_parameters_on_parameter_definition_id ON node_parameters USING btree (parameter_definition_id); @@ -1470,6 +1522,14 @@ CREATE INDEX index_runtimes_on_namespace_id ON runtimes USING btree (namespace_i CREATE UNIQUE INDEX index_runtimes_on_token ON runtimes USING btree (token); +CREATE INDEX index_sub_flow_settings_on_sub_flow_id ON sub_flow_settings USING btree (sub_flow_id); + +CREATE INDEX index_sub_flows_on_function_definition_id ON sub_flows USING btree (function_definition_id); + +CREATE UNIQUE INDEX index_sub_flows_on_node_parameter_id ON sub_flows USING btree (node_parameter_id); + +CREATE INDEX index_sub_flows_on_starting_node_id ON sub_flows USING btree (starting_node_id); + CREATE INDEX index_translations_on_owner ON translations USING btree (owner_type, owner_id); CREATE UNIQUE INDEX index_user_identities_on_provider_id_and_identifier ON user_identities USING btree (provider_id, identifier); @@ -1512,6 +1572,9 @@ ALTER TABLE ONLY function_definitions ALTER TABLE ONLY node_parameters ADD CONSTRAINT fk_rails_2ed7c53167 FOREIGN KEY (parameter_definition_id) REFERENCES parameter_definitions(id) ON DELETE RESTRICT; +ALTER TABLE ONLY sub_flows + ADD CONSTRAINT fk_rails_32ab48790a FOREIGN KEY (node_parameter_id) REFERENCES node_parameters(id) ON DELETE CASCADE; + ALTER TABLE ONLY runtime_flow_types ADD CONSTRAINT fk_rails_3675f29c4e FOREIGN KEY (runtime_id) REFERENCES runtimes(id) ON DELETE CASCADE; @@ -1554,6 +1617,9 @@ ALTER TABLE ONLY node_functions ALTER TABLE ONLY backup_codes ADD CONSTRAINT fk_rails_556c1feac3 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY sub_flow_settings + ADD CONSTRAINT fk_rails_55f76c79cc FOREIGN KEY (sub_flow_id) REFERENCES sub_flows(id) ON DELETE CASCADE; + ALTER TABLE ONLY namespace_members ADD CONSTRAINT fk_rails_567f152a62 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -1629,6 +1695,9 @@ ALTER TABLE ONLY user_sessions ALTER TABLE ONLY namespace_members ADD CONSTRAINT fk_rails_a0a760b9b4 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY sub_flows + ADD CONSTRAINT fk_rails_a99aa3478f FOREIGN KEY (function_definition_id) REFERENCES function_definitions(id) ON DELETE RESTRICT; + ALTER TABLE ONLY flows ADD CONSTRAINT fk_rails_ab927e0ecb FOREIGN KEY (project_id) REFERENCES namespace_projects(id) ON DELETE CASCADE; @@ -1662,6 +1731,9 @@ ALTER TABLE ONLY flows ALTER TABLE ONLY flow_settings ADD CONSTRAINT fk_rails_da3b2fb3c5 FOREIGN KEY (flow_id) REFERENCES flows(id) ON DELETE CASCADE; +ALTER TABLE ONLY sub_flows + ADD CONSTRAINT fk_rails_e27dd4d82a FOREIGN KEY (starting_node_id) REFERENCES node_functions(id) ON DELETE RESTRICT; + ALTER TABLE ONLY runtime_flow_types ADD CONSTRAINT fk_rails_e729dc57e7 FOREIGN KEY (runtime_module_id) REFERENCES runtime_modules(id) ON DELETE CASCADE; @@ -1674,9 +1746,6 @@ ALTER TABLE ONLY runtimes ALTER TABLE ONLY flow_data_type_links ADD CONSTRAINT fk_rails_f4202724d3 FOREIGN KEY (flow_id) REFERENCES flows(id) ON DELETE CASCADE; -ALTER TABLE ONLY node_functions - ADD CONSTRAINT fk_rails_f5d1a9d316 FOREIGN KEY (value_of_node_parameter_id) REFERENCES node_parameters(id) ON DELETE CASCADE; - ALTER TABLE ONLY audit_events ADD CONSTRAINT fk_rails_f64374fc56 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/docs/graphql/enum/errorcodeenum.md b/docs/graphql/enum/errorcodeenum.md index 6aa35ecd..1bd36423 100644 --- a/docs/graphql/enum/errorcodeenum.md +++ b/docs/graphql/enum/errorcodeenum.md @@ -20,7 +20,6 @@ Represents the available error responses | `FAILED_TO_SAVE_VALID_BACKUP_CODE` | The new backup codes could not be saved | | `FLOW_NOT_FOUND` | The flow with the given identifier was not found | | `FLOW_TYPE_NOT_FOUND` | The flow type with the given identifier was not found | -| `FUNCTION_VALUE_NOT_FOUND` | The id for the function value node does not exist | | `GENERIC_KEY_NOT_FOUND` | The given key was not found in the data type | | `IDENTITY_NOT_FOUND` | The external identity with the given identifier was not found | | `IDENTITY_VALIDATION_FAILED` | Failed to validate the external identity | diff --git a/docs/graphql/input_object/flowsettinginput.md b/docs/graphql/input_object/flowsettinginput.md index 403e6ee7..92e3a625 100644 --- a/docs/graphql/input_object/flowsettinginput.md +++ b/docs/graphql/input_object/flowsettinginput.md @@ -8,4 +8,5 @@ Input type for flow settings | Name | Type | Description | |------|------|-------------| +| `cast` | [`String`](../scalar/string.md) | The cast applied to the flow setting | | `value` | [`JSON!`](../scalar/json.md) | The value of the flow setting | diff --git a/docs/graphql/input_object/flowsubflowinput.md b/docs/graphql/input_object/flowsubflowinput.md new file mode 100644 index 00000000..c853ee22 --- /dev/null +++ b/docs/graphql/input_object/flowsubflowinput.md @@ -0,0 +1,14 @@ +--- +title: FlowSubFlowInput +--- + +Input type for sub-flow parameter values + +## Fields + +| Name | Type | Description | +|------|------|-------------| +| `functionIdentifier` | [`String`](../scalar/string.md) | The function identifier to execute | +| `settings` | [`[FlowSubFlowSettingInput!]`](../input_object/flowsubflowsettinginput.md) | The sub-flow settings | +| `signature` | [`String!`](../scalar/string.md) | The sub-flow signature | +| `startingNodeId` | [`NodeFunctionID`](../scalar/nodefunctionid.md) | The starting node to execute | diff --git a/docs/graphql/input_object/flowsubflowsettinginput.md b/docs/graphql/input_object/flowsubflowsettinginput.md new file mode 100644 index 00000000..521c0c89 --- /dev/null +++ b/docs/graphql/input_object/flowsubflowsettinginput.md @@ -0,0 +1,14 @@ +--- +title: FlowSubFlowSettingInput +--- + +Input type for sub-flow settings + +## Fields + +| Name | Type | Description | +|------|------|-------------| +| `defaultValue` | [`JSON`](../scalar/json.md) | The default value of the sub-flow setting | +| `hidden` | [`Boolean`](../scalar/boolean.md) | Whether the sub-flow setting is hidden | +| `identifier` | [`String!`](../scalar/string.md) | The identifier of the sub-flow setting | +| `optional` | [`Boolean`](../scalar/boolean.md) | Whether the sub-flow setting is optional | diff --git a/docs/graphql/input_object/nodeparameterinput.md b/docs/graphql/input_object/nodeparameterinput.md index 04ec1c62..f04da24f 100644 --- a/docs/graphql/input_object/nodeparameterinput.md +++ b/docs/graphql/input_object/nodeparameterinput.md @@ -8,4 +8,5 @@ Input type for Node parameter | Name | Type | Description | |------|------|-------------| +| `cast` | [`String`](../scalar/string.md) | The cast applied to the parameter | | `value` | [`NodeParameterValueInput!`](../input_object/nodeparametervalueinput.md) | The value of the parameter | diff --git a/docs/graphql/input_object/nodeparametervalueinput.md b/docs/graphql/input_object/nodeparametervalueinput.md index 96e3f693..2ccb1ee0 100644 --- a/docs/graphql/input_object/nodeparametervalueinput.md +++ b/docs/graphql/input_object/nodeparametervalueinput.md @@ -9,5 +9,5 @@ Input type for parameter value | Name | Type | Description | |------|------|-------------| | `literalValue` | [`JSON`](../scalar/json.md) | The literal value of the parameter | -| `nodeFunctionId` | [`NodeFunctionID`](../scalar/nodefunctionid.md) | The function value of the parameter as an id | | `referenceValue` | [`ReferenceValueInput`](../input_object/referencevalueinput.md) | The reference value of the parameter | +| `subFlow` | [`FlowSubFlowInput`](../input_object/flowsubflowinput.md) | The sub-flow value of the parameter | diff --git a/docs/graphql/object/flowsetting.md b/docs/graphql/object/flowsetting.md index 460bd694..cf517f4c 100644 --- a/docs/graphql/object/flowsetting.md +++ b/docs/graphql/object/flowsetting.md @@ -8,6 +8,7 @@ Represents a flow setting | Name | Type | Description | |------|------|-------------| +| `cast` | [`String`](../scalar/string.md) | The cast applied to the flow setting | | `createdAt` | [`Time!`](../scalar/time.md) | Time when this FlowSetting was created | | `flowSettingIdentifier` | [`String!`](../scalar/string.md) | The identifier of the flow setting | | `id` | [`FlowSettingID!`](../scalar/flowsettingid.md) | Global ID of this FlowSetting | diff --git a/docs/graphql/object/flowsubflow.md b/docs/graphql/object/flowsubflow.md new file mode 100644 index 00000000..f1ac2b94 --- /dev/null +++ b/docs/graphql/object/flowsubflow.md @@ -0,0 +1,14 @@ +--- +title: FlowSubFlow +--- + +Represents a sub-flow parameter value. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `functionDefinition` | [`FunctionDefinition`](../object/functiondefinition.md) | The resolved function definition to execute. | +| `settings` | [`[FlowSubFlowSetting!]!`](../object/flowsubflowsetting.md) | The sub-flow settings. | +| `signature` | [`String!`](../scalar/string.md) | The sub-flow signature. | +| `startingNodeId` | [`NodeFunctionID`](../scalar/nodefunctionid.md) | The starting node to execute. | diff --git a/docs/graphql/object/flowsubflowsetting.md b/docs/graphql/object/flowsubflowsetting.md new file mode 100644 index 00000000..2fc41613 --- /dev/null +++ b/docs/graphql/object/flowsubflowsetting.md @@ -0,0 +1,14 @@ +--- +title: FlowSubFlowSetting +--- + +Represents a sub-flow setting. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `defaultValue` | [`JSON`](../scalar/json.md) | The default value of the sub-flow setting. | +| `hidden` | [`Boolean`](../scalar/boolean.md) | Whether the sub-flow setting is hidden. | +| `identifier` | [`String!`](../scalar/string.md) | The identifier of the sub-flow setting. | +| `optional` | [`Boolean`](../scalar/boolean.md) | Whether the sub-flow setting is optional. | diff --git a/docs/graphql/object/nodefunctionidwrapper.md b/docs/graphql/object/nodefunctionidwrapper.md deleted file mode 100644 index a5d139fd..00000000 --- a/docs/graphql/object/nodefunctionidwrapper.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: NodeFunctionIdWrapper ---- - -Represents a Node Function id wrapper. - -## Fields without arguments - -| Name | Type | Description | -|------|------|-------------| -| `id` | [`NodeFunctionID!`](../scalar/nodefunctionid.md) | Global ID of this NodeFunctionIdWrapper | diff --git a/docs/graphql/object/nodeparameter.md b/docs/graphql/object/nodeparameter.md index e08ad1ae..77569f5f 100644 --- a/docs/graphql/object/nodeparameter.md +++ b/docs/graphql/object/nodeparameter.md @@ -8,6 +8,7 @@ Represents a Node parameter | Name | Type | Description | |------|------|-------------| +| `cast` | [`String`](../scalar/string.md) | The cast applied to the parameter | | `createdAt` | [`Time!`](../scalar/time.md) | Time when this NodeParameter was created | | `id` | [`NodeParameterID!`](../scalar/nodeparameterid.md) | Global ID of this NodeParameter | | `parameterDefinition` | [`ParameterDefinition!`](../object/parameterdefinition.md) | The definition of the parameter | diff --git a/docs/graphql/union/nodeparametervalue.md b/docs/graphql/union/nodeparametervalue.md index 1c267c84..97435410 100644 --- a/docs/graphql/union/nodeparametervalue.md +++ b/docs/graphql/union/nodeparametervalue.md @@ -6,6 +6,6 @@ Represents a parameter value for a node. ## Possible types +- [`FlowSubFlow`](../object/flowsubflow.md) - [`LiteralValue`](../object/literalvalue.md) -- [`NodeFunctionIdWrapper`](../object/nodefunctionidwrapper.md) - [`ReferenceValue`](../object/referencevalue.md) diff --git a/spec/factories/node_functions.rb b/spec/factories/node_functions.rb index 9cd825aa..f12be9f1 100644 --- a/spec/factories/node_functions.rb +++ b/spec/factories/node_functions.rb @@ -6,6 +6,5 @@ next_node { nil } node_parameters { [] } flow - value_of_node_parameter { nil } end end diff --git a/spec/factories/node_parameters.rb b/spec/factories/node_parameters.rb index 8d0c86df..eb0831c2 100644 --- a/spec/factories/node_parameters.rb +++ b/spec/factories/node_parameters.rb @@ -6,6 +6,5 @@ node_function literal_value { 'value' } reference_value { nil } - function_value { nil } end end diff --git a/spec/factories/sub_flow_settings.rb b/spec/factories/sub_flow_settings.rb new file mode 100644 index 00000000..89587658 --- /dev/null +++ b/spec/factories/sub_flow_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :sub_flow_setting do + sub_flow + identifier { 'setting' } + default_value { nil } + optional { false } + hidden { false } + end +end diff --git a/spec/factories/sub_flows.rb b/spec/factories/sub_flows.rb new file mode 100644 index 00000000..3c848352 --- /dev/null +++ b/spec/factories/sub_flows.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :sub_flow do + node_parameter { association :node_parameter, literal_value: nil, reference_value: nil } + starting_node factory: :node_function + function_definition { nil } + signature { '(): VOID' } + end +end diff --git a/spec/models/node_function_spec.rb b/spec/models/node_function_spec.rb index 5aa9c20b..f8ef7194 100644 --- a/spec/models/node_function_spec.rb +++ b/spec/models/node_function_spec.rb @@ -10,13 +10,6 @@ it { is_expected.to belong_to(:next_node).class_name('NodeFunction').optional } it { is_expected.to belong_to(:flow).class_name('Flow') } - it do - is_expected.to belong_to(:value_of_node_parameter) - .class_name('NodeParameter') - .inverse_of(:function_value) - .optional - end - it { is_expected.to have_many(:node_parameters).inverse_of(:node_function) } end diff --git a/spec/models/node_parameter_spec.rb b/spec/models/node_parameter_spec.rb index bb328be1..3e34791d 100644 --- a/spec/models/node_parameter_spec.rb +++ b/spec/models/node_parameter_spec.rb @@ -10,12 +10,7 @@ describe 'associations' do it { is_expected.to belong_to(:parameter_definition).class_name('ParameterDefinition') } it { is_expected.to have_one(:reference_value) } - - it do - is_expected.to have_one(:function_value) - .class_name('NodeFunction') - .inverse_of(:value_of_node_parameter) - end + it { is_expected.to have_one(:sub_flow) } it { is_expected.to belong_to(:node_function).class_name('NodeFunction').inverse_of(:node_parameters) } end @@ -25,20 +20,18 @@ param = build( :node_parameter, literal_value: 1, - reference_value: create(:reference_value), - function_value: nil + reference_value: create(:reference_value) ) expect(param).not_to be_valid expect(param.errors[:value]) - .to include('Only one of literal_value, reference_value, or function_value must be present') + .to include('Only one of literal_value, reference_value, or sub_flow must be present') end it 'allows all values to be empty' do param = build( :node_parameter, literal_value: nil, - reference_value: nil, - function_value: nil + reference_value: nil ) expect(param).to be_valid end diff --git a/spec/models/sub_flow_setting_spec.rb b/spec/models/sub_flow_setting_spec.rb new file mode 100644 index 00000000..1566609a --- /dev/null +++ b/spec/models/sub_flow_setting_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SubFlowSetting do + subject { create(:sub_flow_setting) } + + describe 'associations' do + it { is_expected.to belong_to(:sub_flow).inverse_of(:sub_flow_settings) } + end + + describe 'validations' do + it { is_expected.to allow_values(true, false).for(:optional) } + it { is_expected.to allow_values(true, false).for(:hidden) } + it { is_expected.to validate_presence_of(:identifier) } + end +end diff --git a/spec/models/sub_flow_spec.rb b/spec/models/sub_flow_spec.rb new file mode 100644 index 00000000..4d3f4870 --- /dev/null +++ b/spec/models/sub_flow_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SubFlow do + subject { create(:sub_flow) } + + describe 'associations' do + it { is_expected.to belong_to(:node_parameter).inverse_of(:sub_flow) } + it { is_expected.to belong_to(:starting_node).class_name('NodeFunction').optional } + it { is_expected.to belong_to(:function_definition).optional } + it { is_expected.to have_many(:sub_flow_settings).inverse_of(:sub_flow) } + end + + describe 'validations' do + it 'requires exactly one execution reference' do + sub_flow = build(:sub_flow, starting_node: nil, function_definition: nil) + + expect(sub_flow).not_to be_valid + expect(sub_flow.errors[:base]).to include('Exactly one of starting_node or function_definition must be present') + end + end +end diff --git a/spec/requests/graphql/mutation/namespace/projects/flows/create_mutation_spec.rb b/spec/requests/graphql/mutation/namespace/projects/flows/create_mutation_spec.rb index 5b927b3c..4fb845b7 100644 --- a/spec/requests/graphql/mutation/namespace/projects/flows/create_mutation_spec.rb +++ b/spec/requests/graphql/mutation/namespace/projects/flows/create_mutation_spec.rb @@ -29,6 +29,10 @@ ...on LiteralValue { value } + ...on FlowSubFlow { + signature + startingNodeId + } ...on ReferenceValue { createdAt id @@ -104,7 +108,10 @@ parameters: [ { value: { - nodeFunctionId: 'gid://sagittarius/NodeFunction/2000', + subFlow: { + startingNodeId: 'gid://sagittarius/NodeFunction/2000', + signature: '(input: INPUT): OUTPUT', + }, }, } ], @@ -201,6 +208,13 @@ :value ) expect(parameter_values).to include(a_hash_including('value' => 100)) + expect(parameter_values).to include( + a_hash_including( + '__typename' => 'FlowSubFlow', + 'signature' => '(input: INPUT): OUTPUT', + 'startingNodeId' => a_string_matching(%r{gid://sagittarius/NodeFunction/\d+}) + ) + ) expect(parameter_values).to include( a_hash_including('referencePath' => [a_hash_including('arrayIndex' => 0, 'path' => 'some.path')]) ) diff --git a/spec/requests/graphql/mutation/namespace/projects/flows/update_mutation_spec.rb b/spec/requests/graphql/mutation/namespace/projects/flows/update_mutation_spec.rb index 5538f286..0ec5642d 100644 --- a/spec/requests/graphql/mutation/namespace/projects/flows/update_mutation_spec.rb +++ b/spec/requests/graphql/mutation/namespace/projects/flows/update_mutation_spec.rb @@ -29,6 +29,14 @@ ...on LiteralValue { value } + ...on FlowSubFlow { + functionDefinition { + id + identifier + } + signature + startingNodeId + } ...on ReferenceValue { createdAt id @@ -110,7 +118,10 @@ parameters: [ { value: { - nodeFunctionId: 'gid://sagittarius/NodeFunction/2000', + subFlow: { + startingNodeId: 'gid://sagittarius/NodeFunction/2000', + signature: '(input: INPUT): OUTPUT', + }, }, } ], @@ -213,6 +224,13 @@ 'value' => 100 ) ) + expect(parameter_values).to include( + a_hash_including( + '__typename' => 'FlowSubFlow', + 'signature' => '(input: INPUT): OUTPUT', + 'startingNodeId' => a_string_matching(%r{gid://sagittarius/NodeFunction/\d+}) + ) + ) expect(parameter_values).to include( a_hash_including( '__typename' => 'ReferenceValue', @@ -243,6 +261,77 @@ end end + context 'when updating a sub-flow by function identifier' do + before do + stub_allowed_ability(NamespaceProjectPolicy, :update_flow, user: current_user, subject: project) + stub_allowed_ability(NamespaceProjectPolicy, :read_namespace_project, user: current_user, subject: project) + end + + let(:input) do + { + flowId: flow.to_global_id.to_s, + flowInput: { + name: generate(:flow_name), + type: flow_type.to_global_id.to_s, + startingNodeId: flow.starting_node.to_global_id.to_s, + settings: [], + nodes: [ + { + id: flow.starting_node.to_global_id.to_s, + functionDefinitionId: function_definition.to_global_id.to_s, + nextNodeId: nil, + parameters: [ + { + value: { + subFlow: { + functionIdentifier: function_definition.identifier, + signature: '(input: INPUT): OUTPUT', + }, + }, + } + ], + } + ], + }, + } + end + + let(:flow) do + create(:flow, project: project, flow_type: flow_type).tap do |f| + node = create(:node_function, flow: f, function_definition: function_definition) + create( + :node_parameter, + node_function: node, + parameter_definition: function_definition.parameter_definitions.first, + literal_value: nil + ) + f.starting_node = node + f.save! + end + end + + it 'stores the referenced function definition on the sub-flow' do + mutate! + + expect(graphql_data_at(:namespaces_projects_flows_update, :errors)).to be_blank + + parameter_value = graphql_data_at(:namespaces_projects_flows_update, :flow, :nodes, :nodes) + .first['parameters']['nodes'].first['value'] + + expect(parameter_value).to include( + '__typename' => 'FlowSubFlow', + 'startingNodeId' => nil + ) + expect(parameter_value['functionDefinition']).to include( + 'id' => function_definition.to_global_id.to_s, + 'identifier' => function_definition.identifier + ) + + sub_flow = flow.reload.starting_node.node_parameters.first.sub_flow + expect(sub_flow.function_definition).to eq(function_definition) + end + end + context 'when removing nodes' do before do stub_allowed_ability(NamespaceProjectPolicy, :update_flow, user: current_user, subject: project) @@ -309,10 +398,8 @@ node_function: node1, parameter_definition: function_definition.parameter_definitions.first, literal_value: nil) - create(:node_function, - flow: f, - function_definition: function_definition, - value_of_node_parameter: parameter) + node2 = create(:node_function, flow: f, function_definition: function_definition) + create(:sub_flow, node_parameter: parameter, starting_node: node2, signature: '(input: INPUT): OUTPUT') f.starting_node = node1 node1.save! f.save! @@ -360,7 +447,7 @@ end end - context 'when clearing function_value on a reused node' do + context 'when clearing sub_flow on a reused node' do before do stub_allowed_ability(NamespaceProjectPolicy, :update_flow, user: current_user, subject: project) stub_allowed_ability(NamespaceProjectPolicy, :read_namespace_project, user: current_user, subject: project) @@ -373,10 +460,8 @@ node_function: node1, parameter_definition: function_definition.parameter_definitions.first, literal_value: nil) - create(:node_function, - flow: f, - function_definition: function_definition, - value_of_node_parameter: parameter) + node2 = create(:node_function, flow: f, function_definition: function_definition) + create(:sub_flow, node_parameter: parameter, starting_node: node2, signature: '(input: INPUT): OUTPUT') f.starting_node = node1 node1.save! f.save!