Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions server/app/controllers/api/v1/connectors_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions server/app/interactors/connectors/delete_connector.rb
Original file line number Diff line number Diff line change
@@ -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
169 changes: 169 additions & 0 deletions server/spec/interactors/connectors/delete_connector_spec.rb
Original file line number Diff line number Diff line change
@@ -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
92 changes: 91 additions & 1 deletion server/spec/requests/api/v1/connectors_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading