From 8a5f34fb2c4806f61c5bf43706c12e6fc6d1a9b4 Mon Sep 17 00:00:00 2001 From: Brun Christophe Date: Fri, 22 May 2026 11:18:09 +0200 Subject: [PATCH 1/2] fix(zendesk): warn instead of crashing on custom-field name collisions Skip Zendesk custom fields whose key collides with a native column or relation (e.g. a user_field named "role") and log a warning, so agent setup is not blocked. The filtered list flows back into the datasource search-mapping to keep schema and query translation in sync. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../collections/base_collection.rb | 20 ++++++++++++ .../collections/organization.rb | 6 ++-- .../collections/ticket.rb | 4 ++- .../collections/ticket/schema_definition.rb | 2 -- .../collections/user.rb | 6 ++-- .../datasource.rb | 15 ++++----- .../collections/user_spec.rb | 31 +++++++++++++++++++ 7 files changed, 68 insertions(+), 16 deletions(-) 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 72174ba91..f24730467 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 @@ -95,6 +95,26 @@ def build_zendesk_query(caller, filter) [translated, filter.search].compact.reject(&:empty?).join(' ') end + # 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 + # serializer in sync with the schema. + def add_custom_fields(custom_fields) + custom_fields.reject do |cf| + name = cf[:column_name] + if schema[:fields].key?(name) + ForestAdminDatasourceZendesk.logger.warn( + "[forest_admin_datasource_zendesk] Custom field '#{name}' on collection " \ + "'#{self.name}' conflicts with an existing field; skipping." + ) + true + else + add_field(name, cf[:schema]) + false + end + end + end + private def sort_field_and_direction(entry) 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 a47e4ff67..40935c0d6 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,6 +3,8 @@ module Collections class Organization < BaseCollection include Searchable + attr_reader :custom_fields + OneToManySchema = ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema ZENDESK_SORTABLE = { @@ -13,9 +15,9 @@ class Organization < BaseCollection def initialize(datasource, custom_fields: []) super(datasource, 'ZendeskOrganization') - @custom_fields = custom_fields define_schema define_relations + @custom_fields = add_custom_fields(custom_fields) enable_search enable_count end @@ -79,8 +81,6 @@ def define_schema is_read_only: true, is_sortable: true)) add_field('updated_at', ColumnSchema.new(column_type: 'Date', filter_operators: DATE_OPS, is_read_only: true, is_sortable: true)) - - @custom_fields.each { |cf| add_field(cf[:column_name], cf[:schema]) } end def define_relations 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 ac9d7e6a2..58bb2dc36 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,6 +6,8 @@ class Ticket < BaseCollection include CommentsEmbedder include Serializer + attr_reader :custom_fields + ManyToOneSchema = ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema ZENDESK_SORTABLE = { @@ -28,9 +30,9 @@ class Ticket < BaseCollection def initialize(datasource, custom_fields: []) super(datasource, 'ZendeskTicket') - @custom_fields = custom_fields define_schema define_relations + @custom_fields = add_custom_fields(custom_fields) enable_search enable_count end diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket/schema_definition.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket/schema_definition.rb index cb21ebb51..9110ac7b9 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket/schema_definition.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/collections/ticket/schema_definition.rb @@ -46,8 +46,6 @@ def define_schema is_read_only: true, is_sortable: true)) add_field('updated_at', ColumnSchema.new(column_type: 'Date', filter_operators: DATE_OPS, is_read_only: true, is_sortable: true)) - - @custom_fields.each { |cf| add_field(cf[:column_name], cf[:schema]) } end def define_relations 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 bb20d9f92..b65cb85e8 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,6 +3,8 @@ 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 @@ -17,9 +19,9 @@ class User < BaseCollection def initialize(datasource, custom_fields: []) super(datasource, 'ZendeskUser') - @custom_fields = custom_fields define_schema define_relations + @custom_fields = add_custom_fields(custom_fields) enable_search enable_count end @@ -89,8 +91,6 @@ def define_schema is_read_only: true, is_sortable: true)) add_field('updated_at', ColumnSchema.new(column_type: 'Date', filter_operators: DATE_OPS, is_read_only: true, is_sortable: true)) - - @custom_fields.each { |cf| add_field(cf[:column_name], cf[:schema]) } end def define_relations diff --git a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/datasource.rb b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/datasource.rb index 396d15490..8ec7cdb17 100644 --- a/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/datasource.rb +++ b/packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/datasource.rb @@ -16,15 +16,16 @@ def initialize(subdomain:, username:, token:) def register_collections introspector = Schema::CustomFieldsIntrospector.new(@client) - ticket_cf = introspector.ticket_custom_fields - user_cf = introspector.user_custom_fields - org_cf = introspector.organization_custom_fields + ticket = Collections::Ticket.new(self, custom_fields: introspector.ticket_custom_fields) + user = Collections::User.new(self, custom_fields: introspector.user_custom_fields) + org = Collections::Organization.new(self, custom_fields: introspector.organization_custom_fields) - add_collection(Collections::Ticket.new(self, custom_fields: ticket_cf)) - add_collection(Collections::User.new(self, custom_fields: user_cf)) - add_collection(Collections::Organization.new(self, custom_fields: org_cf)) + add_collection(ticket) + add_collection(user) + add_collection(org) - @custom_field_mapping = build_custom_field_mapping(ticket_cf, user_cf, org_cf) + @custom_field_mapping = build_custom_field_mapping(ticket.custom_fields, + user.custom_fields, org.custom_fields) end # Forest column name -> Zendesk Search field name. Lives on the instance diff --git a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb index d34d96445..24f1f7fe3 100644 --- a/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb +++ b/packages/forest_admin_datasource_zendesk/spec/forest_admin_datasource_zendesk/collections/user_spec.rb @@ -115,6 +115,37 @@ def zendesk_user(attrs) collection.create(nil, 'email' => 'a@b.com', 'tier' => 'gold') expect(client).to have_received(:create_user) end + + context 'when a custom field collides with a native field' do + let(:cf) do + [{ column_name: 'role', zendesk_id: 42, zendesk_key: 'role', + schema: ForestAdminDatasourceToolkit::Schema::ColumnSchema.new( + column_type: 'String', filter_operators: [], is_read_only: false + ) }] + end + + it 'logs a warning and skips the conflicting custom field instead of raising' do + logger = instance_double(Logger, warn: nil) + allow(ForestAdminDatasourceZendesk).to receive(:logger).and_return(logger) + + expect { collection }.not_to raise_error + expect(logger).to have_received(:warn).with(/Custom field 'role'.*conflicts/) + end + + it 'keeps the native role enum schema on the collection' do + allow(ForestAdminDatasourceZendesk).to receive(:logger).and_return(instance_double(Logger, warn: nil)) + expect(collection.schema[:fields]['role'].column_type).to eq('Enum') + end + + it 'does not overwrite the native role value when serializing' do + allow(ForestAdminDatasourceZendesk).to receive(:logger).and_return(instance_double(Logger, warn: nil)) + user = zendesk_user('id' => 1, 'role' => 'admin', 'user_fields' => { 'role' => 'something-else' }) + allow(client).to receive(:search).and_return([user]) + + result = collection.list(nil, Filter.new, nil).first + expect(result['role']).to eq('admin') + end + end end describe '#create' do From 3810cf0d75e12d7180683720ed0d3f22af8cda57 Mon Sep 17 00:00:00 2001 From: Brun Christophe Date: Fri, 22 May 2026 11:51:48 +0200 Subject: [PATCH 2/2] refactor(zendesk): rename local var to avoid shadowing collection name --- .../collections/base_collection.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 f24730467..72b8c03cb 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 @@ -101,15 +101,15 @@ def build_zendesk_query(caller, filter) # serializer in sync with the schema. def add_custom_fields(custom_fields) custom_fields.reject do |cf| - name = cf[:column_name] - if schema[:fields].key?(name) + column_name = cf[:column_name] + if schema[:fields].key?(column_name) ForestAdminDatasourceZendesk.logger.warn( - "[forest_admin_datasource_zendesk] Custom field '#{name}' on collection " \ - "'#{self.name}' conflicts with an existing field; skipping." + "[forest_admin_datasource_zendesk] Custom field '#{column_name}' on collection " \ + "'#{name}' conflicts with an existing field; skipping." ) true else - add_field(name, cf[:schema]) + add_field(column_name, cf[:schema]) false end end