diff --git a/server/app/controllers/api/v1/connectors_controller.rb b/server/app/controllers/api/v1/connectors_controller.rb index 5c4c6a33c..0ef7d5d11 100644 --- a/server/app/controllers/api/v1/connectors_controller.rb +++ b/server/app/controllers/api/v1/connectors_controller.rb @@ -193,6 +193,22 @@ def connector_params :connector_name, :name, :description, :query_type, configuration: {}) end +<<<<<<< HEAD +======= + + def filter_params + { + workspace: current_workspace, + type: params[:type], + category: params[:category], + sub_category: params[:sub_category], + exclude_sub_category: params[:exclude_sub_category], + provider: params[:provider], + page: params[:page], + per_page: params[:per_page] + } + end +>>>>>>> f6d71341a (fix(CE): added an extra filter in connectors api for excluding on basis of sub category (#1893)) end # rubocop:enable Metrics/ClassLength end diff --git a/server/app/interactors/connector_definitions/filter_connector_definitions.rb b/server/app/interactors/connector_definitions/filter_connector_definitions.rb index 0ebbcfeea..0e0ac95d9 100644 --- a/server/app/interactors/connector_definitions/filter_connector_definitions.rb +++ b/server/app/interactors/connector_definitions/filter_connector_definitions.rb @@ -4,6 +4,22 @@ module ConnectorDefinitions class FilterConnectorDefinitions include Interactor +<<<<<<< HEAD +======= + HOSTED_DATA_STORES = { + "AI Squared Vector Store" => "Postgresql", + "AI Squared Database" => "Postgresql" + }.freeze + + SUB_CATEGORY_MAP = { + "llm" => Connector::LLM_SUB_CATEGORIES, + "database" => Connector::DATABASE_SUB_CATEGORIES, + "web" => Connector::WEB_SUB_CATEGORIES, + "ai_ml_service" => Connector::AI_ML_SERVICE_SUB_CATEGORIES, + "vector" => Connector::VECTOR_SUB_CATEGORIES + }.freeze + +>>>>>>> f6d71341a (fix(CE): added an extra filter in connectors api for excluding on basis of sub category (#1893)) def call context.connectors = Multiwoven::Integrations::Service.connectors.with_indifferent_access @@ -24,6 +40,16 @@ def filter_connectors_by_category end context.connectors[:source] = filter_by_category(context.connectors[:source], categories) context.connectors[:destination] = filter_by_category(context.connectors[:destination], categories) +<<<<<<< HEAD +======= + context.connectors[:destination] = filter_hosted_data_stores(context.connectors[:destination], context.workspace) + end + + def filter_connectors_by_sub_category + sub_categories = SUB_CATEGORY_MAP.fetch(context.sub_category, [context.sub_category]) + context.connectors[:source] = filter_by_sub_category(context.connectors[:source], sub_categories) + context.connectors[:destination] = filter_by_sub_category(context.connectors[:destination], sub_categories) +>>>>>>> f6d71341a (fix(CE): added an extra filter in connectors api for excluding on basis of sub category (#1893)) end def filter_by_category(connectors, categories) diff --git a/server/app/interactors/connectors/filter_connectors.rb b/server/app/interactors/connectors/filter_connectors.rb new file mode 100644 index 000000000..038098efb --- /dev/null +++ b/server/app/interactors/connectors/filter_connectors.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Connectors + class FilterConnectors + include Interactor + + def call + connectors = context.workspace.connectors + + connectors = filter_by_type(connectors) + connectors = filter_by_category(connectors) + connectors = filter_by_sub_category(connectors) + connectors = filter_by_exclude_sub_category(connectors) + connectors = filter_by_provider(connectors) + connectors = paginate(connectors) + + context.connectors = connectors + end + + private + + def filter_by_type(connectors) + return connectors if context.type.blank? + + connectors.send(context.type.downcase) + end + + def filter_by_category(connectors) + return connectors if context.category.blank? + + connectors.send(context.category.downcase) + end + + def filter_by_exclude_sub_category(connectors) + return connectors if context.exclude_sub_category.blank? + + sub_categories = ConnectorDefinitions::FilterConnectorDefinitions::SUB_CATEGORY_MAP.fetch( + context.exclude_sub_category.downcase, [context.exclude_sub_category.downcase] + ) + connectors.where.not(connector_sub_category: sub_categories) + end + + def filter_by_sub_category(connectors) + return connectors if context.sub_category.blank? + + connectors.send(context.sub_category.downcase) + end + + def filter_by_provider(connectors) + return connectors if context.provider.blank? + + connectors.where(connector_name: context.provider) + end + + def paginate(connectors) + page = context.page || 1 + per_page = context.per_page + + connectors.page(page).per(per_page) + end + end +end diff --git a/server/spec/interactors/connectors/filter_connectors_spec.rb b/server/spec/interactors/connectors/filter_connectors_spec.rb new file mode 100644 index 000000000..78f29547f --- /dev/null +++ b/server/spec/interactors/connectors/filter_connectors_spec.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Connectors::FilterConnectors do + let(:workspace) { create(:workspace) } + + let!(:source_connector) do + create(:connector, workspace:, connector_type: "source", connector_name: "Snowflake") + end + + let!(:destination_connector) do + create(:connector, workspace:, connector_type: "destination", connector_name: "Klaviyo") + end + + describe "#call" do + context "without any filters" do + it "returns all connectors for the workspace" do + result = described_class.call(workspace:) + + expect(result.success?).to be true + expect(result.connectors).to include(source_connector, destination_connector) + end + end + + context "with type filter" do + it "returns only source connectors when type is source" do + result = described_class.call(workspace:, type: "source") + + expect(result.success?).to be true + expect(result.connectors).to include(source_connector) + expect(result.connectors).not_to include(destination_connector) + end + + it "returns only destination connectors when type is destination" do + result = described_class.call(workspace:, type: "destination") + + expect(result.success?).to be true + expect(result.connectors).to include(destination_connector) + expect(result.connectors).not_to include(source_connector) + end + + it "handles uppercase type filter" do + result = described_class.call(workspace:, type: "SOURCE") + + expect(result.success?).to be true + expect(result.connectors).to include(source_connector) + expect(result.connectors).not_to include(destination_connector) + end + end + + context "with category filter" do + let!(:ai_ml_connector) do + create(:connector, workspace:, connector_type: "source", + connector_name: "OpenAI", connector_category: "AI Model") + end + + let!(:data_connector) do + create(:connector, workspace:, connector_type: "source", + connector_name: "Postgres", connector_category: "Database") + end + + it "returns only ai_ml connectors when category is ai_ml" do + result = described_class.call(workspace:, category: "ai_ml") + + expect(result.success?).to be true + expect(result.connectors).to include(ai_ml_connector) + expect(result.connectors).not_to include(data_connector) + end + + it "returns only data connectors when category is data" do + result = described_class.call(workspace:, category: "data") + + expect(result.success?).to be true + expect(result.connectors).to include(data_connector) + expect(result.connectors).not_to include(ai_ml_connector) + end + end + + context "with sub_category filter" do + let!(:llm_connector) do + create(:connector, workspace:, connector_type: "source", + connector_name: "OpenAI", connector_sub_category: "LLM") + end + + let!(:database_connector) do + create(:connector, workspace:, connector_type: "source", + connector_name: "Postgres", connector_sub_category: "Relational Database") + end + + it "returns only llm connectors when sub_category is llm" do + result = described_class.call(workspace:, sub_category: "llm") + + expect(result.success?).to be true + expect(result.connectors).to include(llm_connector) + end + + it "returns only database connectors when sub_category is database" do + result = described_class.call(workspace:, sub_category: "database") + + expect(result.success?).to be true + expect(result.connectors).to include(database_connector) + end + end + + context "with exclude_sub_category filter" do + let!(:llm_connector) do + create(:connector, workspace:, connector_type: "source", + connector_name: "OpenAI", connector_sub_category: "LLM") + end + + let!(:database_connector) do + create(:connector, workspace:, connector_type: "source", + connector_name: "Postgres", connector_sub_category: "Relational Database") + end + + it "excludes database connectors when exclude_sub_category is database" do + result = described_class.call(workspace:, exclude_sub_category: "database") + + expect(result.connectors).to include(llm_connector) + expect(result.connectors).not_to include(database_connector) + end + + it "excludes llm connectors when exclude_sub_category is llm" do + result = described_class.call(workspace:, exclude_sub_category: "llm") + + expect(result.connectors).to include(database_connector) + expect(result.connectors).not_to include(llm_connector) + end + + it "returns all connectors when exclude_sub_category is blank" do + result = described_class.call(workspace:, exclude_sub_category: "") + + expect(result.connectors).to include(llm_connector, database_connector) + end + end + + context "with provider filter" do + it "returns connectors matching the provider name" do + result = described_class.call(workspace:, provider: "Snowflake") + + expect(result.success?).to be true + expect(result.connectors).to include(source_connector) + expect(result.connectors).not_to include(destination_connector) + end + + it "returns empty when provider does not match any connector" do + result = described_class.call(workspace:, provider: "NonExistent") + + expect(result.success?).to be true + expect(result.connectors).to be_empty + end + end + + context "with multiple filters" do + let!(:another_source_connector) do + create(:connector, workspace:, connector_type: "source", connector_name: "Postgres") + end + + it "applies all filters together" do + result = described_class.call(workspace:, type: "source", provider: "Snowflake") + + expect(result.success?).to be true + expect(result.connectors).to include(source_connector) + expect(result.connectors).not_to include(destination_connector) + expect(result.connectors).not_to include(another_source_connector) + end + end + + context "with pagination" do + it "paginates results with specified page and per_page" do + result = described_class.call(workspace:, page: 1, per_page: 1) + + expect(result.success?).to be true + expect(result.connectors.size).to eq(1) + end + + it "returns second page of results" do + result = described_class.call(workspace:, page: 2, per_page: 1) + + expect(result.success?).to be true + expect(result.connectors.size).to eq(1) + end + + it "defaults to page 1 when page is not provided" do + result = described_class.call(workspace:) + + expect(result.success?).to be true + expect(result.connectors).not_to be_empty + end + + it "returns empty when page exceeds available results" do + result = described_class.call(workspace:, page: 100, per_page: 10) + + expect(result.success?).to be true + expect(result.connectors).to be_empty + 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..5368c3776 100644 --- a/server/spec/requests/api/v1/connectors_controller_spec.rb +++ b/server/spec/requests/api/v1/connectors_controller_spec.rb @@ -180,6 +180,53 @@ expect(result["data"].count).to eql(0) end +<<<<<<< HEAD +======= + it "returns connectors filtered by provider with pagination" do + get "/api/v1/connectors?provider=Klaviyo&page=1&per_page=10", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:ok) + result = JSON.parse(response.body) + expect(result["data"].count).to eql(1) + expect(result["data"].first.dig("attributes", "connector_name")).to eq("Klaviyo") + expect(result.dig("links", "first")).to include("page=1") + end + + it "returns connectors excluding llm sub_category" do + get "/api/v1/connectors?exclude_sub_category=llm", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:ok) + result = JSON.parse(response.body) + ids = result["data"].map { |c| c["id"] } + expect(ids).not_to include(llm_connector.id.to_s) + expect(ids).to include(vector_connector.id.to_s) + end + + it "returns connectors excluding vector sub_category" do + get "/api/v1/connectors?exclude_sub_category=vector", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:ok) + result = JSON.parse(response.body) + ids = result["data"].map { |c| c["id"] } + expect(ids).not_to include(vector_connector.id.to_s) + expect(ids).to include(llm_connector.id.to_s) + end + + it "returns connectors excluding ai_ml_service sub_category" do + get "/api/v1/connectors?exclude_sub_category=ai_ml_service", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:ok) + result = JSON.parse(response.body) + ids = result["data"].map { |c| c["id"] } + expect(ids).not_to include(ai_ml_service_connector.id.to_s) + end + + it "returns source connectors excluding llm sub_category" do + get "/api/v1/connectors?type=source&exclude_sub_category=llm", headers: auth_headers(user, workspace_id) + expect(response).to have_http_status(:ok) + result = JSON.parse(response.body) + ids = result["data"].map { |c| c["id"] } + expect(ids).not_to include(llm_connector.id.to_s) + expect(result["data"].all? { |c| c.dig("attributes", "connector_type") == "source" }).to be true + end + +>>>>>>> f6d71341a (fix(CE): added an extra filter in connectors api for excluding on basis of sub category (#1893)) it "returns an error response for connectors" do get "/api/v1/connectors?type=destination1", headers: auth_headers(user, workspace_id) expect(response).to have_http_status(:bad_request)