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.
- checksums.yaml +4 -4
- data/app/.DS_Store +0 -0
- data/app/clients/disco_app/graphql_client.rb +85 -0
- data/app/controllers/disco_app/flow/actions_controller.rb +7 -0
- data/app/controllers/disco_app/flow/concerns/actions_controller.rb +47 -0
- data/app/jobs/disco_app/flow/process_action_job.rb +11 -0
- data/app/jobs/disco_app/flow/process_trigger_job.rb +11 -0
- data/app/jobs/disco_app/shop_job.rb +8 -9
- data/app/models/disco_app/concerns/shop.rb +4 -5
- data/app/models/disco_app/flow/action.rb +7 -0
- data/app/models/disco_app/flow/concerns/action.rb +24 -0
- data/app/models/disco_app/flow/concerns/trigger.rb +24 -0
- data/app/models/disco_app/flow/trigger.rb +7 -0
- data/app/services/disco_app/flow/create_action.rb +35 -0
- data/app/services/disco_app/flow/create_trigger.rb +34 -0
- data/app/services/disco_app/flow/process_action.rb +50 -0
- data/app/services/disco_app/flow/process_trigger.rb +52 -0
- data/config/routes.rb +7 -1
- data/db/migrate/20181229100327_create_flow_actions_and_triggers.rb +32 -0
- data/lib/.DS_Store +0 -0
- data/lib/disco_app/configuration.rb +4 -0
- data/lib/disco_app/version.rb +1 -1
- data/lib/generators/disco_app/disco_app_generator.rb +2 -2
- data/lib/generators/disco_app/templates/config/appsignal.yml +12 -0
- data/lib/generators/disco_app/templates/root/.env +3 -0
- data/test/dummy/db/schema.rb +39 -11
- data/test/dummy/log/test.log +0 -0
- data/test/services/disco_app/flow/create_action_test.rb +51 -0
- data/test/services/disco_app/flow/create_trigger_test.rb +56 -0
- data/test/services/disco_app/flow/process_action_test.rb +68 -0
- data/test/services/disco_app/flow/process_trigger_test.rb +61 -0
- data/test/vcr/flow_trigger_invalid_title.yml +35 -0
- data/test/vcr/flow_trigger_valid.yml +38 -0
- metadata +188 -133
- data/app/clients/disco_app/rollbar_client.rb +0 -53
- data/app/clients/disco_app/rollbar_client_error.rb +0 -2
- data/lib/generators/disco_app/templates/initializers/rollbar.rb +0 -21
- data/lib/tasks/rollbar.rake +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc31c69faa480b4a03ebeda1fede57d6c5dbc6f4447dd292de82dfec19f1f178
|
4
|
+
data.tar.gz: cdb8079cf7052deb770d7654823d34fd02858ac0ad2c6c0cbb7b0f78853cc0f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,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
|
@@ -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
|
-
|
26
|
-
@shop.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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,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,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
|
|
data/lib/disco_app/version.rb
CHANGED
@@ -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 '
|
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 '
|
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
|