-
Notifications
You must be signed in to change notification settings - Fork 1
feat(zendesk): add Zendesk datasource package #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
39627de
20e6af0
cabf166
a6b56f0
12412f7
116f909
4ac39d5
22d782e
ed28d88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| *.gem | ||
| .bundle/ | ||
| Gemfile.lock | ||
| coverage/ | ||
| pkg/ | ||
| tmp/ | ||
| .rspec_status |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| --format documentation | ||
| --color | ||
| --require spec_helper |
| 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 |
| 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 |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Critical The gemspec is missing a runtime dependency on 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: |
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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 | ||
There was a problem hiding this comment.
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?