disco_app 0.16.0 → 0.16.1.pre.sidekiq.pre.6.pre.release

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/.DS_Store +0 -0
  3. data/app/clients/disco_app/graphql_client.rb +85 -0
  4. data/app/controllers/disco_app/flow/actions_controller.rb +7 -0
  5. data/app/controllers/disco_app/flow/concerns/actions_controller.rb +47 -0
  6. data/app/jobs/disco_app/flow/process_action_job.rb +11 -0
  7. data/app/jobs/disco_app/flow/process_trigger_job.rb +11 -0
  8. data/app/jobs/disco_app/shop_job.rb +8 -9
  9. data/app/models/disco_app/concerns/shop.rb +4 -5
  10. data/app/models/disco_app/flow/action.rb +7 -0
  11. data/app/models/disco_app/flow/concerns/action.rb +24 -0
  12. data/app/models/disco_app/flow/concerns/trigger.rb +24 -0
  13. data/app/models/disco_app/flow/trigger.rb +7 -0
  14. data/app/services/disco_app/flow/create_action.rb +35 -0
  15. data/app/services/disco_app/flow/create_trigger.rb +34 -0
  16. data/app/services/disco_app/flow/process_action.rb +50 -0
  17. data/app/services/disco_app/flow/process_trigger.rb +52 -0
  18. data/config/routes.rb +7 -1
  19. data/db/migrate/20181229100327_create_flow_actions_and_triggers.rb +32 -0
  20. data/lib/.DS_Store +0 -0
  21. data/lib/disco_app/configuration.rb +4 -0
  22. data/lib/disco_app/version.rb +1 -1
  23. data/lib/generators/disco_app/disco_app_generator.rb +2 -2
  24. data/lib/generators/disco_app/templates/config/appsignal.yml +12 -0
  25. data/lib/generators/disco_app/templates/root/.env +3 -0
  26. data/test/dummy/db/schema.rb +39 -11
  27. data/test/dummy/log/test.log +0 -0
  28. data/test/services/disco_app/flow/create_action_test.rb +51 -0
  29. data/test/services/disco_app/flow/create_trigger_test.rb +56 -0
  30. data/test/services/disco_app/flow/process_action_test.rb +68 -0
  31. data/test/services/disco_app/flow/process_trigger_test.rb +61 -0
  32. data/test/vcr/flow_trigger_invalid_title.yml +35 -0
  33. data/test/vcr/flow_trigger_valid.yml +38 -0
  34. metadata +188 -133
  35. data/app/clients/disco_app/rollbar_client.rb +0 -53
  36. data/app/clients/disco_app/rollbar_client_error.rb +0 -2
  37. data/lib/generators/disco_app/templates/initializers/rollbar.rb +0 -21
  38. data/lib/tasks/rollbar.rake +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ece31304441ed10f09b9391cd7dd60e4f2ee042b8f1fb782d4a3a57508e0018
4
- data.tar.gz: 4795ce5f7be894f915acf1b22342902495de3cff322c4c881634ff26458d6457
3
+ metadata.gz: dc31c69faa480b4a03ebeda1fede57d6c5dbc6f4447dd292de82dfec19f1f178
4
+ data.tar.gz: cdb8079cf7052deb770d7654823d34fd02858ac0ad2c6c0cbb7b0f78853cc0f2
5
5
  SHA512:
6
- metadata.gz: 67ddc280601b0dd494535991a9e217d7eeb94732e0113dffbf973a88096c81f9c31771aa4d2b260a5e586ec3ed90b8d088fe60f697034f07682c0d6f451691a9
7
- data.tar.gz: 69f2f10799bdc2b57f904b81734af3252b1497aa64963e87b2f2201fe68e8cc9028b6127b9d99d613a9bb8b0a80f1d2e848eacd572faba9ffbd9c9d66cf517b8
6
+ metadata.gz: 1f59584cd75e4fca892210ee1c140d86fe2dc34fb840a4520e3b91e2efc78a6aae10825acb19bd809df6ae7376e073219cbb91b631a6f18d282fb3aef364b06b
7
+ data.tar.gz: e912f48e0bb236cd6753692db5c4c61e05973c0e09939e88ca6b86f88631800bafa303672632b962de12abd296a3a9a5b7c31995a676dac710f4aa1838036d1b
data/app/.DS_Store ADDED
Binary file
@@ -0,0 +1,85 @@
1
+ require 'rest-client'
2
+
3
+ ##
4
+ # This file defines a very simple GraphQL API client to support a single type
5
+ # of GraphQL API call for a Shopify store - sending a Shopify Flow trigger.
6
+ #
7
+ # We use this simple approach rather than using an existing GraphQL client
8
+ # library such as https://github.com/github/graphql-client (either standalone
9
+ # or as bundled with the Shopify API gem) for a couple of reasons:
10
+ #
11
+ # - These libraries tend to presume that a single client instance is
12
+ # instantiated once and then reused across the application, which isn't the
13
+ # case when we're making API calls once per trigger for each background
14
+ # job.
15
+ # - These libraries make an API call to fetch the Shopify GraphQL schema on
16
+ # initialisation. The schema is very large, so the API call takes a number
17
+ # of seconds to complete and when parsed consumes a large amount of memory.
18
+ # - These libraries do not natively work well with the idea of a dynamic API
19
+ # endpoint (ie, changing the request URL frequently), which is required
20
+ # when making many requests to different Shopify stores.
21
+ #
22
+ module DiscoApp
23
+ class GraphqlClient
24
+
25
+ def initialize(shop)
26
+ @shop = shop
27
+ end
28
+
29
+ ##
30
+ # Fire a Shopify Flow Trigger.
31
+ # Returns a tuple {Boolean, Array} representing {success, errors}.
32
+ def create_flow_trigger(title, resource_name, resource_url, properties)
33
+ body = {
34
+ trigger_title: title,
35
+ resources: [
36
+ {
37
+ name: resource_name,
38
+ url: resource_url
39
+ }
40
+ ],
41
+ properties: properties
42
+ }
43
+
44
+ # The double .to_json.to_json below looks odd but is required to properly escape the JSON hash
45
+ # when inserting it into the GraphQL mutation call.
46
+ response = execute(%Q(
47
+ mutation {
48
+ flowTriggerReceive(body: #{body.to_json.to_json}) {
49
+ userErrors {
50
+ field,
51
+ message
52
+ }
53
+ }
54
+ }
55
+ ))
56
+
57
+ errors = response.dig(:data, :flowTriggerReceive, :userErrors)
58
+ [errors.empty?, errors]
59
+ end
60
+
61
+ private
62
+
63
+ def execute(query)
64
+ response = RestClient::Request.execute(
65
+ method: :post,
66
+ headers: headers,
67
+ url: url,
68
+ payload: { query: query }.to_json
69
+ )
70
+ JSON.parse(response.body).with_indifferent_access
71
+ end
72
+
73
+ def headers
74
+ {
75
+ 'Content-Type' => 'application/json',
76
+ 'X-Shopify-Access-Token' => @shop.shopify_token
77
+ }
78
+ end
79
+
80
+ def url
81
+ "https://#{@shop.shopify_domain}/admin/api/graphql.json"
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,7 @@
1
+ module DiscoApp
2
+ module Flow
3
+ class ActionsController < ActionController::Base
4
+ include DiscoApp::Flow::Concerns::ActionsController
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ module DiscoApp
2
+ module Flow
3
+ module Concerns
4
+ module ActionsController
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_action :verify_flow_action
10
+ before_action :find_shop
11
+ protect_from_forgery with: :null_session
12
+ end
13
+
14
+ def create_flow_action
15
+ DiscoApp::Flow::CreateAction.call(
16
+ shop: @shop,
17
+ action_id: params[:id],
18
+ action_run_id: params[:action_run_id],
19
+ properties: params[:properties]
20
+ )
21
+
22
+ head :ok
23
+ end
24
+
25
+ private
26
+
27
+ def verify_flow_action
28
+ unless flow_action_is_valid?
29
+ head :unauthorized
30
+ end
31
+ request.body.rewind
32
+ end
33
+
34
+ # Shopify Flow action endpoints use the same verification method as webhooks, which is why we reuse this
35
+ # service method here.
36
+ def flow_action_is_valid?
37
+ DiscoApp::WebhookService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'])
38
+ end
39
+
40
+ def find_shop
41
+ @shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shopify_domain])
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,11 @@
1
+ module DiscoApp
2
+ module Flow
3
+ class ProcessActionJob < DiscoApp::ShopJob
4
+
5
+ def perform(_shop, action)
6
+ ProcessAction.call(action: action)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module DiscoApp
2
+ module Flow
3
+ class ProcessTriggerJob < DiscoApp::ShopJob
4
+
5
+ def perform(_shop, trigger)
6
+ ProcessTrigger.call(trigger: trigger)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -1,10 +1,10 @@
1
+ require 'appsignal'
2
+
1
3
  # The base class for all jobs that should be performed in the context of a
2
4
  # particular Shop's API session. The first argument to any job inheriting from
3
5
  # this class must be the domain of the relevant store, so that the appropriate
4
6
  # Shop model can be fetched and the temporary API session created.
5
7
 
6
- require 'rollbar'
7
-
8
8
  class DiscoApp::ShopJob < ApplicationJob
9
9
 
10
10
  queue_as :default
@@ -22,13 +22,12 @@ class DiscoApp::ShopJob < ApplicationJob
22
22
  end
23
23
 
24
24
  def shop_context(job, block)
25
- Rollbar.scoped(rollbar_scope) do
26
- @shop.with_api_context { block.call(job.arguments) }
27
- end
28
- end
29
-
30
- def rollbar_scope
31
- { person: { id: @shop.id, username: @shop.shopify_domain } }
25
+ Appsignal.tag_request(
26
+ shop_id: @shop.id,
27
+ shopify_domain: @shop.shopify_domain
28
+ )
29
+
30
+ @shop.with_api_context { block.call(job.arguments) }
32
31
  end
33
32
 
34
33
  end
@@ -15,6 +15,10 @@ module DiscoApp::Concerns::Shop
15
15
  # Define relationship to sessions.
16
16
  has_many :sessions, class_name: 'DiscoApp::Session', dependent: :destroy
17
17
 
18
+ # Define relationship to Flow actions and triggers.
19
+ has_many :flow_actions, class_name: 'DiscoApp::Flow::Action', dependent: :destroy
20
+ has_many :flow_triggers, class_name: 'DiscoApp::Flow::Trigger', dependent: :destroy
21
+
18
22
  # Define possible installation statuses as an enum.
19
23
  enum status: {
20
24
  never_installed: 0,
@@ -73,11 +77,6 @@ module DiscoApp::Concerns::Shop
73
77
  "https://#{shopify_domain}/admin"
74
78
  end
75
79
 
76
- # Convenience method to get the email of the shop's admin, to display in Rollbar.
77
- def email
78
- data[:email]
79
- end
80
-
81
80
  def installed_duration
82
81
  distance_of_time_in_words_to_now(created_at.time)
83
82
  end
@@ -0,0 +1,7 @@
1
+ module DiscoApp
2
+ module Flow
3
+ class Action < ApplicationRecord
4
+ include DiscoApp::Flow::Concerns::Action
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module DiscoApp
2
+ module Flow
3
+ module Concerns
4
+ module Action
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+
9
+ belongs_to :shop
10
+
11
+ self.table_name = :disco_app_flow_actions
12
+
13
+ enum status: {
14
+ pending: 0,
15
+ succeeded: 1,
16
+ failed: 2
17
+ }
18
+
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module DiscoApp
2
+ module Flow
3
+ module Concerns
4
+ module Trigger
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+
9
+ belongs_to :shop
10
+
11
+ self.table_name = :disco_app_flow_triggers
12
+
13
+ enum status: {
14
+ pending: 0,
15
+ succeeded: 1,
16
+ failed: 2
17
+ }
18
+
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module DiscoApp
2
+ module Flow
3
+ class Trigger < ApplicationRecord
4
+ include DiscoApp::Flow::Concerns::Trigger
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ require 'interactor'
2
+
3
+ module DiscoApp
4
+ module Flow
5
+ class CreateAction
6
+
7
+ include Interactor
8
+
9
+ delegate :shop, :action_id, :action_run_id, :properties, to: :context
10
+ delegate :action, to: :context
11
+
12
+ def call
13
+ create_action
14
+ enqueue_process_action_job
15
+ end
16
+
17
+ private
18
+
19
+ def create_action
20
+ context.action = shop.flow_actions.create!(
21
+ action_id: action_id,
22
+ action_run_id: action_run_id,
23
+ properties: properties
24
+ )
25
+ rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
26
+ context.fail!
27
+ end
28
+
29
+ def enqueue_process_action_job
30
+ ProcessActionJob.perform_later(shop, action)
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ require 'interactor'
2
+
3
+ module DiscoApp
4
+ module Flow
5
+ class CreateTrigger
6
+
7
+ include Interactor
8
+
9
+ delegate :shop, :title, :resource_name, :resource_url, :properties, to: :context
10
+ delegate :trigger, to: :context
11
+
12
+ def call
13
+ create_trigger
14
+ enqueue_process_trigger_job
15
+ end
16
+
17
+ private
18
+
19
+ def create_trigger
20
+ context.trigger = shop.flow_triggers.create!(
21
+ title: title,
22
+ resource_name: resource_name,
23
+ resource_url: resource_url,
24
+ properties: properties
25
+ )
26
+ end
27
+
28
+ def enqueue_process_trigger_job
29
+ ProcessTriggerJob.perform_later(shop, trigger)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ require 'interactor'
2
+
3
+ module DiscoApp
4
+ module Flow
5
+ class ProcessAction
6
+
7
+ include Interactor
8
+
9
+ delegate :action, to: :context
10
+ delegate :action_service_class, to: :context
11
+
12
+ def call
13
+ validate_action
14
+ find_action_service_class
15
+ execute_action_service_class
16
+ end
17
+
18
+ private
19
+
20
+ def validate_action
21
+ context.fail! unless action.pending?
22
+ end
23
+
24
+ def find_action_service_class
25
+ context.action_service_class =
26
+ action.action_id.classify.safe_constantize ||
27
+ %Q(Flow::Actions::#{("#{action.action_id}".classify)}).safe_constantize
28
+
29
+ if action_service_class.nil?
30
+ update_action(false, ["Could not find service class for #{action.action_id}"])
31
+ context.fail!
32
+ end
33
+ end
34
+
35
+ def execute_action_service_class
36
+ result = action_service_class.call(shop: action.shop, properties: action.properties)
37
+ update_action(result.success?, result.errors)
38
+ end
39
+
40
+ def update_action(success, errors)
41
+ action.update!(
42
+ status: success ? Action.statuses[:succeeded] : Action.statuses[:failed],
43
+ processing_errors: success ? [] : errors,
44
+ processed_at: Time.current
45
+ )
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,52 @@
1
+ require 'interactor'
2
+
3
+ module DiscoApp
4
+ module Flow
5
+ class ProcessTrigger
6
+
7
+ include Interactor
8
+
9
+ delegate :trigger, to: :context
10
+ delegate :api_success, :api_errors, to: :context
11
+
12
+ def call
13
+ validate_trigger
14
+ make_api_request
15
+ update_trigger
16
+ fail_if_errors_present
17
+ end
18
+
19
+ private
20
+
21
+ def validate_trigger
22
+ context.fail! unless trigger.pending?
23
+ end
24
+
25
+ def make_api_request
26
+ context.api_success, context.api_errors = api_client.create_flow_trigger(
27
+ trigger.title,
28
+ trigger.resource_name,
29
+ trigger.resource_url,
30
+ trigger.properties
31
+ )
32
+ end
33
+
34
+ def update_trigger
35
+ trigger.update!(
36
+ status: api_success ? Trigger.statuses[:succeeded] : Trigger.statuses[:failed],
37
+ processing_errors: api_success ? [] : api_errors,
38
+ processed_at: Time.current
39
+ )
40
+ end
41
+
42
+ def fail_if_errors_present
43
+ context.fail! unless api_success
44
+ end
45
+
46
+ def api_client
47
+ @api_client ||= DiscoApp::GraphqlClient.new(trigger.shop)
48
+ end
49
+
50
+ end
51
+ end
52
+ end
data/config/routes.rb CHANGED
@@ -9,6 +9,12 @@ DiscoApp::Engine.routes.draw do
9
9
  post 'webhooks' => :process_webhook, as: :webhooks
10
10
  end
11
11
 
12
+ namespace :flow do
13
+ controller :actions do
14
+ post 'actions/:id' => :create_flow_action, as: :flow_actions
15
+ end
16
+ end
17
+
12
18
  resources :user_sessions, only: [:new, :create, :destroy]
13
19
  get 'auth/shopify_user/callback' => 'user_sessions#callback'
14
20
 
@@ -46,7 +52,7 @@ DiscoApp::Engine.routes.draw do
46
52
  end
47
53
 
48
54
  # Make the Sidekiq Web UI accessible using the same credentials as the admin.
49
- if Rails.env.production? || Rails.env.staging?
55
+ if Rails.env.production? || Rails.env.staging?
50
56
  Sidekiq::Web.use Rack::Auth::Basic do |username, password|
51
57
  [
52
58
  ENV['ADMIN_APP_USERNAME'].present?,
@@ -0,0 +1,32 @@
1
+ class CreateFlowActionsAndTriggers < ActiveRecord::Migration[5.2]
2
+
3
+ def change
4
+ create_table :disco_app_flow_actions do |t|
5
+ t.integer :shop_id, limit: 8
6
+ t.string :action_id
7
+ t.string :action_run_id
8
+ t.jsonb :properties, default: {}
9
+ t.integer :status, default: 0
10
+ t.datetime :processed_at, null: true
11
+ t.jsonb :processing_errors, default: []
12
+ t.timestamps null: false
13
+ end
14
+
15
+ create_table :disco_app_flow_triggers do |t|
16
+ t.integer :shop_id, limit: 8
17
+ t.string :title
18
+ t.string :resource_name
19
+ t.string :resource_url
20
+ t.jsonb :properties, default: {}
21
+ t.integer :status, default: 0
22
+ t.datetime :processed_at, null: true
23
+ t.jsonb :processing_errors, default: []
24
+ t.timestamps null: false
25
+ end
26
+
27
+ add_foreign_key :disco_app_flow_actions, :disco_app_shops, column: :shop_id, on_delete: :cascade
28
+ add_foreign_key :disco_app_flow_triggers, :disco_app_shops, column: :shop_id, on_delete: :cascade
29
+ add_index :disco_app_flow_actions, :action_run_id, unique: true
30
+ end
31
+
32
+ end
data/lib/.DS_Store ADDED
Binary file
@@ -8,6 +8,10 @@ module DiscoApp
8
8
  # Set the list of Shopify webhook topics to register.
9
9
  attr_accessor :webhook_topics
10
10
 
11
+ # Set Flow configuration
12
+ attr_accessor :flow_actions
13
+ attr_accessor :flow_triggers
14
+
11
15
  # Set the below if using an application proxy.
12
16
  attr_accessor :app_proxy_prefix
13
17
 
@@ -1,3 +1,3 @@
1
1
  module DiscoApp
2
- VERSION = '0.16.0'
2
+ VERSION = '0.16.1-sidekiq-6-release'
3
3
  end
@@ -41,7 +41,7 @@ class DiscoAppGenerator < Rails::Generators::Base
41
41
  gem 'premailer-rails'
42
42
  gem 'react-rails'
43
43
  gem 'render_anywhere'
44
- gem 'rollbar'
44
+ gem 'appsignal'
45
45
  gem 'shopify_app'
46
46
  gem 'sidekiq'
47
47
 
@@ -156,7 +156,7 @@ class DiscoAppGenerator < Rails::Generators::Base
156
156
  application configuration, env: :staging
157
157
 
158
158
  # Monitoring configuration
159
- copy_file 'initializers/rollbar.rb', 'config/initializers/rollbar.rb'
159
+ copy_file 'config/appsignal.yml', 'config/appsignal.yml'
160
160
  copy_file 'config/newrelic.yml', 'config/newrelic.yml'
161
161
  end
162
162
 
@@ -0,0 +1,12 @@
1
+ default: &defaults
2
+ name: <%= ENV['SHOPIFY_APP_NAME'] || 'Unknown App' %>
3
+ push_api_key: <%= ENV['APPSIGNAL_PUSH_API_KEY'] %>
4
+ development:
5
+ <<: *defaults
6
+ active: false
7
+ staging:
8
+ <<: *defaults
9
+ active: true
10
+ production:
11
+ <<: *defaults
12
+ active: true
@@ -18,3 +18,6 @@ REDIS_PROVIDER=
18
18
  DISCO_API_URL=
19
19
 
20
20
  WHITELISTED_DOMAINS=
21
+
22
+ # You can find this listed in 1Password
23
+ APPSIGNAL_PUSH_API_KEY=