From 6d42edb2e80ee11a201513c09359117fc02cfebb Mon Sep 17 00:00:00 2001 From: Dario Pranjic Date: Sun, 19 Apr 2026 20:37:41 +0200 Subject: [PATCH] Add function definitions and data type links with runtime attributes also implement grpc endpoint --- app/finders/data_types_finder.rb | 11 + app/graphql/types/function_definition_type.rb | 33 +-- .../types/parameter_definition_type.rb | 6 +- app/grpc/function_handler.rb | 19 ++ app/models/function_definition.rb | 19 +- .../function_definition_data_type_link.rb | 6 + app/models/parameter_definition.rb | 3 +- app/services/error_code.rb | 5 + .../function_definitions/update_service.rb | 154 +++++++++++ .../update_service.rb | 37 +-- ...02944_overrideable_function_definitions.rb | 33 +++ db/schema_migrations/20260409202944 | 1 + db/structure.sql | 51 +++- docs/graphql/enum/errorcodeenum.md | 5 + docs/graphql/object/functiondefinition.md | 2 + .../function_definition_data_type_links.rb | 8 + spec/factories/function_definitions.rb | 8 + spec/factories/parameter_definitions.rb | 2 + .../types/function_definition_type_spec.rb | 2 + .../function_definition_service_spec.rb | 248 ++++++++++++++++++ ...untime_function_definition_service_spec.rb | 20 +- 21 files changed, 589 insertions(+), 84 deletions(-) create mode 100644 app/grpc/function_handler.rb create mode 100644 app/models/function_definition_data_type_link.rb create mode 100644 app/services/runtimes/grpc/function_definitions/update_service.rb create mode 100644 db/migrate/20260409202944_overrideable_function_definitions.rb create mode 100644 db/schema_migrations/20260409202944 create mode 100644 spec/factories/function_definition_data_type_links.rb create mode 100644 spec/requests/grpc/sagittarius/function_definition_service_spec.rb diff --git a/app/finders/data_types_finder.rb b/app/finders/data_types_finder.rb index 0bba0d50..fd354e17 100644 --- a/app/finders/data_types_finder.rb +++ b/app/finders/data_types_finder.rb @@ -5,6 +5,7 @@ def execute data_types = base_scope data_types = by_data_type(data_types) data_types = by_runtime_function_definition(data_types) + data_types = by_function_definition(data_types) data_types = by_flow_type(data_types) data_types = by_flow(data_types) @@ -35,6 +36,16 @@ def by_runtime_function_definition(data_types) data_types.where(id: referenced_data_types_ids) end + def by_function_definition(data_types) + return data_types unless params[:function_definition] + + referenced_data_types_ids = FunctionDefinitionDataTypeLink + .where(function_definition: params[:function_definition]) + .select(:referenced_data_type_id) + + data_types.where(id: referenced_data_types_ids) + end + def by_flow_type(data_types) return data_types unless params[:flow_type] diff --git a/app/graphql/types/function_definition_type.rb b/app/graphql/types/function_definition_type.rb index cbc7883a..f2dd31c4 100644 --- a/app/graphql/types/function_definition_type.rb +++ b/app/graphql/types/function_definition_type.rb @@ -6,7 +6,9 @@ class FunctionDefinitionType < Types::BaseObject authorize :read_function_definition - field :identifier, String, null: false, description: 'Identifier of the function' + field :identifier, String, null: false, + description: 'Identifier of the function', + method: :runtime_name field :parameter_definitions, Types::ParameterDefinitionType.connection_type, null: true, @@ -33,6 +35,14 @@ class FunctionDefinitionType < Types::BaseObject field :throws_error, Boolean, null: false, description: 'Indicates if the function can throw an error' + field :version, String, + null: false, + description: 'Version of the runtime function definition' + + field :definition_source, String, + null: true, + description: 'The source that defines this definition' + # rubocop:disable GraphQL/ExtractType field :display_icon, String, null: true, description: 'Display icon of the function' @@ -45,27 +55,8 @@ class FunctionDefinitionType < Types::BaseObject id_field FunctionDefinition timestamps - def identifier - object.runtime_function_definition&.runtime_name - end - - def signature - object.runtime_function_definition&.signature - end - - def throws_error - object.runtime_function_definition&.throws_error - end - - def display_icon - object.runtime_function_definition&.display_icon - end - def linked_data_types - return [] unless object.runtime_function_definition - - DataTypesFinder.new({ runtime_function_definition: object.runtime_function_definition, - expand_recursively: true }).execute + DataTypesFinder.new({ function_definition: object, expand_recursively: true }).execute end end end diff --git a/app/graphql/types/parameter_definition_type.rb b/app/graphql/types/parameter_definition_type.rb index d0207094..7f0bfa7f 100644 --- a/app/graphql/types/parameter_definition_type.rb +++ b/app/graphql/types/parameter_definition_type.rb @@ -6,7 +6,7 @@ class ParameterDefinitionType < Types::BaseObject authorize :read_parameter_definition - field :identifier, String, null: false, description: 'Identifier of the parameter' + field :identifier, String, null: false, description: 'Identifier of the parameter', method: :runtime_name field :descriptions, [Types::TranslationType], null: true, description: 'Description of the parameter' field :names, [Types::TranslationType], null: true, description: 'Name of the parameter' @@ -23,9 +23,5 @@ class ParameterDefinitionType < Types::BaseObject id_field ParameterDefinition timestamps - - def identifier - object.runtime_parameter_definition&.runtime_name - end end end diff --git a/app/grpc/function_handler.rb b/app/grpc/function_handler.rb new file mode 100644 index 00000000..f99c9384 --- /dev/null +++ b/app/grpc/function_handler.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class FunctionHandler < Tucana::Sagittarius::FunctionDefinitionService::Service + include GrpcHandler + include Code0::ZeroTrack::Loggable + + def update(request, _call) + current_runtime = Runtime.find(Code0::ZeroTrack::Context.current[:runtime][:id]) + + response = Runtimes::Grpc::FunctionDefinitions::UpdateService.new( + current_runtime, + request.functions + ).execute + + logger.debug("FunctionHandler#update response: #{response.inspect}") + + Tucana::Sagittarius::FunctionDefinitionUpdateResponse.new(success: response.success?) + end +end diff --git a/app/models/function_definition.rb b/app/models/function_definition.rb index 3710a422..76ab4046 100644 --- a/app/models/function_definition.rb +++ b/app/models/function_definition.rb @@ -3,6 +3,11 @@ class FunctionDefinition < ApplicationRecord belongs_to :runtime_function_definition + has_one :runtime, through: :runtime_function_definition + + has_many :function_definition_data_type_links, inverse_of: :function_definition + has_many :referenced_data_types, through: :function_definition_data_type_links, source: :referenced_data_type + has_many :node_functions, inverse_of: :function_definition has_many :parameter_definitions, inverse_of: :function_definition @@ -19,20 +24,20 @@ class FunctionDefinition < ApplicationRecord def to_grpc Tucana::Shared::FunctionDefinition.new( - runtime_name: runtime_function_definition.runtime_name, + runtime_name: runtime_name, parameter_definitions: parameter_definitions.map(&:to_grpc), - signature: runtime_function_definition.signature, - throws_error: runtime_function_definition.throws_error, + signature: signature, + throws_error: throws_error, name: names.map(&:to_grpc), description: descriptions.map(&:to_grpc), documentation: documentations.map(&:to_grpc), deprecation_message: deprecation_messages.map(&:to_grpc), display_message: display_messages.map(&:to_grpc), alias: aliases.map(&:to_grpc), - linked_data_type_identifiers: runtime_function_definition.referenced_data_types.map(&:identifier), - version: runtime_function_definition.version, - display_icon: runtime_function_definition.display_icon, - definition_source: runtime_function_definition.definition_source, + linked_data_type_identifiers: referenced_data_types.map(&:identifier), + version: version, + display_icon: display_icon, + definition_source: definition_source, runtime_definition_name: runtime_function_definition.runtime_name ) end diff --git a/app/models/function_definition_data_type_link.rb b/app/models/function_definition_data_type_link.rb new file mode 100644 index 00000000..c3ac1274 --- /dev/null +++ b/app/models/function_definition_data_type_link.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class FunctionDefinitionDataTypeLink < ApplicationRecord + belongs_to :function_definition, inverse_of: :function_definition_data_type_links + belongs_to :referenced_data_type, class_name: 'DataType' +end diff --git a/app/models/parameter_definition.rb b/app/models/parameter_definition.rb index 2c5d23eb..d8b14530 100644 --- a/app/models/parameter_definition.rb +++ b/app/models/parameter_definition.rb @@ -23,7 +23,8 @@ def function_definition_matches_definition def to_grpc Tucana::Shared::ParameterDefinition.new( - runtime_name: runtime_parameter_definition.runtime_name, + runtime_name: runtime_name, + runtime_definition_name: runtime_parameter_definition.runtime_name, default_value: Tucana::Shared::Value.from_ruby(default_value), name: names.map(&:to_grpc), description: descriptions.map(&:to_grpc), diff --git a/app/services/error_code.rb b/app/services/error_code.rb index 48db2f63..baf25d92 100644 --- a/app/services/error_code.rb +++ b/app/services/error_code.rb @@ -90,6 +90,11 @@ def self.error_codes invalid_runtime_status: { description: 'The runtime status is invalid because of active model errors' }, invalid_runtime_status_configuration: { description: 'The runtime status configuration is invalid because of active model errors' }, invalid_runtime_feature: { description: 'The runtime feature is invalid because of active model errors' }, + invalid_function_definition: { description: 'The function definition is invalid because of active model errors' }, + parent_runtime_function_definition_not_found: { description: 'The parent runtime function definition could not be found' }, + parent_runtime_parameter_definition_not_found: { description: 'The parent runtime parameter definition could not be found' }, + invalid_parameter_definition: { description: 'The parameter definition is invalid because of active model errors' }, + parameter_definition_count_mismatch: { description: 'The number of parameter definitions does not match the number of runtime parameter definitions' }, primary_level_not_found: { description: '', deprecation_reason: 'Outdated concept' }, secondary_level_not_found: { description: '', deprecation_reason: 'Outdated concept' }, diff --git a/app/services/runtimes/grpc/function_definitions/update_service.rb b/app/services/runtimes/grpc/function_definitions/update_service.rb new file mode 100644 index 00000000..016621c9 --- /dev/null +++ b/app/services/runtimes/grpc/function_definitions/update_service.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +module Runtimes + module Grpc + module FunctionDefinitions + class UpdateService + include Sagittarius::Database::Transactional + include Code0::ZeroTrack::Loggable + include Runtimes::Grpc::TranslationUpdateHelper + include Runtimes::Grpc::DataTypeHelper + + attr_reader :current_runtime, :function_definitions + + def initialize(current_runtime, function_definitions) + @current_runtime = current_runtime + @function_definitions = function_definitions + end + + def execute + transactional do |t| + # rubocop:disable Rails/SkipsModelValidations -- when marking definitions as removed, we don't care about validations + FunctionDefinition + .joins(:runtime) + .where(runtimes: { id: current_runtime }) + .update_all(removed_at: Time.zone.now) + # rubocop:enable Rails/SkipsModelValidations + function_definitions.each do |function_definition| + response = update_function_definition(function_definition, t) + next if response.persisted? + + logger.error(message: 'Failed to update function definition', + runtime_id: current_runtime.id, + definition_identifier: function_definition.runtime_name, + errors: response.errors.full_messages) + + t.rollback_and_return! ServiceResponse.error(message: 'Failed to update function definition', + error_code: :invalid_function_definition, + details: response.errors) + end + + UpdateRuntimeCompatibilityJob.perform_later({ runtime_id: current_runtime.id }) + + logger.info(message: 'Updated function definitions for runtime', runtime_id: current_runtime.id) + + ServiceResponse.success(message: 'Updated function definition', + payload: function_definitions) + end + end + + protected + + def update_function_definition(function_definition, t) + parent_runtime_function_definition = RuntimeFunctionDefinition.find_by( + runtime: current_runtime, + runtime_name: function_definition.runtime_definition_name + ) + if parent_runtime_function_definition.nil? + t.rollback_and_return! ServiceResponse.error( + message: 'Could not find parent runtime function definition for function definition', + error_code: :parent_runtime_function_definition_not_found, + details: { runtime_definition_name: function_definition.runtime_definition_name } + ) + end + + db_object = FunctionDefinition.find_or_initialize_by( + runtime_function_definition_id: parent_runtime_function_definition.id, + runtime_name: function_definition.runtime_name, + runtime_definition_name: function_definition.runtime_definition_name + ) + db_object.runtime_function_definition = parent_runtime_function_definition + db_object.removed_at = nil + db_object.signature = function_definition.signature + db_object.throws_error = function_definition.throws_error + db_object.version = function_definition.version + db_object.definition_source = function_definition.definition_source + db_object.display_icon = function_definition.display_icon + db_object.names = update_translations(function_definition.name, db_object.names) + db_object.descriptions = update_translations(function_definition.description, db_object.descriptions) + db_object.documentations = update_translations(function_definition.documentation, db_object.documentations) + db_object.deprecation_messages = update_translations(function_definition.deprecation_message, + db_object.deprecation_messages) + db_object.display_messages = update_translations(function_definition.display_message, + db_object.display_messages) + db_object.aliases = update_translations(function_definition.alias, db_object.aliases) + + db_object.save + + db_object.parameter_definitions = update_parameters(db_object, function_definition.parameter_definitions, + db_object.parameter_definitions, t) + + link_data_types(db_object, function_definition.linked_data_type_identifiers, t) + db_object + end + + def update_parameters(function_definition, parameters, db_parameters, t) + # rubocop:disable Rails/SkipsModelValidations -- when marking definitions as removed, we don't care about validations + db_parameters.update_all(removed_at: Time.zone.now) + # rubocop:enable Rails/SkipsModelValidations + + parameters.each do |real_param| + db_param = db_parameters.find do |current_param| + current_param.runtime_definition_name == real_param.runtime_definition_name + end + if db_param.nil? + db_param = ParameterDefinition.new + db_parameters << db_param + end + + runtime_parameter_definition = RuntimeParameterDefinition.find_by( + runtime_function_definition_id: function_definition.runtime_function_definition_id, + runtime_name: real_param.runtime_definition_name + ) + if runtime_parameter_definition.nil? + t.rollback_and_return! ServiceResponse.error( + message: 'Could not find parent runtime parameter definition for function definition parameter', + error_code: :parent_runtime_parameter_definition_not_found, + details: { runtime_definition_name: real_param.runtime_definition_name } + ) + end + + db_param.function_definition = function_definition + db_param.runtime_parameter_definition = runtime_parameter_definition + db_param.runtime_name = real_param.runtime_name + db_param.runtime_definition_name = real_param.runtime_definition_name + db_param.removed_at = nil + + db_param.names = update_translations(real_param.name, db_param.names) + db_param.descriptions = update_translations(real_param.description, db_param.descriptions) + db_param.documentations = update_translations(real_param.documentation, db_param.documentations) + + db_param.default_value = real_param.default_value&.to_ruby(true) + + next if db_param.save + + t.rollback_and_return! ServiceResponse.error( + message: 'Could not save parameter definition', + error_code: :invalid_parameter_definition, + details: db_param.errors + ) + end + + if db_parameters.reload.where(removed_at: nil).count != + function_definition.runtime_function_definition.parameters.size + t.rollback_and_return! ServiceResponse.error( + message: 'Number of parameter definitions does not match number of runtime parameter definitions', + error_code: :parameter_definition_count_mismatch + ) + end + db_parameters + end + end + end + end +end diff --git a/app/services/runtimes/grpc/runtime_function_definitions/update_service.rb b/app/services/runtimes/grpc/runtime_function_definitions/update_service.rb index c236e9a7..4c4777f2 100644 --- a/app/services/runtimes/grpc/runtime_function_definitions/update_service.rb +++ b/app/services/runtimes/grpc/runtime_function_definitions/update_service.rb @@ -69,20 +69,6 @@ def update_runtime_function_definition(runtime_function_definition, t) db_object.save - if db_object.function_definitions.empty? - definition = FunctionDefinition.new - definition.names = update_translations(runtime_function_definition.name, definition.names) - definition.descriptions = update_translations(runtime_function_definition.description, - definition.descriptions) - definition.documentations = update_translations(runtime_function_definition.documentation, - definition.documentations) - definition.display_messages = update_translations(runtime_function_definition.display_message, - definition.display_messages) - definition.aliases = update_translations(runtime_function_definition.alias, definition.aliases) - - db_object.function_definitions << definition - end - db_object.parameters = update_parameters(db_object, runtime_function_definition.runtime_parameter_definitions, db_object.parameters, t) @@ -111,24 +97,13 @@ def update_parameters(runtime_function_definition, parameters, db_parameters, t) db_param.default_value = real_param.default_value&.to_ruby(true) - unless db_param.save - t.rollback_and_return! ServiceResponse.error( - message: 'Could not save runtime parameter definition', - error_code: :invalid_runtime_parameter_definition, - details: db_param.errors - ) - end - - next unless db_param.parameter_definitions.empty? - - definition = ParameterDefinition.new - definition.names = update_translations(real_param.name, definition.names) - definition.descriptions = update_translations(real_param.description, definition.descriptions) - definition.documentations = update_translations(real_param.documentation, definition.documentations) - definition.default_value = db_param.default_value - definition.function_definition = runtime_function_definition.function_definitions.first + next if db_param.save - db_param.parameter_definitions << definition + t.rollback_and_return! ServiceResponse.error( + message: 'Could not save runtime parameter definition', + error_code: :invalid_runtime_parameter_definition, + details: db_param.errors + ) end db_parameters diff --git a/db/migrate/20260409202944_overrideable_function_definitions.rb b/db/migrate/20260409202944_overrideable_function_definitions.rb new file mode 100644 index 00000000..1b814f88 --- /dev/null +++ b/db/migrate/20260409202944_overrideable_function_definitions.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class OverrideableFunctionDefinitions < Code0::ZeroTrack::Database::Migration[1.0] + def change + add_column :function_definitions, :removed_at, :datetime_with_timezone + add_column :function_definitions, :runtime_name, :text, limit: 50 + add_column :function_definitions, :runtime_definition_name, :text, limit: 50 + add_column :function_definitions, :version, :text + add_column :function_definitions, :definition_source, :text, limit: 50 + # rubocop:disable Rails/NotNullColumn -- backwards compatibility intentionally ignored + add_column :function_definitions, :signature, :text, null: false, limit: 500 + # rubocop:enable Rails/NotNullColumn + add_column :function_definitions, :throws_error, :boolean, default: false, null: false + add_column :function_definitions, :display_icon, :text, limit: 100 + + add_column :parameter_definitions, :runtime_name, :text + add_column :parameter_definitions, :removed_at, :datetime_with_timezone + add_column :parameter_definitions, :runtime_definition_name, :text, limit: 50 + + create_table :function_definition_data_type_links do |t| + t.references :function_definition, null: false, + foreign_key: { on_delete: :cascade }, + index: false + t.references :referenced_data_type, null: false, + foreign_key: { to_table: :data_types, on_delete: :restrict }, + index: false + + t.index %i[function_definition_id referenced_data_type_id], unique: true + + t.timestamps_with_timezone + end + end +end diff --git a/db/schema_migrations/20260409202944 b/db/schema_migrations/20260409202944 new file mode 100644 index 00000000..a7ca5fea --- /dev/null +++ b/db/schema_migrations/20260409202944 @@ -0,0 +1 @@ +6940150660a630b3f7dcb7c3ac745643f42d4a99d90f3c9520320f3deb072b89 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 40475aed..e09ee450 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -301,11 +301,41 @@ CREATE SEQUENCE flows_id_seq ALTER SEQUENCE flows_id_seq OWNED BY flows.id; +CREATE TABLE function_definition_data_type_links ( + id bigint NOT NULL, + function_definition_id bigint NOT NULL, + referenced_data_type_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE function_definition_data_type_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE function_definition_data_type_links_id_seq OWNED BY function_definition_data_type_links.id; + CREATE TABLE function_definitions ( id bigint NOT NULL, runtime_function_definition_id bigint 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, + removed_at timestamp with time zone, + runtime_name text, + runtime_definition_name text, + version text, + definition_source text, + signature text NOT NULL, + throws_error boolean DEFAULT false NOT NULL, + display_icon text, + CONSTRAINT check_22a6e03130 CHECK ((char_length(runtime_name) <= 50)), + CONSTRAINT check_9ce4520c6a CHECK ((char_length(definition_source) <= 50)), + CONSTRAINT check_b8c3048abc CHECK ((char_length(display_icon) <= 100)), + CONSTRAINT check_c3b670ab47 CHECK ((char_length(signature) <= 500)), + CONSTRAINT check_df78af4987 CHECK ((char_length(runtime_definition_name) <= 50)) ); CREATE SEQUENCE function_definitions_id_seq @@ -614,7 +644,11 @@ CREATE TABLE parameter_definitions ( default_value jsonb, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - function_definition_id bigint NOT NULL + function_definition_id bigint NOT NULL, + runtime_name text, + removed_at timestamp with time zone, + runtime_definition_name text, + CONSTRAINT check_deb54dae8e CHECK ((char_length(runtime_definition_name) <= 50)) ); CREATE SEQUENCE parameter_definitions_id_seq @@ -921,6 +955,8 @@ ALTER TABLE ONLY flow_types ALTER COLUMN id SET DEFAULT nextval('flow_types_id_s ALTER TABLE ONLY flows ALTER COLUMN id SET DEFAULT nextval('flows_id_seq'::regclass); +ALTER TABLE ONLY function_definition_data_type_links ALTER COLUMN id SET DEFAULT nextval('function_definition_data_type_links_id_seq'::regclass); + ALTER TABLE ONLY function_definitions ALTER COLUMN id SET DEFAULT nextval('function_definitions_id_seq'::regclass); ALTER TABLE ONLY namespace_licenses ALTER COLUMN id SET DEFAULT nextval('namespace_licenses_id_seq'::regclass); @@ -1023,6 +1059,9 @@ ALTER TABLE ONLY flow_types ALTER TABLE ONLY flows ADD CONSTRAINT flows_pkey PRIMARY KEY (id); +ALTER TABLE ONLY function_definition_data_type_links + ADD CONSTRAINT function_definition_data_type_links_pkey PRIMARY KEY (id); + ALTER TABLE ONLY function_definitions ADD CONSTRAINT function_definitions_pkey PRIMARY KEY (id); @@ -1128,6 +1167,8 @@ CREATE UNIQUE INDEX idx_on_flow_id_referenced_data_type_id_14b02b52f8 ON flow_da CREATE UNIQUE INDEX idx_on_flow_type_id_referenced_data_type_id_70312c9382 ON flow_type_data_type_links USING btree (flow_type_id, referenced_data_type_id); +CREATE UNIQUE INDEX idx_on_function_definition_id_referenced_data_type__0311fd8b69 ON function_definition_data_type_links USING btree (function_definition_id, referenced_data_type_id); + CREATE UNIQUE INDEX idx_on_namespace_role_id_ability_a092da8841 ON namespace_role_abilities USING btree (namespace_role_id, ability); CREATE UNIQUE INDEX idx_on_role_id_project_id_5d4b5917dc ON namespace_role_project_assignments USING btree (role_id, project_id); @@ -1319,6 +1360,9 @@ ALTER TABLE ONLY runtime_statuses ALTER TABLE ONLY parameter_definitions ADD CONSTRAINT fk_rails_3b02763f84 FOREIGN KEY (runtime_parameter_definition_id) REFERENCES runtime_parameter_definitions(id) ON DELETE CASCADE; +ALTER TABLE ONLY function_definition_data_type_links + ADD CONSTRAINT fk_rails_42dfc4930a FOREIGN KEY (function_definition_id) REFERENCES function_definitions(id) ON DELETE CASCADE; + ALTER TABLE ONLY data_type_data_type_links ADD CONSTRAINT fk_rails_443c90661b FOREIGN KEY (referenced_data_type_id) REFERENCES data_types(id) ON DELETE RESTRICT; @@ -1430,6 +1474,9 @@ ALTER TABLE ONLY flow_settings ALTER TABLE ONLY runtimes ADD CONSTRAINT fk_rails_eeb42116cc FOREIGN KEY (namespace_id) REFERENCES namespaces(id); +ALTER TABLE ONLY function_definition_data_type_links + ADD CONSTRAINT fk_rails_f0cfd6d571 FOREIGN KEY (referenced_data_type_id) REFERENCES data_types(id) ON DELETE RESTRICT; + ALTER TABLE ONLY flow_data_type_links ADD CONSTRAINT fk_rails_f4202724d3 FOREIGN KEY (flow_id) REFERENCES flows(id) ON DELETE CASCADE; diff --git a/docs/graphql/enum/errorcodeenum.md b/docs/graphql/enum/errorcodeenum.md index 675b3578..bc0d1e1f 100644 --- a/docs/graphql/enum/errorcodeenum.md +++ b/docs/graphql/enum/errorcodeenum.md @@ -31,6 +31,7 @@ Represents the available error responses | `INVALID_FLOW` | The flow is invalid because of active model errors | | `INVALID_FLOW_SETTING` | The flow setting is invalid because of active model errors | | `INVALID_FLOW_TYPE` | The flow type is invalid because of active model errors | +| `INVALID_FUNCTION_DEFINITION` | The function definition is invalid because of active model errors | | `INVALID_FUNCTION_ID` | The function ID is invalid | | `INVALID_LOGIN_DATA` | Invalid login data provided | | `INVALID_NAMESPACE_LICENSE` | The namespace license is invalid because of active model errors | @@ -40,6 +41,7 @@ Represents the available error responses | `INVALID_NODE_FUNCTION` | The node function is invalid | | `INVALID_NODE_PARAMETER` | The node parameter is invalid | | `INVALID_ORGANIZATION` | The organization is invalid because of active model errors | +| `INVALID_PARAMETER_DEFINITION` | The parameter definition is invalid because of active model errors | | `INVALID_PARAMETER_INDEX` | The parameter index is invalid | | `INVALID_PASSWORD_REPEAT` | The provided password repeat does not match the password | | `INVALID_REFERENCE_VALUE` | The reference value is invalid | @@ -75,6 +77,9 @@ Represents the available error responses | `NO_PRIMARY_RUNTIME` | The project does not have a primary runtime | | `ORGANIZATION_NOT_FOUND` | The organization with the given identifier was not found | | `OUTDATED_DEFINITION` | The primary runtime has a newer definition than this one | +| `PARAMETER_DEFINITION_COUNT_MISMATCH` | The number of parameter definitions does not match the number of runtime parameter definitions | +| `PARENT_RUNTIME_FUNCTION_DEFINITION_NOT_FOUND` | The parent runtime function definition could not be found | +| `PARENT_RUNTIME_PARAMETER_DEFINITION_NOT_FOUND` | The parent runtime parameter definition could not be found | | `PRIMARY_LEVEL_NOT_FOUND` | **Deprecated:** Outdated concept | | `PROJECT_NOT_FOUND` | The namespace project with the given identifier was not found | | `REFERENCED_VALUE_NOT_FOUND` | A referenced value could not be found | diff --git a/docs/graphql/object/functiondefinition.md b/docs/graphql/object/functiondefinition.md index 5dc79475..79473a70 100644 --- a/docs/graphql/object/functiondefinition.md +++ b/docs/graphql/object/functiondefinition.md @@ -10,6 +10,7 @@ Represents a function definition |------|------|-------------| | `aliases` | [`[Translation!]`](../object/translation.md) | Name of the function | | `createdAt` | [`Time!`](../scalar/time.md) | Time when this FunctionDefinition was created | +| `definitionSource` | [`String`](../scalar/string.md) | The source that defines this definition | | `deprecationMessages` | [`[Translation!]`](../object/translation.md) | Deprecation message of the function | | `descriptions` | [`[Translation!]`](../object/translation.md) | Description of the function | | `displayIcon` | [`String`](../scalar/string.md) | Display icon of the function | @@ -24,3 +25,4 @@ Represents a function definition | `signature` | [`String!`](../scalar/string.md) | Signature of the function | | `throwsError` | [`Boolean!`](../scalar/boolean.md) | Indicates if the function can throw an error | | `updatedAt` | [`Time!`](../scalar/time.md) | Time when this FunctionDefinition was last updated | +| `version` | [`String!`](../scalar/string.md) | Version of the runtime function definition | diff --git a/spec/factories/function_definition_data_type_links.rb b/spec/factories/function_definition_data_type_links.rb new file mode 100644 index 00000000..daa64efb --- /dev/null +++ b/spec/factories/function_definition_data_type_links.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :function_definition_data_type_link do + function_definition + referenced_data_type factory: :data_type + end +end diff --git a/spec/factories/function_definitions.rb b/spec/factories/function_definitions.rb index 0f68bd39..d0a7772c 100644 --- a/spec/factories/function_definitions.rb +++ b/spec/factories/function_definitions.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true FactoryBot.define do + sequence(:function_definition_name) { |n| "runtime_function_definition#{n}" } + factory :function_definition do runtime_function_definition + runtime_name { generate(:function_definition_name) } + parameter_definitions { [] } + signature { '(): undefined' } + version { '0.0.0' } + definition_source { 'sagittarius' } + display_icon { nil } end end diff --git a/spec/factories/parameter_definitions.rb b/spec/factories/parameter_definitions.rb index 4cb69880..7e5efb62 100644 --- a/spec/factories/parameter_definitions.rb +++ b/spec/factories/parameter_definitions.rb @@ -3,6 +3,8 @@ FactoryBot.define do factory :parameter_definition do runtime_parameter_definition + runtime_name { runtime_parameter_definition.runtime_name } + runtime_definition_name { runtime_parameter_definition.runtime_name } function_definition do association :function_definition, diff --git a/spec/graphql/types/function_definition_type_spec.rb b/spec/graphql/types/function_definition_type_spec.rb index 31473ba8..b0137245 100644 --- a/spec/graphql/types/function_definition_type_spec.rb +++ b/spec/graphql/types/function_definition_type_spec.rb @@ -19,6 +19,8 @@ throwsError displayIcon linkedDataTypes + version + definitionSource createdAt updatedAt ] diff --git a/spec/requests/grpc/sagittarius/function_definition_service_spec.rb b/spec/requests/grpc/sagittarius/function_definition_service_spec.rb new file mode 100644 index 00000000..cb14b6c8 --- /dev/null +++ b/spec/requests/grpc/sagittarius/function_definition_service_spec.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'sagittarius.FunctionDefinitionService', :need_grpc_server do + include GrpcHelpers + + let(:stub) { create_stub Tucana::Sagittarius::FunctionDefinitionService } + + describe 'Update' do + context 'when create' do + let(:runtime) { create(:runtime) } + let!(:list_data_type) { create(:data_type, identifier: 'LIST', runtime: runtime) } + let!(:predicate_data_type) { create(:data_type, identifier: 'PREDICATE', runtime: runtime) } + let!(:existing_runtime_function_definition) do + create(:runtime_function_definition, + runtime: runtime, + runtime_name: 'std::list::filter', + names: [create(:translation, code: 'en_US', content: 'Old Filter List')]).tap do |definition| + create(:runtime_parameter_definition, runtime_name: 'list', runtime_function_definition: definition) + create(:runtime_parameter_definition, runtime_name: 'predicate', runtime_function_definition: definition) + end + end + + let(:functions) do + [ + { + runtime_definition_name: 'std::list::filter', + runtime_name: 'std::list::filter', + signature: '(list: LIST, predicate: PREDICATE): LIST', + name: [ + { code: 'en_US', content: 'Filter List' } + ], + description: [ + { code: 'en_US', content: 'Filters a list by a predicate' } + ], + documentation: [ + { code: 'en_US', content: 'Filter documentation' } + ], + deprecation_message: [ + { code: 'en_US', content: 'Use filter_v2 instead' } + ], + display_message: [ + { code: 'en_US', content: 'Filter elements in ${list} matching ${predicate}' } + ], + alias: [ + { code: 'en_US', content: 'filter;array;list' } + ], + throws_error: false, + linked_data_type_identifiers: %w[LIST PREDICATE], + parameter_definitions: [ + { + runtime_definition_name: 'list', + runtime_name: 'list', + default_value: nil, + name: [ + { code: 'en_US', content: 'Input List' } + ], + description: [ + { code: 'en_US', content: 'The list to be filtered' } + ], + documentation: [ + { code: 'en_US', content: 'List documentation' } + ], + }, + { + runtime_definition_name: 'predicate', + runtime_name: 'predicate', + default_value: Tucana::Shared::Value.from_ruby({ 'key' => 'value' }), + name: [ + { code: 'en_US', content: 'Filter Predicate' } + ], + description: [ + { code: 'en_US', content: 'A function that returns a boolean' } + ], + documentation: [], + } + ], + version: '0.0.0', + definition_source: 'taurus', + display_icon: 'filter-icon', + } + ] + end + + let(:message) do + Tucana::Sagittarius::FunctionDefinitionUpdateRequest.new(functions: functions) + end + + it 'creates a correct functions' do + expect(stub.update(message, authorization(runtime)).success).to be(true) + + function = FunctionDefinition.last + expect(function.runtime_function_definition).to eq(existing_runtime_function_definition) + expect(function.runtime_name).to eq('std::list::filter') + expect(function.runtime_definition_name).to eq('std::list::filter') + expect(function.signature).to eq('(list: LIST, predicate: PREDICATE): LIST') + expect(function.names.first.content).to eq('Filter List') + expect(function.descriptions.first.content).to eq('Filters a list by a predicate') + expect(function.documentations.first.content).to eq('Filter documentation') + expect(function.deprecation_messages.first.content).to eq('Use filter_v2 instead') + expect(function.aliases.first.content).to eq('filter;array;list') + expect(function.display_messages.first.content).to eq('Filter elements in ${list} matching ${predicate}') + expect(function.throws_error).to be(false) + expect(function.version).to eq('0.0.0') + expect(function.definition_source).to eq('taurus') + expect(function.display_icon).to eq('filter-icon') + expect(function.referenced_data_types).to contain_exactly(list_data_type, predicate_data_type) + + expect(function.parameter_definitions.count).to eq(2) + list_param = function.parameter_definitions.find_by(runtime_name: 'list', runtime_definition_name: 'list') + expect(list_param.names.first.content).to eq('Input List') + expect(list_param.descriptions.first.content).to eq('The list to be filtered') + expect(list_param.documentations.first.content).to eq('List documentation') + expect(list_param.default_value).to be_nil + + predicate_param = function.parameter_definitions.find_by(runtime_name: 'predicate', + runtime_definition_name: 'predicate') + expect(predicate_param.names.first.content).to eq('Filter Predicate') + expect(predicate_param.default_value).to eq({ 'key' => 'value' }) + + expect(ParameterDefinition.count).to eq(2) + list_param_def = ParameterDefinition.find_by(runtime_parameter_definition: list_param) + expect(list_param_def.names.first.content).to eq('Input List') + expect(list_param_def.descriptions.first.content).to eq('The list to be filtered') + expect(list_param_def.documentations.first.content).to eq('List documentation') + expect(list_param_def.default_value).to be_nil + end + end + + context 'when update' do + let(:runtime) { create(:runtime) } + let(:list_data_type) { create(:data_type, identifier: 'LIST', runtime: runtime) } + let!(:existing_runtime_function_definition) do + create(:runtime_function_definition, + runtime: runtime, + runtime_name: 'std::list::filter', + names: [create(:translation, code: 'en_US', content: 'Old Filter List')]).tap do |definition| + create(:runtime_parameter_definition, runtime_name: 'list', runtime_function_definition: definition) + end + end + + let!(:existing_function_definition) do + create(:function_definition, + runtime_function_definition: existing_runtime_function_definition, + runtime_definition_name: 'std::list::filter', + runtime_name: 'std::list::filter', + names: [create(:translation, code: 'en_US', content: 'Filter List')]).tap do |function_definition| + create(:parameter_definition, + function_definition: function_definition, + runtime_definition_name: 'list', + runtime_name: 'list', + runtime_parameter_definition: existing_runtime_function_definition.parameters.first) + end + end + + let(:functions) do + [ + { + runtime_definition_name: 'std::list::filter', + runtime_name: 'std::list::filter', + signature: '(list: LIST): LIST', + name: [ + { code: 'de_DE', content: 'Liste filtern' } + ], + parameter_definitions: [ + { + runtime_definition_name: 'list', + runtime_name: 'some_updated_name', + name: [ + { code: 'de_DE', content: 'Eingabeliste' } + ], + } + ], + linked_data_type_identifiers: [list_data_type.identifier], + version: '0.0.0', + } + ] + end + + let(:message) do + Tucana::Sagittarius::FunctionDefinitionUpdateRequest.new(functions: functions) + end + + context 'when removing parameters' do + let(:functions) do + [ + { + runtime_definition_name: 'std::list::filter', + runtime_name: 'std::list::filter', + signature: '(list: LIST): LIST', + name: [ + { code: 'de_DE', content: 'Liste filtern' } + ], + parameter_definitions: [], + linked_data_type_identifiers: [list_data_type.identifier], + version: '0.0.0', + } + ] + end + + it 'fails' do + expect(stub.update(message, authorization(runtime)).success).to be(false) + expect(FunctionDefinition.first.parameter_definitions.count).to eq(1) + end + end + + it 'creates a correct functions' do + expect(stub.update(message, authorization(runtime)).success).to be(true) + + expect(FunctionDefinition.count).to eq(1) + + function = FunctionDefinition.last + expect(function.id).to eq(existing_function_definition.id) + parameter = function.parameter_definitions.first + expect(parameter.runtime_name).to eq('some_updated_name') + expect(parameter.names.first.content).to eq('Eingabeliste') + + expect(FunctionDefinition.count).to eq(1) + expect(ParameterDefinition.count).to eq(1) + end + end + + context 'when deleting' do + let(:runtime) { create(:runtime) } + + let!(:existing_function_definition) do + create(:function_definition, runtime: runtime) + end + + let(:functions) do + [] + end + + let(:message) do + Tucana::Sagittarius::FunctionDefinitionUpdateRequest.new(functions: functions) + end + + describe 'function definitions' do + it 'marks them as removed' do + expect(stub.update(message, authorization(runtime)).success).to be(true) + + expect(existing_function_definition.reload.removed_at).to be_present + end + end + end + end +end diff --git a/spec/requests/grpc/sagittarius/runtime_function_definition_service_spec.rb b/spec/requests/grpc/sagittarius/runtime_function_definition_service_spec.rb index 79a284a6..8223d571 100644 --- a/spec/requests/grpc/sagittarius/runtime_function_definition_service_spec.rb +++ b/spec/requests/grpc/sagittarius/runtime_function_definition_service_spec.rb @@ -104,21 +104,7 @@ expect(predicate_param.names.first.content).to eq('Filter Predicate') expect(predicate_param.default_value).to eq({ 'key' => 'value' }) - function_definition = FunctionDefinition.first - expect(function_definition.names.first.content).to eq('Filter List') - expect(function_definition.descriptions.first.content).to eq('Filters a list by a predicate') - expect(function_definition.documentations.first.content).to eq('Filter documentation') - expect(function_definition.aliases.first.content).to eq('filter;array;list') - expect(function_definition.display_messages.first.content).to eq( - 'Filter elements in ${list} matching ${predicate}' - ) - - expect(ParameterDefinition.count).to eq(2) - list_param_def = ParameterDefinition.find_by(runtime_parameter_definition: list_param) - expect(list_param_def.names.first.content).to eq('Input List') - expect(list_param_def.descriptions.first.content).to eq('The list to be filtered') - expect(list_param_def.documentations.first.content).to eq('List documentation') - expect(list_param_def.default_value).to be_nil + expect(ParameterDefinition.count).to eq(0) end end @@ -172,8 +158,8 @@ expect(parameter.runtime_name).to eq('list') expect(parameter.names.first.content).to eq('Eingabeliste') - expect(FunctionDefinition.count).to eq(1) - expect(ParameterDefinition.count).to eq(1) + expect(FunctionDefinition.count).to eq(0) + expect(ParameterDefinition.count).to eq(0) end end