Skip to content

Commit

Permalink
Merge pull request #247 from discolabs/develop
Browse files Browse the repository at this point in the history
Release 0.16.1
  • Loading branch information
gavinballard authored Jan 1, 2019
2 parents e209427 + 13d35dd commit 4f30c12
Show file tree
Hide file tree
Showing 39 changed files with 996 additions and 132 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Change Log
All notable changes to this project will be documented in this file.

## 0.16.1 - 2019-01-01
### Added
- Support for Shopify Flow triggers and actions

## 0.16.0 - 2018-10-01
### Changed
- Update to Rails 5.2
Expand Down
164 changes: 164 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,170 @@ listen for the `app/uninstalled` and `shop/update` webhook topics.

To check which webhooks are registered by your app run `shop.with_api_context{ShopifyAPI::Webhook.find(:all)}` from your console, where `shop = DiscoApp::Shop.find(your_shop_id)`.

### Shopify Flow
The gem provides support for [Shopify Flow Connectors][], allowing applications
built with this framework to define and send triggers and receive and process
actions. Each trigger that's created or action that's received is stored in the
database as `DiscoApp::Flow::Trigger` and `DiscoApp::Flow::Action` models
respectively.

Triggers and actions are processed asynchronously as background jobs. The
success or failure of a trigger or action is stored in a `status` attribute in
the models. If a trigger or action fails for any reason, the reported reasons
for failure are stored in a `processing_errors` attribute.

Applications that are sending a lot of triggers, or receiving a lot of actions,
may want to clear out the trigger and action database tables periodically.

[Shopify Flow Connectors]: https://help.shopify.com/en/api/embedded-apps/app-extensions/flow

#### Triggers
Shopify Flow Triggers are events that happen inside a Shopify app that can be
used inside Shopify Flow to start workflows. There's no special configuration
that you need to undertake to start using Flow triggers with a Disco App -
assuming that you've [defined a trigger][] in your application's configuration
from the Shopify Partner dashboard, you can fire that trigger with the
following code:

```ruby
DiscoApp::Flow::CreateTrigger.call(
shop: @shop,
title: 'Customer became a VIP',
resource_name: 'Customer Jane Doe',
resource_url: 'https://store.myshopify.com/admin/customers/734299256292',
properties: {
'Customer email' => '[email protected]'
}
)
```

Upon execution, a new `DiscoApp::Flow::Trigger` model will be persisted and a
background job enqueued to send the trigger information to the relevant Shopify
store's GraphQL API endpoint.

The arguments passed to the `CreateTrigger` method are:

- `shop`: The relevant `DiscoApp::Shop` instance the trigger relates to;
- `title`: The title of the trigger. This must exactly match the title of the
trigger as defined from the Shopify Partner dashboard;
- `resource_name`: A short description of the object the trigger relates to.
This is used by the Shopify Flow app to display workflow event history to
store owners;
- `resource_url`: A URL that can be followed by a store owner to view more
information about the object the trigger relates to;
- `properties`: A payload hash containing data about the trigger event that can
be used by merchants within their workflows. The presence and data types of
the values in this hash must exactly match those configured for the relevant
trigger in the Shopify Partner dashboard.

[defined a trigger]: https://help.shopify.com/en/api/embedded-apps/app-extensions/flow/create-triggers

#### Actions
Shopify Flow Actions are the operations a Shopify application can perform as
part of a workflow. Like Triggers, [Actions must be defined][] within the
Shopify Partner Dashboard configuration page for the application. The Disco App
gem provides an `DiscoApp::Flow::ActionsController`, which serves a similar
function to the `DiscoApp::WebhooksController` - it receives and verifies
incoming requests from Shopify before handing them off for processing.

Unlike webhook processing, incoming actions are persisted to the database in
the form of a `DiscoApp::Flow::Action` model before being processed. When
attempting to process an action, Disco App will attempt to find, instantiate
and call a service object with the same name as the `action_id` of the
relevant action. The `action_id` is determined by the URL used by Shopify to
send the action payload.

To take an example, an action may be configured in the Shopify Dashboard with
the following attributes:

- Action title: `Email customer`;
- Action description: `Send an email to a customer`;
- HTTPS request URL: `https://example.discolabs.com/flow/action/email_customer`.

When Shopify sends a request for this action, the `action_id` of the persisted
action model will be `email_customer` (derived from the request URL). When
trying to process this action, Disco App will attempt to look for either an
`EmailCustomer` or `Flow::Actions::EmailCustomer` service object class within
the current application. If found, the `call` method will be called on that
object with the relevant `DiscoApp::Shop` instance and the provided action
properties hash being passed as keyword arguments - essentially, something like
this:

```ruby
Flow::Actions::EmailCustomer.call(shop: action.shop, properties: action.properties)
```

In this way Disco App expects applications using Shopify Flow actions to define
service objects to process those actions using a typical Disco interactor
pattern.

[Actions must be defined]: https://help.shopify.com/en/api/embedded-apps/app-extensions/flow/create-actions

#### Configuration
Strictly speaking, the only two things that need to be done inside application
code to support Shopify Flow Actions and Triggers are:

1. Call `DiscoApp::Flow::CreateTrigger` anywhere in your code where a trigger
should be fired;
2. Create a `Flow::Actions::ActionName` service object class for each action
you'd like your application to be able to process.

Assuming you've configured your application's Flow integration correctly from
the Shopify Partner dashboard, the sending of triggers and receiving of actions
should then "just work".

However, to help maintain an overview of the actions and triggers supported by
your application with its codebase, it's recommended to maintain two additional
initializers in your application's configuration that describe them. These
files should then be treated as the source of truth for your application's
actions and triggers, and should be referenced when setting up or updating your
application's Flow configuration from the Partner Dashboard.

Examples of each initializer follow.

```ruby
# config/initializers/disco_app_flow_actions.rb
DiscoApp.configure do |config|
config.flow_actions = {
email_customer: {
title: 'Email customer',
description: 'Send an email to a customer',
properties: [
{
name: 'customer_email',
label: 'Customer email',
help_text: 'The email address of the customer.',
type: :email,
required: true
}
]
}
}
end
```

```ruby
# config/initializers/disco_app_flow_triggers.rb
DiscoApp.configure do |config|
config.flow_triggers = {
customer_became_a_vip: {
title: 'Customer became a VIP',
description: 'A customer successfully qualified for VIP status.',
properties: [
{
name: 'Customer email',
description: 'The email address of the customer.',
type: :email
}
]
}
}
end
```

In future versions of Disco App, the creation of triggers and the processing of
actions may be validated against the schema defined in these initializers.

### Asset Rendering
It's a pretty common pattern for apps to want to render and update Shopify
assets (Javascript, stylesheets, Liquid snippets etc) whenever a store owner
Expand Down
8 changes: 8 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ This file contains more detailed instructions on what's required when updating
an application between one release version of the gem to the next. It's intended
as more in-depth accompaniment to the notes in `CHANGELOG.md` for each version.

## Upgrading from 0.16.0 to 0.16.1
Ensure new Shopify Flow database migrations are brought across and run:

```
bundle exec rake disco_app:install:migrations`
bundle exec rake db:migrate
```

## Upgrading from 0.15.2 to 0.16.0 (inclusive)
Upgrade your app to Rails version 5.2. See the [Rails upgrade docs](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-5-1-to-rails-5-2).

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.16.0
0.16.1
85 changes: 85 additions & 0 deletions app/clients/disco_app/graphql_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require 'rest-client'

##
# This file defines a very simple GraphQL API client to support a single type
# of GraphQL API call for a Shopify store - sending a Shopify Flow trigger.
#
# We use this simple approach rather than using an existing GraphQL client
# library such as https://github.com/github/graphql-client (either standalone
# or as bundled with the Shopify API gem) for a couple of reasons:
#
# - These libraries tend to presume that a single client instance is
# instantiated once and then reused across the application, which isn't the
# case when we're making API calls once per trigger for each background
# job.
# - These libraries make an API call to fetch the Shopify GraphQL schema on
# initialisation. The schema is very large, so the API call takes a number
# of seconds to complete and when parsed consumes a large amount of memory.
# - These libraries do not natively work well with the idea of a dynamic API
# endpoint (ie, changing the request URL frequently), which is required
# when making many requests to different Shopify stores.
#
module DiscoApp
class GraphqlClient

def initialize(shop)
@shop = shop
end

##
# Fire a Shopify Flow Trigger.
# Returns a tuple {Boolean, Array} representing {success, errors}.
def create_flow_trigger(title, resource_name, resource_url, properties)
body = {
trigger_title: title,
resources: [
{
name: resource_name,
url: resource_url
}
],
properties: properties
}

# The double .to_json.to_json below looks odd but is required to properly escape the JSON hash
# when inserting it into the GraphQL mutation call.
response = execute(%Q(
mutation {
flowTriggerReceive(body: #{body.to_json.to_json}) {
userErrors {
field,
message
}
}
}
))

errors = response.dig(:data, :flowTriggerReceive, :userErrors)
[errors.empty?, errors]
end

private

def execute(query)
response = RestClient::Request.execute(
method: :post,
headers: headers,
url: url,
payload: { query: query }.to_json
)
JSON.parse(response.body).with_indifferent_access
end

def headers
{
'Content-Type' => 'application/json',
'X-Shopify-Access-Token' => @shop.shopify_token
}
end

def url
"https://#{@shop.shopify_domain}/admin/api/graphql.json"
end

end
end
53 changes: 0 additions & 53 deletions app/clients/disco_app/rollbar_client.rb

This file was deleted.

2 changes: 0 additions & 2 deletions app/clients/disco_app/rollbar_client_error.rb

This file was deleted.

7 changes: 7 additions & 0 deletions app/controllers/disco_app/flow/actions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module DiscoApp
module Flow
class ActionsController < ActionController::Base
include DiscoApp::Flow::Concerns::ActionsController
end
end
end
47 changes: 47 additions & 0 deletions app/controllers/disco_app/flow/concerns/actions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module DiscoApp
module Flow
module Concerns
module ActionsController

extend ActiveSupport::Concern

included do
before_action :verify_flow_action
before_action :find_shop
protect_from_forgery with: :null_session
end

def create_flow_action
DiscoApp::Flow::CreateAction.call(
shop: @shop,
action_id: params[:id],
action_run_id: params[:action_run_id],
properties: params[:properties]
)

head :ok
end

private

def verify_flow_action
unless flow_action_is_valid?
head :unauthorized
end
request.body.rewind
end

# Shopify Flow action endpoints use the same verification method as webhooks, which is why we reuse this
# service method here.
def flow_action_is_valid?
DiscoApp::WebhookService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'])
end

def find_shop
@shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shopify_domain])
end

end
end
end
end
Loading

0 comments on commit 4f30c12

Please sign in to comment.