From f17ae58c558dfefb99bdfad88098472e70125894 Mon Sep 17 00:00:00 2001 From: Brun Christophe Date: Fri, 22 May 2026 15:30:12 +0200 Subject: [PATCH] refactor(zendesk): hoist collection setup into BaseCollection template Move define_schema/define_relations/add_custom_fields/enable_search/ enable_count orchestration into BaseCollection#initialize. Subclasses shrink to one super call and can opt out of search/count via the searchable: / countable: kwargs. define_schema and define_relations now raise NotImplementedError when a subclass omits them; native_driver is propagated through. Co-Authored-By: Claude Opus 4.7 (1M context) --- .rubocop.yml | 2 + .../collections/base_collection.rb | 22 +++++++++- .../collections/organization.rb | 9 +--- .../collections/ticket.rb | 9 +--- .../collections/user.rb | 9 +--- .../collections/base_collection_spec.rb | 44 +++++++++++++++++++ 6 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/base_collection_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index dcb1f6894..1b9e09e2b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -251,6 +251,7 @@ Naming/PredicatePrefix: Metrics/ParameterLists: Exclude: + - 'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb' - 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/datasource.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/query_handler.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb' @@ -347,6 +348,7 @@ Metrics/BlockLength: Metrics/ClassLength: Exclude: + - 'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb' - 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/collection.rb' - 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/datasource.rb' - 'packages/forest_admin_datasource_snowflake/lib/forest_admin_datasource_snowflake/utils/query.rb' diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb index 72b8c03cb..a9aac3202 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/base_collection.rb @@ -11,6 +11,23 @@ class BaseCollection < ForestAdminDatasourceToolkit::Collection DATE_OPS = [Operators::EQUAL, Operators::BEFORE, Operators::AFTER, Operators::PRESENT, Operators::BLANK].freeze + attr_reader :custom_fields + + # Template method: subclasses implement `define_schema` and + # `define_relations` as hooks; ordering between them, custom-field + # registration, and the search/count flags is owned here so collisions + # are always evaluated against the final native schema. A subclass can + # opt out of search/count by passing `searchable: false` / `countable: + # false` through `super`. + def initialize(datasource, name, custom_fields: [], searchable: true, countable: true, native_driver: nil) + super(datasource, name, native_driver) + define_schema + define_relations + @custom_fields = add_custom_fields(custom_fields) + enable_search if searchable + enable_count if countable + end + def aggregate(caller, filter, aggregation, _limit = nil) unless aggregation.operation == 'Count' && aggregation.field.nil? && aggregation.groups.empty? raise ForestAdminDatasourceToolkit::Exceptions::ForestException, @@ -97,7 +114,7 @@ def build_zendesk_query(caller, filter) # Adds custom fields, skipping any whose column name collides with a # field already declared on the collection (native column or relation). - # Returns the list of fields actually added so callers can keep their + # Returns the subset actually added so callers can keep their # serializer in sync with the schema. def add_custom_fields(custom_fields) custom_fields.reject do |cf| @@ -117,6 +134,9 @@ def add_custom_fields(custom_fields) private + def define_schema = raise(NotImplementedError, "#{self.class} did not implement define_schema") + def define_relations = raise(NotImplementedError, "#{self.class} did not implement define_relations") + def sort_field_and_direction(entry) return [entry.field, entry.ascending] if entry.respond_to?(:field) diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/organization.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/organization.rb index 40935c0d6..9470b61f8 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/organization.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/organization.rb @@ -3,8 +3,6 @@ module Collections class Organization < BaseCollection include Searchable - attr_reader :custom_fields - OneToManySchema = ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema ZENDESK_SORTABLE = { @@ -14,12 +12,7 @@ class Organization < BaseCollection }.freeze def initialize(datasource, custom_fields: []) - super(datasource, 'ZendeskOrganization') - define_schema - define_relations - @custom_fields = add_custom_fields(custom_fields) - enable_search - enable_count + super(datasource, 'ZendeskOrganization', custom_fields: custom_fields) end def create(_caller, data) diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket.rb index 58bb2dc36..342d69f04 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket.rb @@ -6,8 +6,6 @@ class Ticket < BaseCollection include CommentsEmbedder include Serializer - attr_reader :custom_fields - ManyToOneSchema = ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema ZENDESK_SORTABLE = { @@ -29,12 +27,7 @@ class Ticket < BaseCollection }.freeze def initialize(datasource, custom_fields: []) - super(datasource, 'ZendeskTicket') - define_schema - define_relations - @custom_fields = add_custom_fields(custom_fields) - enable_search - enable_count + super(datasource, 'ZendeskTicket', custom_fields: custom_fields) end def list(caller, filter, projection) diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/user.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/user.rb index b65cb85e8..5f2c5c18a 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/user.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/user.rb @@ -3,8 +3,6 @@ module Collections class User < BaseCollection include Searchable - attr_reader :custom_fields - ManyToOneSchema = ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema OneToManySchema = ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema ENUM_ROLE = %w[end-user agent admin].freeze @@ -18,12 +16,7 @@ class User < BaseCollection }.freeze def initialize(datasource, custom_fields: []) - super(datasource, 'ZendeskUser') - define_schema - define_relations - @custom_fields = add_custom_fields(custom_fields) - enable_search - enable_count + super(datasource, 'ZendeskUser', custom_fields: custom_fields) end def create(_caller, data) diff --git a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/base_collection_spec.rb b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/base_collection_spec.rb new file mode 100644 index 000000000..fbd7e5e69 --- /dev/null +++ b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/base_collection_spec.rb @@ -0,0 +1,44 @@ +module ForestAdminDatasourceZendesk + RSpec.describe Collections::BaseCollection do + let(:datasource) do + instance_double(ForestAdminDatasourceZendesk::Datasource, + client: instance_double(ForestAdminDatasourceZendesk::Client), + custom_field_mapping: {}) + end + + describe 'subclass contract' do + it 'raises NotImplementedError naming define_schema when the hook is missing' do + subclass = Class.new(described_class) + expect { subclass.new(datasource, 'X') } + .to raise_error(NotImplementedError, /define_schema/) + end + + it 'raises NotImplementedError naming define_relations when only define_schema is implemented' do + subclass = Class.new(described_class) { def define_schema; end } + expect { subclass.new(datasource, 'X') } + .to raise_error(NotImplementedError, /define_relations/) + end + end + + describe 'search/count opt-out' do + let(:subclass) do + Class.new(described_class) do + def define_schema; end + def define_relations; end + end + end + + it 'enables search and count by default' do + collection = subclass.new(datasource, 'X') + expect(collection.is_searchable?).to be(true) + expect(collection.is_countable?).to be(true) + end + + it 'honours searchable: false / countable: false from super' do + collection = subclass.new(datasource, 'X', searchable: false, countable: false) + expect(collection.is_searchable?).to be(false) + expect(collection.is_countable?).to be(false) + end + end + end +end