shopify_api 6.0.0 → 7.0.0
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/.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
|