diff --git a/server/app/controllers/api/v1/connectors_controller.rb b/server/app/controllers/api/v1/connectors_controller.rb index 5c4c6a33c..11e95fb68 100644 --- a/server/app/controllers/api/v1/connectors_controller.rb +++ b/server/app/controllers/api/v1/connectors_controller.rb @@ -77,8 +77,17 @@ def destroy authorize @connector @action = "delete" @audit_resource = @connector.name - @connector.destroy! - head :no_content + + result = DeleteConnector.call(connector: @connector) + + if result.success? + head :no_content + else + render_error( + message: result.error, + status: :unprocessable_content + ) + end end def discover diff --git a/server/app/interactors/connectors/delete_connector.rb b/server/app/interactors/connectors/delete_connector.rb new file mode 100644 index 000000000..e3c8f6e14 --- /dev/null +++ b/server/app/interactors/connectors/delete_connector.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Connectors + class DeleteConnector + include Interactor + + def call + check_dependencies + delete_connector if context.success? + end + + private + + def check_dependencies + dependencies = [] + dependencies << "models" if context.connector.models.exists? + dependencies << "workflow components" if used_in_workflow_components? + + return if dependencies.empty? + + context.fail!( + error: "Cannot delete connector. This connector is used in #{dependencies.join(', ')}. " \ + "Please delete or update the associated resources first." + ) + end + + def used_in_workflow_components? + # Check if connector is referenced in any workflow component configurations + Agents::Component.where(workspace_id: context.connector.workspace_id).find_each do |component| + config = component.configuration || {} + connector_keys = %w[llm_model database llm_connector_id judge_llm_connector_id] + return true if connector_keys.any? { |key| config[key]&.to_i == context.connector.id } + end + false + end + + def delete_connector + context.connector.destroy! + end + end +end diff --git a/server/spec/interactors/connectors/delete_connector_spec.rb b/server/spec/interactors/connectors/delete_connector_spec.rb new file mode 100644 index 000000000..fd92c4f60 --- /dev/null +++ b/server/spec/interactors/connectors/delete_connector_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Connectors::DeleteConnector do + let(:workspace) { create(:workspace) } + let(:connector) { create(:connector, workspace:) } + + describe "#call" do + context "when connector has no dependencies" do + it "successfully deletes the connector" do + result = described_class.call(connector:) + expect(result.success?).to eq(true) + expect(Connector.exists?(connector.id)).to be false + end + end + + context "when connector is linked to models" do + let!(:model) { create(:model, connector:, workspace:) } + + it "fails to delete the connector" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("Cannot delete connector") + expect(result.error).to include("models") + expect(Connector.exists?(connector.id)).to be true + end + + it "does not delete the model" do + described_class.call(connector:) + expect(Model.exists?(model.id)).to be true + end + end + + context "when connector is used in workflow components" do + let(:workflow) { create(:workflow, workspace:) } + + context "with llm_model component type" do + let!(:component) do + create(:component, workspace:, workflow:, component_type: :llm_model, + configuration: { "llm_model" => connector.id }) + end + + it "fails to delete the connector" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("Cannot delete connector") + expect(result.error).to include("workflow components") + expect(Connector.exists?(connector.id)).to be true + end + end + + context "with data_storage component type" do + let!(:component) do + create(:component, workspace:, workflow:, component_type: :data_storage, + configuration: { "database" => connector.id }) + end + + it "fails to delete the connector" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("workflow components") + end + end + + context "with vector_store component type" do + let!(:component) do + create(:component, workspace:, workflow:, component_type: :vector_store, + configuration: { "database" => connector.id }) + end + + it "fails to delete the connector" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("workflow components") + end + end + + context "with agent component type" do + let!(:component) do + create(:component, workspace:, workflow:, component_type: :agent, + configuration: { "llm_connector_id" => connector.id }) + end + + it "fails to delete the connector" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("workflow components") + end + end + + context "with llm_router component type" do + let!(:component) do + create(:component, workspace:, workflow:, component_type: :llm_router, + configuration: { "judge_llm_connector_id" => connector.id }) + end + + it "fails to delete the connector" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("workflow components") + end + end + end + + context "when connector has multiple dependencies" do + let(:workflow) { create(:workflow, workspace:) } + let!(:model) { create(:model, connector:, workspace:) } + let!(:component) do + create(:component, workspace:, workflow:, component_type: :llm_model, + configuration: { "llm_model" => connector.id }) + end + + it "fails to delete and lists all dependencies" do + result = described_class.call(connector:) + expect(result.failure?).to eq(true) + expect(result.error).to include("Cannot delete connector") + expect(result.error).to include("models") + expect(result.error).to include("workflow components") + end + + it "does not delete the connector" do + described_class.call(connector:) + expect(Connector.exists?(connector.id)).to be true + end + + it "does not delete associated resources" do + described_class.call(connector:) + expect(Model.exists?(model.id)).to be true + end + end + + context "when component references a different connector" do + let(:workflow) { create(:workflow, workspace:) } + let(:other_connector) { create(:connector, workspace:) } + let!(:component) do + create(:component, workspace:, workflow:, component_type: :llm_model, + configuration: { "llm_model" => other_connector.id }) + end + + it "successfully deletes the connector" do + result = described_class.call(connector:) + expect(result.success?).to eq(true) + expect(Connector.exists?(connector.id)).to be false + end + + it "does not affect the other connector" do + described_class.call(connector:) + expect(Connector.exists?(other_connector.id)).to be true + end + end + + context "when connector belongs to different workspace" do + let(:other_workspace) { create(:workspace) } + let(:other_workflow) { create(:workflow, workspace: other_workspace) } + let!(:other_component) do + create(:component, workspace: other_workspace, workflow: other_workflow, + component_type: :llm_model, + configuration: { "llm_model" => 999 }) + end + + it "successfully deletes the connector" do + result = described_class.call(connector:) + expect(result.success?).to eq(true) + expect(Connector.exists?(connector.id)).to be false + end + end + end +end diff --git a/server/spec/requests/api/v1/connectors_controller_spec.rb b/server/spec/requests/api/v1/connectors_controller_spec.rb index 30df38785..e128a9e58 100644 --- a/server/spec/requests/api/v1/connectors_controller_spec.rb +++ b/server/spec/requests/api/v1/connectors_controller_spec.rb @@ -554,12 +554,102 @@ expect(audit_log.updated_at).not_to be_nil end - it "returns fail viwer role" do + it "returns fail viewer role" do workspace.workspace_users.first.update(role: viewer_role) delete "/api/v1/connectors/#{connectors.first.id}", headers: auth_headers(user, workspace_id) expect(response).to have_http_status(:forbidden) end + it "returns error when connector has associated models" do + source_connector = connectors.find { |c| c.connector_type == "source" } + create(:model, connector: source_connector, workspace:) + + delete "/api/v1/connectors/#{source_connector.id}", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:unprocessable_content) + + response_hash = JSON.parse(response.body).with_indifferent_access + expect(response_hash[:errors]).to be_present + expect(response_hash[:errors].first[:detail]).to include("Cannot delete connector") + expect(response_hash[:errors].first[:detail]).to include("models") + end + + it "returns error when connector is used in workflow components (llm_model)" do + source_connector = connectors.find { |c| c.connector_type == "source" } + workflow = create(:workflow, workspace:) + create(:component, workspace:, workflow:, component_type: :llm_model, + configuration: { "llm_model" => source_connector.id }) + + delete "/api/v1/connectors/#{source_connector.id}", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:unprocessable_content) + + response_hash = JSON.parse(response.body).with_indifferent_access + expect(response_hash[:errors]).to be_present + expect(response_hash[:errors].first[:detail]).to include("Cannot delete connector") + expect(response_hash[:errors].first[:detail]).to include("workflow components") + end + + it "returns error when connector is used in workflow components (data_storage)" do + source_connector = connectors.find { |c| c.connector_type == "source" } + workflow = create(:workflow, workspace:) + create(:component, workspace:, workflow:, component_type: :data_storage, + configuration: { "database" => source_connector.id }) + + delete "/api/v1/connectors/#{source_connector.id}", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:unprocessable_content) + + response_hash = JSON.parse(response.body).with_indifferent_access + expect(response_hash[:errors]).to be_present + expect(response_hash[:errors].first[:detail]).to include("Cannot delete connector") + expect(response_hash[:errors].first[:detail]).to include("workflow components") + end + + it "returns error when connector is used in workflow components (vector_store)" do + source_connector = connectors.find { |c| c.connector_type == "source" } + workflow = create(:workflow, workspace:) + create(:component, workspace:, workflow:, component_type: :vector_store, + configuration: { "database" => source_connector.id }) + + delete "/api/v1/connectors/#{source_connector.id}", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:unprocessable_content) + + response_hash = JSON.parse(response.body).with_indifferent_access + expect(response_hash[:errors]).to be_present + expect(response_hash[:errors].first[:detail]).to include("Cannot delete connector") + expect(response_hash[:errors].first[:detail]).to include("workflow components") + end + + it "returns error when connector is used in workflow components (agent)" do + source_connector = connectors.find { |c| c.connector_type == "source" } + workflow = create(:workflow, workspace:) + create(:component, workspace:, workflow:, component_type: :agent, + configuration: { "llm_connector_id" => source_connector.id }) + + delete "/api/v1/connectors/#{source_connector.id}", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:unprocessable_content) + + response_hash = JSON.parse(response.body).with_indifferent_access + expect(response_hash[:errors]).to be_present + expect(response_hash[:errors].first[:detail]).to include("Cannot delete connector") + expect(response_hash[:errors].first[:detail]).to include("workflow components") + end + + it "returns error with multiple dependencies listed" do + source_connector = connectors.find { |c| c.connector_type == "source" } + create(:model, connector: source_connector, workspace:) + workflow = create(:workflow, workspace:) + create(:component, workspace:, workflow:, component_type: :llm_model, + configuration: { "llm_model" => source_connector.id }) + + delete "/api/v1/connectors/#{source_connector.id}", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:unprocessable_content) + + response_hash = JSON.parse(response.body).with_indifferent_access + expect(response_hash[:errors]).to be_present + expect(response_hash[:errors].first[:detail]).to include("Cannot delete connector") + expect(response_hash[:errors].first[:detail]).to include("models") + expect(response_hash[:errors].first[:detail]).to include("workflow components") + end + it "returns an error response while delete wrong connector" do delete "/api/v1/connectors/test", headers: auth_headers(user, workspace_id) expect(response).to have_http_status(:bad_request)