Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
- forest_admin_datasource_mongoid
- forest_admin_rpc_agent
- forest_admin_datasource_rpc
- forest_admin_datasource_zendesk

steps:
- name: Checkout
Expand Down Expand Up @@ -66,6 +67,7 @@ jobs:
- forest_admin_datasource_mongoid
- forest_admin_rpc_agent
- forest_admin_datasource_rpc
- forest_admin_datasource_zendesk
services:
mongodb:
image: mongo:latest
Expand Down Expand Up @@ -129,7 +131,7 @@ jobs:
with:
verbose: true
oidc: true
files: ${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_active_record/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_customizer/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_toolkit/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_mongoid/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_rpc_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_rpc/coverage.json
files: ${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_active_record/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_customizer/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_toolkit/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_mongoid/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_rpc_agent/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_rpc/coverage.json,${{ github.workspace }}/reports/${{ matrix.ruby-version }}-forest_admin_datasource_zendesk/coverage.json

deploy:
name: Release package
Expand Down
7 changes: 5 additions & 2 deletions .releaserc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ module.exports = {
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_rails/lib/forest_admin_rails/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/version.rb; ',
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/version.rb; '+
'sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/version.rb; ',
successCmd:
'( cd packages/forest_admin_agent && gem build && gem push forest_admin_agent-*.gem );' +
'( cd packages/forest_admin_datasource_active_record && gem build && gem push forest_admin_datasource_active_record-*.gem );' +
Expand All @@ -37,7 +38,8 @@ module.exports = {
'( cd packages/forest_admin_rails && gem build && gem push forest_admin_rails-*.gem );' +
'( cd packages/forest_admin_datasource_mongoid && gem build && gem push forest_admin_datasource_mongoid-*.gem );' +
'( cd packages/forest_admin_rpc_agent && gem build && gem push forest_admin_rpc_agent-*.gem );' +
'( cd packages/forest_admin_datasource_rpc && gem build && gem push forest_admin_datasource_rpc-*.gem );' ,
'( cd packages/forest_admin_datasource_rpc && gem build && gem push forest_admin_datasource_rpc-*.gem );' +
'( cd packages/forest_admin_datasource_zendesk && gem build && gem push forest_admin_datasource_zendesk-*.gem );' ,
},
],
[
Expand All @@ -56,6 +58,7 @@ module.exports = {
'packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/version.rb',
'packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/version.rb',
'packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/version.rb',
'packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/version.rb',
'package.json'
],
},
Expand Down
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Gemspec/RequireMFA:
- 'packages/forest_admin_test_toolkit/forest_admin_test_toolkit.gemspec'
- 'packages/forest_admin_datasource_customizer/forest_admin_datasource_customizer.gemspec'
- 'packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec'
- 'packages/forest_admin_datasource_zendesk/forest_admin_datasource_zendesk.gemspec'

# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Expand Down Expand Up @@ -253,6 +254,7 @@ Metrics/ModuleLength:
- 'packages/forest_admin_datasource_rpc/spec/**/*'
- 'packages/forest_admin_datasource_mongoid/spec/**/*'
- 'packages/forest_admin_datasource_customizer/spec/**/*'
- 'packages/forest_admin_datasource_zendesk/spec/**/*'
- 'packages/forest_admin_rails/spec/**/*'
- 'packages/forest_admin_rpc_agent/spec/**/*'
- 'packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/helpers.rb'
Expand Down
7 changes: 7 additions & 0 deletions packages/forest_admin_datasource_zendesk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.gem
.bundle/
Gemfile.lock
coverage/
pkg/
tmp/
.rspec_status
3 changes: 3 additions & 0 deletions packages/forest_admin_datasource_zendesk/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
13 changes: 13 additions & 0 deletions packages/forest_admin_datasource_zendesk/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
source 'https://rubygems.org'

gemspec

gem 'forest_admin_datasource_toolkit'
gem 'rake', '~> 13.0'
gem 'rubocop', '~> 1.21'

group :development, :test do
gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'webmock', '~> 3.0'
end
16 changes: 16 additions & 0 deletions packages/forest_admin_datasource_zendesk/Gemfile-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in forest_admin_datasource_zendesk.gemspec
gemspec

gem 'rake', '~> 13.0'
gem 'rubocop', '~> 1.21'

group :development, :test do
gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit'
gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-html', '~> 0.12.3'
gem 'simplecov_json_formatter', '~> 0.1.4'
gem 'webmock', '~> 3.0'
end
6 changes: 6 additions & 0 deletions packages/forest_admin_datasource_zendesk/Rakefile
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the Rakefile here isn't actually used (CI and release both bypass rake). Worth dropping?

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task default: :spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)

require_relative 'lib/forest_admin_datasource_zendesk/version'

Gem::Specification.new do |spec|
spec.name = 'forest_admin_datasource_zendesk'
spec.version = ForestAdminDatasourceZendesk::VERSION
spec.authors = ['Forest Admin']
spec.email = ['contact@forestadmin.com']
spec.homepage = 'https://www.forestadmin.com'
spec.summary = 'Zendesk datasource for Forest Admin Ruby agent.'
spec.description = 'Surface Zendesk tickets, users, organizations and comments as Forest Admin collections.'
spec.license = 'GPL-3.0'
spec.required_ruby_version = '>= 3.0.0'

spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = 'https://github.com/ForestAdmin/agent-ruby'
spec.metadata['changelog_uri'] = 'https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md'
spec.metadata['rubygems_mfa_required'] = 'false'

spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(File.expand_path(f) == __FILE__) ||
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
end
end
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_dependency 'activesupport', '>= 6.1'
spec.add_dependency 'zeitwerk', '~> 2.3'
spec.add_dependency 'zendesk_api', '~> 3.0'
end
Comment on lines +32 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical forest_admin_datasource_zendesk/forest_admin_datasource_zendesk.gemspec:32

The gemspec is missing a runtime dependency on forest_admin_datasource_toolkit, which the library requires at runtime via require 'forest_admin_datasource_toolkit' and inherits from heavily. When the gem is installed by an end user, forest_admin_datasource_toolkit will not be pulled in, causing a LoadError on startup. Add spec.add_dependency 'forest_admin_datasource_toolkit' to the gemspec.

  spec.add_dependency 'activesupport', '>= 6.1'
  spec.add_dependency 'zeitwerk', '~> 2.3'
  spec.add_dependency 'zendesk_api', '~> 3.0'
+  spec.add_dependency 'forest_admin_datasource_toolkit'
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/forest_admin_datasource_zendesk/forest_admin_datasource_zendesk.gemspec around lines 32-35:

The gemspec is missing a runtime dependency on `forest_admin_datasource_toolkit`, which the library requires at runtime via `require 'forest_admin_datasource_toolkit'` and inherits from heavily. When the gem is installed by an end user, `forest_admin_datasource_toolkit` will not be pulled in, causing a `LoadError` on startup. Add `spec.add_dependency 'forest_admin_datasource_toolkit'` to the gemspec.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require_relative 'forest_admin_datasource_zendesk/version'
require 'logger'
require 'zeitwerk'
require 'forest_admin_datasource_toolkit'
require 'zendesk_api'

loader = Zeitwerk::Loader.for_gem
loader.setup

module ForestAdminDatasourceZendesk
class Error < StandardError; end
class ConfigurationError < Error; end
class UnsupportedOperatorError < Error; end

class APIError < Error; end

class << self
attr_writer :logger

def logger
@logger ||= default_logger
end

private

def default_logger
return Rails.logger if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger

Logger.new($stderr).tap { |l| l.progname = 'forest_admin_datasource_zendesk' }
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
module ForestAdminDatasourceZendesk
class Client
include Writes
include Introspection

MAX_PER_PAGE = 100

def initialize(configuration)
@configuration = configuration
end

def search(type, **opts)
params = build_search_params(type, opts)
must_succeed("search(#{type})") { api.search(params).to_a }
end

def count(type, query:)
must_succeed("count(#{type})") do
body = api.connection.get('search/count', query: compose_query(type, query)).body
Integer(body['count'] || 0)
end
end

def fetch_ticket_comments(ticket_id)
must_succeed("fetch_ticket_comments(#{ticket_id})") do
Array(api.connection.get("tickets/#{ticket_id}/comments").body['comments'])
end
end

def find_ticket(id) = find_one(api.tickets, id)
def find_user(id) = find_one(api.users, id)
def find_organization(id) = find_one(api.organizations, id)

def fetch_user_emails(ids)
best_effort('fetch_user_emails', default: {}) do
bulk_show_many('users', ids) { |u| [u['id'], u['email']] }
end
end

def fetch_tickets_by_ids(ids)
must_succeed('fetch_tickets_by_ids') do
bulk_show_many('tickets', ids) { |t| [t['id'], t] }
end
end

def fetch_users_by_ids(ids)
best_effort('fetch_users_by_ids', default: {}) do
bulk_show_many('users', ids) { |u| [u['id'], u] }
end
end

def fetch_organizations_by_ids(ids)
best_effort('fetch_organizations_by_ids', default: {}) do
bulk_show_many('organizations', ids) { |o| [o['id'], o] }
end
end

private

def find_one(api_collection, id)
api_collection.find(id: id)
rescue ZendeskAPI::Error::RecordNotFound
nil
end

def bulk_show_many(resource, ids)
ids = Array(ids).compact.uniq
return {} if ids.empty?

ids.each_slice(MAX_PER_PAGE).with_object({}) do |batch, acc|
body = api.connection.get("#{resource}/show_many", ids: batch.join(',')).body
Array(body[resource]).each do |item|
k, v = yield(item)
acc[k] = v
end
end
end

def must_succeed(operation)
yield
rescue StandardError => e
raise APIError, "Zendesk API call failed: #{operation}: #{e.class}: #{e.message}"
end

def best_effort(operation, default:)
yield
rescue StandardError => e
ForestAdminDatasourceZendesk.logger.warn(
"[forest_admin_datasource_zendesk] #{operation} failed; degrading: #{e.class}: #{e.message}"
)
default
end

def compose_query(type, query)
[type ? "type:#{type}" : nil, query.to_s.strip].compact.reject(&:empty?).join(' ')
end

def build_search_params(type, opts)
params = {
query: compose_query(type, opts[:query]),
per_page: [opts[:per_page] || MAX_PER_PAGE, MAX_PER_PAGE].min,
page: opts[:page] || 1
}
params[:sort_by] = opts[:sort_by] if opts[:sort_by]
params[:sort_order] = opts[:sort_order] if opts[:sort_order]
params
end

def api
@api ||= ZendeskAPI::Client.new do |c|
c.url = @configuration.url
c.username = @configuration.username
c.token = @configuration.token
c.retry = true
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module ForestAdminDatasourceZendesk
class Client
module Introspection
def fetch_ticket_fields
best_effort('fetch_ticket_fields (custom fields will be unavailable)', default: []) do
Array(api.connection.get('ticket_fields').body['ticket_fields'])
end
end

def fetch_user_fields
best_effort('fetch_user_fields (custom fields will be unavailable)', default: []) do
Array(api.connection.get('user_fields').body['user_fields'])
end
end

def fetch_organization_fields
best_effort('fetch_organization_fields (custom fields will be unavailable)', default: []) do
Array(api.connection.get('organization_fields').body['organization_fields'])
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module ForestAdminDatasourceZendesk
class Client
module Writes
def create_ticket(attributes) = post_resource('tickets', 'ticket', attributes)
def update_ticket(id, attrs) = put_resource('tickets', 'ticket', id, attrs)
def delete_ticket(id) = delete_resource('tickets', id)

def create_user(attributes) = post_resource('users', 'user', attributes)
def update_user(id, attrs) = put_resource('users', 'user', id, attrs)
def delete_user(id) = delete_resource('users', id)

def create_organization(attrs) = post_resource('organizations', 'organization', attrs)
def update_organization(id, attrs) = put_resource('organizations', 'organization', id, attrs)
def delete_organization(id) = delete_resource('organizations', id)

private

def post_resource(path, key, attributes)
must_succeed("create(#{path})") do
body = api.connection.post(path) { |req| req.body = { key => attributes } }.body
body[key] || body
end
end

def put_resource(path, key, id, attributes)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 4): put_resource [qlty:function-parameters]

must_succeed("update(#{path}/#{id})") do
body = api.connection.put("#{path}/#{id}") { |req| req.body = { key => attributes } }.body
body[key] || body
end
end

def delete_resource(path, id)
must_succeed("delete(#{path}/#{id})") do
api.connection.delete("#{path}/#{id}")
true
end
end
end
end
end
Loading
Loading