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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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=