disco_app 0.15.2 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/clients/disco_app/graphql_client.rb +85 -0
  3. data/app/controllers/disco_app/flow/actions_controller.rb +7 -0
  4. data/app/controllers/disco_app/flow/concerns/actions_controller.rb +47 -0
  5. data/app/jobs/disco_app/flow/process_action_job.rb +11 -0
  6. data/app/jobs/disco_app/flow/process_trigger_job.rb +11 -0
  7. data/app/jobs/disco_app/shop_job.rb +8 -9
  8. data/app/models/disco_app/concerns/renders_assets.rb +1 -1
  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/20150525000000_create_shops_if_not_existent.rb +80 -80
  20. data/db/migrate/20170315062548_create_disco_app_sources.rb +2 -0
  21. data/db/migrate/20170315062629_add_sources_to_shop_subscriptions.rb +2 -0
  22. data/db/migrate/20170327214540_create_disco_app_users.rb +2 -1
  23. data/db/migrate/20170606160751_fix_disco_app_users_index.rb +2 -0
  24. data/db/migrate/20181229100327_create_flow_actions_and_triggers.rb +32 -0
  25. data/lib/disco_app/configuration.rb +4 -0
  26. data/lib/disco_app/version.rb +1 -1
  27. data/lib/generators/disco_app/disco_app_generator.rb +24 -8
  28. data/lib/generators/disco_app/templates/config/appsignal.yml +12 -0
  29. data/lib/generators/disco_app/templates/config/cable.yml.tt +11 -0
  30. data/lib/generators/disco_app/templates/config/database.yml.tt +6 -3
  31. data/lib/generators/disco_app/templates/config/environments/staging.rb +108 -0
  32. data/lib/generators/disco_app/templates/config/newrelic.yml +3 -0
  33. data/lib/generators/disco_app/templates/controllers/home_controller.rb +1 -0
  34. data/lib/generators/disco_app/templates/initializers/session_store.rb +1 -1
  35. data/lib/generators/disco_app/templates/root/.env +3 -0
  36. data/lib/generators/disco_app/templates/root/.rubocop.yml +223 -158
  37. data/test/dummy/config/database.yml +3 -0
  38. data/test/dummy/config/environments/staging.rb +85 -0
  39. data/test/dummy/config/secrets.yml +3 -0
  40. data/test/dummy/db/schema.rb +39 -11
  41. data/test/services/disco_app/flow/create_action_test.rb +51 -0
  42. data/test/services/disco_app/flow/create_trigger_test.rb +56 -0
  43. data/test/services/disco_app/flow/process_action_test.rb +68 -0
  44. data/test/services/disco_app/flow/process_trigger_test.rb +61 -0
  45. data/test/vcr/flow_trigger_invalid_title.yml +35 -0
  46. data/test/vcr/flow_trigger_valid.yml +38 -0
  47. metadata +66 -11
  48. data/app/clients/disco_app/rollbar_client.rb +0 -53
  49. data/app/clients/disco_app/rollbar_client_error.rb +0 -2
  50. data/lib/generators/disco_app/templates/initializers/rollbar.rb +0 -23
  51. data/lib/tasks/rollbar.rake +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffe2c53f6a63376cf9484d15fbfb9030d7e4b5c07e0c38cccbc3f2ef095ac00f
4
- data.tar.gz: 91019e6d795d0bd4b930badc400d8add493da10965312747db9b67ed58a8addc
3
+ metadata.gz: '058c0c520f0593e746cf7273793c2cefdd47af9e816224f8d3402c485e20a20e'
4
+ data.tar.gz: 9683bca6877f2b8c806a01024abab14fbedaed90d6c8a0505d6219011d9b4120
5
5
  SHA512:
6
- metadata.gz: dba9193914aa136db5dd59dd9c25b49e70396967f0aa2f0cbe05682408864c93d6e57f6afb771ad5c19b3e00074efb4b1d9f6e76f3805aed7473a390993a2183
7
- data.tar.gz: 3b171fa44ccab7a8af5470c50106ff1023370a29c95da68e9f2909287d551d5858ac840063daf9c091ed2184157eb25184278b4ac05916ffe887b1fe6df1b09d
6
+ metadata.gz: 82a23148e598236b391871d345deb085792afc6d3e422743410aca316f2d67a67e17a399649ee35d5ce5341ec3fa6d0f644e892592c6cdb29f4c49116bdf6e60
7
+ data.tar.gz: 670725a58544ac120136ab6cc67cf4c8b173a8d96e411041578f63c021b4480dbaf4e5979b901303634b5146aeea947a8dee22bb8b9b7c99682e2fb3cb98dace
@@ -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
@@ -67,7 +67,7 @@ module DiscoApp::Concerns::RendersAssets
67
67
  assets: nil,
68
68
  triggered_by: nil,
69
69
  script_tags: nil,
70
- minify: Rails.env.production?
70
+ minify: Rails.env.production? || Rails.env.staging?
71
71
  }
72
72
  end
73
73
 
@@ -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?
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?,