shopify_api 6.0.0 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -19
- data/CHANGELOG +11 -0
- data/Gemfile +1 -2
- data/Gemfile_ar41 +5 -0
- data/Gemfile_ar50 +5 -0
- data/Gemfile_ar51 +5 -0
- data/Gemfile_ar_master +0 -1
- data/README.md +116 -9
- data/RELEASING +2 -5
- data/lib/shopify_api.rb +4 -4
- data/lib/shopify_api/api_version.rb +116 -0
- data/lib/shopify_api/disable_prefix_check.rb +31 -0
- data/lib/shopify_api/limits.rb +5 -5
- data/lib/shopify_api/resources/access_scope.rb +6 -1
- data/lib/shopify_api/resources/asset.rb +15 -11
- data/lib/shopify_api/resources/base.rb +63 -1
- data/lib/shopify_api/resources/fulfillment_event.rb +1 -1
- data/lib/shopify_api/resources/graphql.rb +1 -1
- data/lib/shopify_api/resources/inventory_level.rb +2 -2
- data/lib/shopify_api/resources/location.rb +1 -1
- data/lib/shopify_api/resources/marketing_event.rb +2 -0
- data/lib/shopify_api/resources/payment.rb +1 -1
- data/lib/shopify_api/resources/refund.rb +4 -3
- data/lib/shopify_api/resources/shipping_rate.rb +1 -1
- data/lib/shopify_api/resources/shop.rb +4 -2
- data/lib/shopify_api/resources/smart_collection.rb +1 -1
- data/lib/shopify_api/session.rb +45 -16
- data/lib/shopify_api/version.rb +1 -1
- data/shopify_api.gemspec +3 -2
- data/test/access_scope_test.rb +23 -0
- data/test/api_version_test.rb +144 -0
- data/test/base_test.rb +75 -32
- data/test/detailed_log_subscriber_test.rb +51 -12
- data/test/fixtures/access_scopes.json +10 -0
- data/test/limits_test.rb +2 -2
- data/test/marketing_event_test.rb +1 -1
- data/test/recurring_application_charge_test.rb +3 -9
- data/test/session_test.rb +158 -32
- data/test/test_helper.rb +27 -11
- metadata +33 -21
- data/Gemfile_ar30 +0 -6
- data/Gemfile_ar31 +0 -6
- data/Gemfile_ar32 +0 -6
- data/Gemfile_ar40 +0 -6
- data/lib/active_resource/base_ext.rb +0 -21
- data/lib/active_resource/disable_prefix_check.rb +0 -36
- data/lib/active_resource/to_query.rb +0 -10
- data/lib/shopify_api/json_format.rb +0 -18
- data/lib/shopify_api/resources/o_auth.rb +0 -17
- data/lib/shopify_api/resources/ping/conversation.rb +0 -42
- data/lib/shopify_api/resources/ping/delivery_confirmation_details.rb +0 -10
- data/lib/shopify_api/resources/ping/message.rb +0 -8
- data/test/fixtures/o_auth_revoke.json +0 -5
- data/test/o_auth_test.rb +0 -8
- data/test/ping/conversation_test.rb +0 -71
- data/test/ping/message_test.rb +0 -23
data/lib/shopify_api/limits.rb
CHANGED
@@ -7,14 +7,14 @@ module ShopifyAPI
|
|
7
7
|
end
|
8
8
|
|
9
9
|
module ClassMethods
|
10
|
-
|
11
|
-
#
|
12
|
-
# Eg:
|
10
|
+
# Takes form <call count>/<bucket size>
|
11
|
+
# See https://help.shopify.com/en/api/getting-started/api-call-limit
|
12
|
+
# Eg: 2/40
|
13
13
|
CREDIT_LIMIT_HEADER_PARAM = {
|
14
|
-
:
|
14
|
+
shop: 'X-Shopify-Shop-Api-Call-Limit',
|
15
15
|
}
|
16
16
|
|
17
|
-
|
17
|
+
##
|
18
18
|
# How many more API calls can I make?
|
19
19
|
# @return {Integer}
|
20
20
|
#
|
@@ -10,24 +10,24 @@ module ShopifyAPI
|
|
10
10
|
#
|
11
11
|
# Initialize with a key:
|
12
12
|
# asset = ShopifyAPI::Asset.new(:key => 'assets/special.css', :theme_id => 12345)
|
13
|
-
#
|
13
|
+
#
|
14
14
|
# Find by key:
|
15
15
|
# asset = ShopifyAPI::Asset.find('assets/image.png', :params => {:theme_id => 12345})
|
16
|
-
#
|
16
|
+
#
|
17
17
|
# Get the text or binary value:
|
18
18
|
# asset.value # decodes from attachment attribute if necessary
|
19
|
-
#
|
19
|
+
#
|
20
20
|
# You can provide new data for assets in a few different ways:
|
21
|
-
#
|
21
|
+
#
|
22
22
|
# * assign text data for the value directly:
|
23
23
|
# asset.value = "div.special {color:red;}"
|
24
|
-
#
|
24
|
+
#
|
25
25
|
# * provide binary data for the value:
|
26
26
|
# asset.attach(File.read('image.png'))
|
27
|
-
#
|
27
|
+
#
|
28
28
|
# * set a URL from which Shopify will fetch the value:
|
29
29
|
# asset.src = "http://mysite.com/image.png"
|
30
|
-
#
|
30
|
+
#
|
31
31
|
# * set a source key of another of your assets from which
|
32
32
|
# the value will be copied:
|
33
33
|
# asset.source_key = "assets/another_image.png"
|
@@ -44,15 +44,19 @@ module ShopifyAPI
|
|
44
44
|
end
|
45
45
|
|
46
46
|
# find an asset by key:
|
47
|
-
# ShopifyAPI::Asset.find('layout/theme.liquid', :params => {:
|
47
|
+
# ShopifyAPI::Asset.find('layout/theme.liquid', :params => { theme_id: 99 })
|
48
48
|
def self.find(*args)
|
49
49
|
if args[0].is_a?(Symbol)
|
50
50
|
super
|
51
51
|
else
|
52
|
-
params = {:
|
52
|
+
params = { asset: { key: args[0] } }
|
53
53
|
params = params.merge(args[1][:params]) if args[1] && args[1][:params]
|
54
|
-
path_prefix = params[:theme_id] ? "
|
55
|
-
resource = find(
|
54
|
+
path_prefix = params[:theme_id] ? "themes/#{params[:theme_id]}/" : ""
|
55
|
+
resource = find(
|
56
|
+
:one,
|
57
|
+
from: api_version.construct_api_path("#{path_prefix}assets.#{format.extension}"),
|
58
|
+
params: params
|
59
|
+
)
|
56
60
|
resource.prefix_options[:theme_id] = params[:theme_id] if resource && params[:theme_id]
|
57
61
|
resource
|
58
62
|
end
|
@@ -4,6 +4,7 @@ module ShopifyAPI
|
|
4
4
|
class Base < ActiveResource::Base
|
5
5
|
class InvalidSessionError < StandardError; end
|
6
6
|
extend Countable
|
7
|
+
|
7
8
|
self.timeout = 90
|
8
9
|
self.include_root_in_json = false
|
9
10
|
self.headers['User-Agent'] = ["ShopifyAPI/#{ShopifyAPI::VERSION}",
|
@@ -28,6 +29,8 @@ module ShopifyAPI
|
|
28
29
|
end
|
29
30
|
|
30
31
|
class << self
|
32
|
+
threadsafe_attribute(:_api_version)
|
33
|
+
|
31
34
|
if ActiveResource::Base.respond_to?(:_headers) && ActiveResource::Base.respond_to?(:_headers_defined?)
|
32
35
|
def headers
|
33
36
|
if _headers_defined?
|
@@ -54,21 +57,80 @@ module ShopifyAPI
|
|
54
57
|
raise InvalidSessionError.new("Session cannot be nil") if session.nil?
|
55
58
|
self.site = session.site
|
56
59
|
self.headers.merge!('X-Shopify-Access-Token' => session.token)
|
60
|
+
self.api_version = session.api_version
|
57
61
|
end
|
58
62
|
|
59
63
|
def clear_session
|
60
64
|
self.site = nil
|
61
65
|
self.password = nil
|
62
66
|
self.user = nil
|
67
|
+
self.api_version = nil
|
63
68
|
self.headers.delete('X-Shopify-Access-Token')
|
64
69
|
end
|
65
70
|
|
71
|
+
def api_version
|
72
|
+
if _api_version_defined?
|
73
|
+
_api_version
|
74
|
+
elsif superclass != Object && superclass.site
|
75
|
+
superclass.api_version.dup.freeze
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def api_version=(value)
|
80
|
+
self._api_version = value
|
81
|
+
end
|
82
|
+
|
83
|
+
def prefix(options = {})
|
84
|
+
api_version.construct_api_path(resource_prefix(options))
|
85
|
+
end
|
86
|
+
|
87
|
+
def prefix_source
|
88
|
+
''
|
89
|
+
end
|
90
|
+
|
91
|
+
def resource_prefix(_options = {})
|
92
|
+
''
|
93
|
+
end
|
94
|
+
|
95
|
+
# Sets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.json</tt>).
|
96
|
+
# Default value is <tt>site.path</tt>.
|
97
|
+
def resource_prefix=(value)
|
98
|
+
@prefix_parameters = nil
|
99
|
+
|
100
|
+
resource_prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.parser.escape options[#{key}].to_s}" }
|
101
|
+
|
102
|
+
silence_warnings do
|
103
|
+
# Redefine the new methods.
|
104
|
+
instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
105
|
+
def prefix_source() "#{value}" end
|
106
|
+
def resource_prefix(options={}) "#{resource_prefix_call}" end
|
107
|
+
RUBY_EVAL
|
108
|
+
end
|
109
|
+
rescue => e
|
110
|
+
logger&.error("Couldn't set prefix: #{e}\n #{code}")
|
111
|
+
raise
|
112
|
+
end
|
113
|
+
|
114
|
+
def prefix=(value)
|
115
|
+
if value.start_with?('/admin')
|
116
|
+
raise ArgumentError, "'#{value}' can no longer start /admin/. Change to using resource_prefix="
|
117
|
+
end
|
118
|
+
|
119
|
+
warn(
|
120
|
+
'[DEPRECATED] ShopifyAPI::Base#prefix= is deprecated and will be removed in a future version. ' \
|
121
|
+
'Use `self.resource_prefix=` instead.'
|
122
|
+
)
|
123
|
+
self.resource_prefix = value
|
124
|
+
end
|
125
|
+
|
126
|
+
alias_method :set_prefix, :prefix=
|
127
|
+
|
66
128
|
def init_prefix(resource)
|
67
129
|
init_prefix_explicit(resource.to_s.pluralize, "#{resource}_id")
|
68
130
|
end
|
69
131
|
|
70
132
|
def init_prefix_explicit(resource_type, resource_id)
|
71
|
-
self.
|
133
|
+
self.resource_prefix = "#{resource_type}/:#{resource_id}/"
|
72
134
|
|
73
135
|
define_method resource_id.to_sym do
|
74
136
|
@prefix_options[resource_id]
|
@@ -7,7 +7,7 @@ module ShopifyAPI
|
|
7
7
|
class GraphQL
|
8
8
|
def initialize
|
9
9
|
uri = Base.site.dup
|
10
|
-
uri.path =
|
10
|
+
uri.path = Base.api_version.construct_graphql_path
|
11
11
|
@http = ::GraphQL::Client::HTTP.new(uri.to_s) do
|
12
12
|
define_method(:headers) do |_context|
|
13
13
|
Base.headers
|
@@ -4,11 +4,11 @@ module ShopifyAPI
|
|
4
4
|
class InventoryLevel < Base
|
5
5
|
|
6
6
|
# The default path structure in ActiveResource for delete would result in:
|
7
|
-
# /admin/inventory_levels/#{ inventory_level.id }.json?#{ params }, but since
|
7
|
+
# /admin/api/<version>/inventory_levels/#{ inventory_level.id }.json?#{ params }, but since
|
8
8
|
# InventroyLevels are a second class resource made up of a Where and a What
|
9
9
|
# (Location and InventoryItem), it does not have a resource ID. Here we
|
10
10
|
# redefine element_path to remove the id so HTTP DELETE requests go to
|
11
|
-
# /admin/inventory_levels.json?#{ params } instead.
|
11
|
+
# /admin/api/<version>/inventory_levels.json?#{ params } instead.
|
12
12
|
#
|
13
13
|
def self.element_path(prefix_options = {}, query_options = nil)
|
14
14
|
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
@@ -2,7 +2,7 @@ module ShopifyAPI
|
|
2
2
|
class Location < Base
|
3
3
|
|
4
4
|
def inventory_levels
|
5
|
-
ShopifyAPI::InventoryLevel.find(:all, from: "
|
5
|
+
ShopifyAPI::InventoryLevel.find(:all, from: "#{self.class.prefix}locations/#{id}/inventory_levels.json")
|
6
6
|
end
|
7
7
|
end
|
8
8
|
end
|
@@ -4,9 +4,10 @@ module ShopifyAPI
|
|
4
4
|
|
5
5
|
def self.calculate(*args)
|
6
6
|
options = { :refund => args[0] }
|
7
|
-
params =
|
8
|
-
|
9
|
-
|
7
|
+
params = {}
|
8
|
+
params = args[1][:params] if args[1] && args[1][:params]
|
9
|
+
|
10
|
+
resource = post(:calculate, params, options.to_json)
|
10
11
|
instantiate_record(format.decode(resource.body), {})
|
11
12
|
end
|
12
13
|
end
|
@@ -2,8 +2,10 @@ module ShopifyAPI
|
|
2
2
|
# Shop object. Use Shop.current to receive
|
3
3
|
# the shop.
|
4
4
|
class Shop < Base
|
5
|
-
|
6
|
-
|
5
|
+
include ActiveResource::Singleton
|
6
|
+
|
7
|
+
def self.current(options = {})
|
8
|
+
find(options)
|
7
9
|
end
|
8
10
|
|
9
11
|
def metafields(**options)
|
@@ -5,7 +5,7 @@ module ShopifyAPI
|
|
5
5
|
|
6
6
|
def products(options = {})
|
7
7
|
if options.present?
|
8
|
-
Product.find(:all, from: "
|
8
|
+
Product.find(:all, from: "#{self.class.prefix}smart_collections/#{id}/products.json", params: options)
|
9
9
|
else
|
10
10
|
Product.find(:all, params: { collection_id: id })
|
11
11
|
end
|
data/lib/shopify_api/session.rb
CHANGED
@@ -10,7 +10,9 @@ module ShopifyAPI
|
|
10
10
|
cattr_accessor :api_key, :secret, :myshopify_domain
|
11
11
|
self.myshopify_domain = 'myshopify.com'
|
12
12
|
|
13
|
-
attr_accessor :
|
13
|
+
attr_accessor :domain, :token, :name, :extra
|
14
|
+
attr_reader :api_version
|
15
|
+
alias_method :url, :domain
|
14
16
|
|
15
17
|
class << self
|
16
18
|
|
@@ -18,11 +20,14 @@ module ShopifyAPI
|
|
18
20
|
params.each { |k,value| public_send("#{k}=", value) }
|
19
21
|
end
|
20
22
|
|
21
|
-
def temp(domain
|
22
|
-
session = new(domain, token)
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
def temp(domain:, token:, api_version:, &block)
|
24
|
+
session = new(domain: domain, token: token, api_version: api_version)
|
25
|
+
|
26
|
+
with_session(session, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_session(session, &_block)
|
30
|
+
original_session = extract_current_session
|
26
31
|
|
27
32
|
begin
|
28
33
|
ShopifyAPI::Base.activate_session(session)
|
@@ -32,12 +37,19 @@ module ShopifyAPI
|
|
32
37
|
end
|
33
38
|
end
|
34
39
|
|
35
|
-
def
|
36
|
-
|
40
|
+
def with_version(api_version, &block)
|
41
|
+
original_session = extract_current_session
|
42
|
+
session = new(domain: original_session.site, token: original_session.token, api_version: api_version)
|
43
|
+
|
44
|
+
with_session(session, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepare_domain(domain)
|
48
|
+
return nil if domain.blank?
|
37
49
|
# remove http:// or https://
|
38
|
-
|
50
|
+
domain = domain.strip.gsub(%r{\Ahttps?://}, '')
|
39
51
|
# extract host, removing any username, password or path
|
40
|
-
shop = URI.parse("https://#{
|
52
|
+
shop = URI.parse("https://#{domain}").host
|
41
53
|
# extract subdomain of .myshopify.com
|
42
54
|
if idx = shop.index(".")
|
43
55
|
shop = shop.slice(0, idx)
|
@@ -63,10 +75,18 @@ module ShopifyAPI
|
|
63
75
|
params = params.except(:signature, :hmac, :action, :controller)
|
64
76
|
params.map{|k,v| "#{URI.escape(k.to_s, '&=%')}=#{URI.escape(v.to_s, '&%')}"}.sort.join('&')
|
65
77
|
end
|
78
|
+
|
79
|
+
def extract_current_session
|
80
|
+
site = ShopifyAPI::Base.site.to_s
|
81
|
+
token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
|
82
|
+
version = ShopifyAPI::Base.api_version
|
83
|
+
new(domain: site, token: token, api_version: version)
|
84
|
+
end
|
66
85
|
end
|
67
86
|
|
68
|
-
def initialize(
|
69
|
-
self.
|
87
|
+
def initialize(domain:, token:, api_version:, extra: {})
|
88
|
+
self.domain = self.class.prepare_domain(domain)
|
89
|
+
self.api_version = api_version
|
70
90
|
self.token = token
|
71
91
|
self.extra = extra
|
72
92
|
end
|
@@ -74,7 +94,7 @@ module ShopifyAPI
|
|
74
94
|
def create_permission_url(scope, redirect_uri = nil)
|
75
95
|
params = {:client_id => api_key, :scope => scope.join(',')}
|
76
96
|
params[:redirect_uri] = redirect_uri if redirect_uri
|
77
|
-
"
|
97
|
+
construct_oauth_url("authorize", params)
|
78
98
|
end
|
79
99
|
|
80
100
|
def request_token(params)
|
@@ -103,11 +123,15 @@ module ShopifyAPI
|
|
103
123
|
end
|
104
124
|
|
105
125
|
def site
|
106
|
-
"https://#{
|
126
|
+
"https://#{domain}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def api_version=(version)
|
130
|
+
@api_version = version.nil? ? nil : ApiVersion.coerce_to_version(version)
|
107
131
|
end
|
108
132
|
|
109
133
|
def valid?
|
110
|
-
|
134
|
+
domain.present? && token.present? && api_version.present?
|
111
135
|
end
|
112
136
|
|
113
137
|
def expires_in
|
@@ -132,12 +156,17 @@ module ShopifyAPI
|
|
132
156
|
end
|
133
157
|
|
134
158
|
def access_token_request(code)
|
135
|
-
uri = URI.parse(
|
159
|
+
uri = URI.parse(construct_oauth_url('access_token'))
|
136
160
|
https = Net::HTTP.new(uri.host, uri.port)
|
137
161
|
https.use_ssl = true
|
138
162
|
request = Net::HTTP::Post.new(uri.request_uri)
|
139
163
|
request.set_form_data('client_id' => api_key, 'client_secret' => secret, 'code' => code)
|
140
164
|
https.request(request)
|
141
165
|
end
|
166
|
+
|
167
|
+
def construct_oauth_url(path, query_params = {})
|
168
|
+
query_string = "?#{parameterize(query_params)}" unless query_params.empty?
|
169
|
+
"https://#{domain}/admin/oauth/#{path}#{query_string}"
|
170
|
+
end
|
142
171
|
end
|
143
172
|
end
|
data/lib/shopify_api/version.rb
CHANGED
data/shopify_api.gemspec
CHANGED
@@ -23,9 +23,9 @@ Gem::Specification.new do |s|
|
|
23
23
|
s.summary = %q{ShopifyAPI is a lightweight gem for accessing the Shopify admin REST web services}
|
24
24
|
s.license = "MIT"
|
25
25
|
|
26
|
-
s.required_ruby_version = ">= 2.
|
26
|
+
s.required_ruby_version = ">= 2.4"
|
27
27
|
|
28
|
-
s.add_runtime_dependency("activeresource", ">=
|
28
|
+
s.add_runtime_dependency("activeresource", ">= 4.1.0", "< 6.0.0")
|
29
29
|
s.add_runtime_dependency("rack")
|
30
30
|
s.add_runtime_dependency("graphql-client")
|
31
31
|
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
|
|
34
34
|
s.add_development_dependency("minitest", ">= 4.0")
|
35
35
|
s.add_development_dependency("rake")
|
36
36
|
s.add_development_dependency("timecop")
|
37
|
+
s.add_development_dependency("rubocop")
|
37
38
|
s.add_development_dependency("pry")
|
38
39
|
s.add_development_dependency("pry-byebug")
|
39
40
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
class AccessScopeTest < Test::Unit::TestCase
|
5
|
+
test 'access scope does not use the versioned resource urls' do
|
6
|
+
fake(
|
7
|
+
'access_scopes',
|
8
|
+
url: 'https://shop2.myshopify.com/admin/oauth/access_scopes.json',
|
9
|
+
method: :get,
|
10
|
+
status: 201,
|
11
|
+
body: load_fixture('access_scopes'),
|
12
|
+
extension: false
|
13
|
+
)
|
14
|
+
|
15
|
+
unstable_version = ShopifyAPI::Session.new(domain: 'shop2.myshopify.com', token: 'token2', api_version: :unstable)
|
16
|
+
|
17
|
+
ShopifyAPI::Base.activate_session(unstable_version)
|
18
|
+
|
19
|
+
scope_handles = ShopifyAPI::AccessScope.find(:all).map(&:handle)
|
20
|
+
|
21
|
+
assert_equal(['write_product_listings', 'read_shipping'], scope_handles)
|
22
|
+
end
|
23
|
+
end
|