shopify_api 13.1.0 → 13.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/workflows/build.yml +4 -0
  4. data/BREAKING_CHANGES_FOR_V10.md +231 -0
  5. data/CHANGELOG.md +14 -1
  6. data/CONTRIBUTING.md +25 -0
  7. data/Gemfile.lock +8 -15
  8. data/README.md +10 -34
  9. data/ROADMAP.md +10 -0
  10. data/docs/README.md +0 -1
  11. data/docs/getting_started.md +20 -3
  12. data/docs/usage/custom_apps.md +75 -0
  13. data/docs/usage/graphql.md +63 -18
  14. data/docs/usage/oauth.md +160 -27
  15. data/docs/usage/rest.md +204 -62
  16. data/docs/usage/webhooks.md +22 -4
  17. data/lib/shopify_api/admin_versions.rb +2 -1
  18. data/lib/shopify_api/auth/jwt_payload.rb +2 -2
  19. data/lib/shopify_api/auth/oauth.rb +15 -5
  20. data/lib/shopify_api/clients/http_client.rb +5 -1
  21. data/lib/shopify_api/context.rb +11 -5
  22. data/lib/shopify_api/rest/base.rb +22 -14
  23. data/lib/shopify_api/rest/resources/2022_10/assigned_fulfillment_order.rb +5 -3
  24. data/lib/shopify_api/rest/resources/2022_10/customer_address.rb +10 -0
  25. data/lib/shopify_api/rest/resources/2022_10/fulfillment_request.rb +10 -0
  26. data/lib/shopify_api/rest/resources/2022_10/order_risk.rb +5 -3
  27. data/lib/shopify_api/rest/resources/2023_01/assigned_fulfillment_order.rb +5 -3
  28. data/lib/shopify_api/rest/resources/2023_01/balance.rb +4 -0
  29. data/lib/shopify_api/rest/resources/2023_01/customer_address.rb +10 -0
  30. data/lib/shopify_api/rest/resources/2023_01/fulfillment_request.rb +10 -0
  31. data/lib/shopify_api/rest/resources/2023_01/order.rb +3 -0
  32. data/lib/shopify_api/rest/resources/2023_01/order_risk.rb +5 -3
  33. data/lib/shopify_api/rest/resources/2023_01/shop.rb +0 -3
  34. data/lib/shopify_api/rest/resources/2023_01/variant.rb +1 -5
  35. data/lib/shopify_api/rest/resources/2023_04/assigned_fulfillment_order.rb +5 -3
  36. data/lib/shopify_api/rest/resources/2023_04/balance.rb +4 -0
  37. data/lib/shopify_api/rest/resources/2023_04/customer_address.rb +10 -0
  38. data/lib/shopify_api/rest/resources/2023_04/fulfillment_request.rb +10 -0
  39. data/lib/shopify_api/rest/resources/2023_04/order.rb +3 -0
  40. data/lib/shopify_api/rest/resources/2023_04/order_risk.rb +5 -3
  41. data/lib/shopify_api/rest/resources/2023_04/shop.rb +0 -3
  42. data/lib/shopify_api/rest/resources/2023_04/variant.rb +1 -5
  43. data/lib/shopify_api/rest/resources/2023_07/assigned_fulfillment_order.rb +5 -3
  44. data/lib/shopify_api/rest/resources/2023_07/balance.rb +4 -0
  45. data/lib/shopify_api/rest/resources/2023_07/customer_address.rb +10 -0
  46. data/lib/shopify_api/rest/resources/2023_07/fulfillment_request.rb +10 -0
  47. data/lib/shopify_api/rest/resources/2023_07/order.rb +3 -0
  48. data/lib/shopify_api/rest/resources/2023_07/order_risk.rb +5 -3
  49. data/lib/shopify_api/rest/resources/2023_07/report.rb +121 -0
  50. data/lib/shopify_api/rest/resources/2023_07/shop.rb +0 -3
  51. data/lib/shopify_api/rest/resources/2023_07/variant.rb +1 -5
  52. data/lib/shopify_api/rest/resources/2023_10/abandoned_checkout.rb +190 -0
  53. data/lib/shopify_api/rest/resources/2023_10/access_scope.rb +58 -0
  54. data/lib/shopify_api/rest/resources/2023_10/apple_pay_certificate.rb +105 -0
  55. data/lib/shopify_api/rest/resources/2023_10/application_charge.rb +109 -0
  56. data/lib/shopify_api/rest/resources/2023_10/application_credit.rb +91 -0
  57. data/lib/shopify_api/rest/resources/2023_10/article.rb +265 -0
  58. data/lib/shopify_api/rest/resources/2023_10/asset.rb +118 -0
  59. data/lib/shopify_api/rest/resources/2023_10/assigned_fulfillment_order.rb +88 -0
  60. data/lib/shopify_api/rest/resources/2023_10/balance.rb +54 -0
  61. data/lib/shopify_api/rest/resources/2023_10/blog.rb +162 -0
  62. data/lib/shopify_api/rest/resources/2023_10/cancellation_request.rb +83 -0
  63. data/lib/shopify_api/rest/resources/2023_10/carrier_service.rb +116 -0
  64. data/lib/shopify_api/rest/resources/2023_10/checkout.rb +209 -0
  65. data/lib/shopify_api/rest/resources/2023_10/collect.rb +142 -0
  66. data/lib/shopify_api/rest/resources/2023_10/collection.rb +110 -0
  67. data/lib/shopify_api/rest/resources/2023_10/collection_listing.rb +155 -0
  68. data/lib/shopify_api/rest/resources/2023_10/comment.rb +283 -0
  69. data/lib/shopify_api/rest/resources/2023_10/country.rb +137 -0
  70. data/lib/shopify_api/rest/resources/2023_10/currency.rb +57 -0
  71. data/lib/shopify_api/rest/resources/2023_10/custom_collection.rb +187 -0
  72. data/lib/shopify_api/rest/resources/2023_10/customer.rb +329 -0
  73. data/lib/shopify_api/rest/resources/2023_10/customer_address.rb +211 -0
  74. data/lib/shopify_api/rest/resources/2023_10/deprecated_api_call.rb +57 -0
  75. data/lib/shopify_api/rest/resources/2023_10/discount_code.rb +222 -0
  76. data/lib/shopify_api/rest/resources/2023_10/dispute.rb +111 -0
  77. data/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb +117 -0
  78. data/lib/shopify_api/rest/resources/2023_10/dispute_file_upload.rb +81 -0
  79. data/lib/shopify_api/rest/resources/2023_10/draft_order.rb +275 -0
  80. data/lib/shopify_api/rest/resources/2023_10/event.rb +148 -0
  81. data/lib/shopify_api/rest/resources/2023_10/fulfillment.rb +231 -0
  82. data/lib/shopify_api/rest/resources/2023_10/fulfillment_event.rb +166 -0
  83. data/lib/shopify_api/rest/resources/2023_10/fulfillment_order.rb +312 -0
  84. data/lib/shopify_api/rest/resources/2023_10/fulfillment_request.rb +97 -0
  85. data/lib/shopify_api/rest/resources/2023_10/fulfillment_service.rb +130 -0
  86. data/lib/shopify_api/rest/resources/2023_10/gift_card.rb +218 -0
  87. data/lib/shopify_api/rest/resources/2023_10/gift_card_adjustment.rb +118 -0
  88. data/lib/shopify_api/rest/resources/2023_10/image.rb +157 -0
  89. data/lib/shopify_api/rest/resources/2023_10/inventory_item.rb +108 -0
  90. data/lib/shopify_api/rest/resources/2023_10/inventory_level.rb +179 -0
  91. data/lib/shopify_api/rest/resources/2023_10/location.rb +167 -0
  92. data/lib/shopify_api/rest/resources/2023_10/locations_for_move.rb +56 -0
  93. data/lib/shopify_api/rest/resources/2023_10/marketing_event.rb +209 -0
  94. data/lib/shopify_api/rest/resources/2023_10/metafield.rb +344 -0
  95. data/lib/shopify_api/rest/resources/2023_10/mobile_platform_application.rb +110 -0
  96. data/lib/shopify_api/rest/resources/2023_10/order.rb +491 -0
  97. data/lib/shopify_api/rest/resources/2023_10/order_risk.rb +144 -0
  98. data/lib/shopify_api/rest/resources/2023_10/page.rb +194 -0
  99. data/lib/shopify_api/rest/resources/2023_10/payment.rb +140 -0
  100. data/lib/shopify_api/rest/resources/2023_10/payment_gateway.rb +143 -0
  101. data/lib/shopify_api/rest/resources/2023_10/payment_transaction.rb +107 -0
  102. data/lib/shopify_api/rest/resources/2023_10/payout.rb +97 -0
  103. data/lib/shopify_api/rest/resources/2023_10/policy.rb +69 -0
  104. data/lib/shopify_api/rest/resources/2023_10/price_rule.rb +223 -0
  105. data/lib/shopify_api/rest/resources/2023_10/product.rb +223 -0
  106. data/lib/shopify_api/rest/resources/2023_10/product_listing.rb +196 -0
  107. data/lib/shopify_api/rest/resources/2023_10/product_resource_feedback.rb +88 -0
  108. data/lib/shopify_api/rest/resources/2023_10/province.rb +132 -0
  109. data/lib/shopify_api/rest/resources/2023_10/recurring_application_charge.rb +172 -0
  110. data/lib/shopify_api/rest/resources/2023_10/redirect.rb +139 -0
  111. data/lib/shopify_api/rest/resources/2023_10/refund.rb +151 -0
  112. data/lib/shopify_api/rest/resources/2023_10/report.rb +121 -0
  113. data/lib/shopify_api/rest/resources/2023_10/resource_feedback.rb +73 -0
  114. data/lib/shopify_api/rest/resources/2023_10/script_tag.rb +155 -0
  115. data/lib/shopify_api/rest/resources/2023_10/shipping_zone.rb +83 -0
  116. data/lib/shopify_api/rest/resources/2023_10/shop.rb +218 -0
  117. data/lib/shopify_api/rest/resources/2023_10/smart_collection.rb +216 -0
  118. data/lib/shopify_api/rest/resources/2023_10/storefront_access_token.rb +87 -0
  119. data/lib/shopify_api/rest/resources/2023_10/tender_transaction.rb +93 -0
  120. data/lib/shopify_api/rest/resources/2023_10/theme.rb +123 -0
  121. data/lib/shopify_api/rest/resources/2023_10/transaction.rb +184 -0
  122. data/lib/shopify_api/rest/resources/2023_10/usage_charge.rb +102 -0
  123. data/lib/shopify_api/rest/resources/2023_10/user.rb +138 -0
  124. data/lib/shopify_api/rest/resources/2023_10/variant.rb +208 -0
  125. data/lib/shopify_api/rest/resources/2023_10/webhook.rb +168 -0
  126. data/lib/shopify_api/version.rb +1 -1
  127. data/lib/shopify_api/webhooks/registration.rb +19 -4
  128. data/lib/shopify_api/webhooks/registrations/event_bridge.rb +1 -1
  129. data/lib/shopify_api/webhooks/registrations/http.rb +1 -1
  130. data/lib/shopify_api/webhooks/registrations/pub_sub.rb +2 -1
  131. data/lib/shopify_api/webhooks/registry.rb +25 -5
  132. data/shopify_api.gemspec +0 -1
  133. metadata +81 -20
  134. data/.github/workflows/stale.yml +0 -43
  135. data/docs/issues.md +0 -39
  136. data/docs/usage/session_storage.md +0 -46
data/docs/usage/rest.md CHANGED
@@ -1,10 +1,153 @@
1
1
  # Make a REST API call
2
2
 
3
- Once OAuth is complete, we can use the `ShopifyAPI::Clients::Rest::Admin` client to make an API call to the Shopify Admin API. To do this, you can create an instance of `ShopifyAPI::Clients::Rest::Admin` using the current session to make requests to the Admin API.
3
+ Once OAuth is complete, we can use `ShopifyAPI`'s REST library to make authenticated API calls to the Shopify Admin API.
4
+ #### Required Session
5
+ Every API request requires a valid
6
+ [ShopifyAPI::Auth::Session](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/session.rb).
7
+
8
+ To instantiate a session, we recommend you either use the `shopify_app` if working in Rails, or refer to our OAuth docs on constructing a session:
9
+ - ["Custom Apps"](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/custom_apps.md) - documentation on how to create Session from a custom app API token.
10
+ - ["Performing OAuth"](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/oauth.md) - documentation on how to create new sessions
11
+ - [[ShopifyApp] - "Session"](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/sessions.md) - documentation on session handling if you're using the [`ShopifyApp`](https://github.com/Shopify/shopify_app) gem.
12
+
13
+ #### There are 2 methods you can use to make REST API calls to Shopify:
14
+ - [Using REST Resources](#using-rest-resources)
15
+ - Resource classes with similar syntax as `ActiveResource`, and follows our REST convention. Example:
16
+ ``` ruby
17
+ # Update product title
18
+ product = ShopifyAPI::Product.find(id: <product_id>)
19
+ product.title = "My awesome product"
20
+ product.save!
21
+ ```
22
+
23
+ - [Using REST Admin Client](#using-rest-admin-client)
24
+ - More manual input method to make the API call. Example:
25
+ ```ruby
26
+ # Create a new client.
27
+ rest_client = ShopifyAPI::Clients::Rest::Admin.new
28
+
29
+ # Update product title
30
+ body = {
31
+ product: {
32
+ title: "My cool product"
33
+ }
34
+ }
35
+
36
+ # Use `client.put` to send your request to the specified Shopify Admin REST API endpoint.
37
+ rest_client.put(path: "products/<id>.json", body: body)
38
+ ```
39
+
40
+ ## Using REST Resources
41
+ We provide a templated class library to access REST resources similar to `ActiveResource`. Format of the methods closely resemble our [REST API schema](https://shopify.dev/docs/api/admin-rest).
42
+
43
+ The version of REST resource that's loaded and used is set from [`ShopifyAPI::Context.setup`](https://github.com/Shopify/shopify-api-ruby/blob/main/README.md#setup-shopify-context)
44
+
45
+ ### Instantiation
46
+ Create an instance of the REST resource you'd like to use and optionally provide the following parameters.
47
+ #### Constructor parameters
48
+ | Parameter | Type | Notes |
49
+ | ----------|------|-------|
50
+ | `session` | `ShopifyAPI::Auth::Session` | Default value is `nil`. <br><br>When `nil` is passed in, active session information is inferred from `ShopifyAPI::Context.active_session`. <br>To set active session, use `ShopifyAPI::Context.activate_session`. <br><br>This is handled automatically behind the scenes if you use ShopifyApp's [session controllers](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/sessions.md). |
51
+ | `from_hash` | `Hash` | Default value is `nil`. Sets the resource properties to the values provided from the hash. |
52
+
53
+ Examples:
54
+
55
+ ```ruby
56
+ # To construct an Orders object using default session
57
+ # This creates a new order object with properties provided from the hash
58
+ order = ShopifyAPI::Orders.new(from_hash: {property: value})
59
+ order.save!
60
+ ```
4
61
 
5
- ## Methods
62
+ ### Methods
63
+ Typical methods provided for each resources are:
64
+ - `find`
65
+ - `delete`
66
+ - `all`
67
+ - `count`
68
+
69
+ Full list of methods can be found on each of the resource class.
70
+ - Path:
71
+ - https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/rest/resources/#{version}/#{resource}.rb
72
+ - Example for `Order` resource on `2023-04` version:
73
+ - https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/rest/resources/2023_04/order.rb
74
+
75
+ ### Usage Examples
76
+ ⚠️ Reference documentation on [shopify.dev](https://shopify.dev/docs/api/admin-rest) contains more examples on how to use each REST Resources.
77
+
78
+ ```Ruby
79
+ # Find and update a customer email
80
+ customer = ShopifyAPI::Customer.find(id: customer_id)
81
+ customer.email = "steve-lastnameson@example.com"
82
+ customer.save!
83
+
84
+ # Create a new product from hash
85
+ product_properties = {
86
+ title: "My awesome product"
87
+ }
88
+ product = ShopifyAPI::Product.new(from_hash: product_properties)
89
+ product.save!
90
+
91
+ # Create a product manually
92
+ product = ShopifyAPI::Product.new
93
+ product.title = "Another one"
94
+ product.save!
95
+
96
+ # Get all orders
97
+ orders = ShopifyAPI::Orders.all
6
98
 
7
- The Rest Admin client offers the 4 core request methods: `get`, `delete`, `post`, and `put`. These methods each take the parameters outlined in the table below. If the request is successful these methods will all return a `ShopifyAPI::Clients::HttpResponse` object, which has properties `code`, `headers`, and `body` otherwise an error will be raised describing what went wrong.
99
+ # Retrieve a specific fulfillment order
100
+ fulfillment_order_id = 123456789
101
+ fulfillment_order = ShopifyAPI::FulfillmentOrder.find(id: fulfillment_order_id)
102
+
103
+ # Remove an existing product image
104
+ product_id = 1234567
105
+ image_id = 1233211234567
106
+ ShopifyAPI::Image.delete(product_id: product_id, id: image_id)
107
+ ```
108
+
109
+ More examples can be found in each resource's documentation on [shopify.dev](https://shopify.dev/docs/api/admin-rest), e.g.:
110
+ - [Order](https://shopify.dev/docs/api/admin-rest/current/resources/order)
111
+ - [Product](https://shopify.dev/docs/api/admin-rest/current/resources/product)
112
+
113
+ ## Using REST Admin Client
114
+
115
+ ### Instantiation
116
+ Create an instance of [`ShopifyAPI::Clients::Rest::Admin`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/clients/rest/admin.rb) using the current session to make requests to the Admin API.
117
+ #### Constructor parameters
118
+ | Parameter | Type | Notes |
119
+ | ----------|------|-------|
120
+ | `session` | `ShopifyAPI::Auth::Session` | Default value is `nil`. <br><br>When `nil` is passed in, active session information is inferred from `ShopifyAPI::Context.active_session`. <br>To set active session, use `ShopifyAPI::Context.activate_session`. <br><br>This is handled automatically behind the scenes if you use ShopifyApp's [session controllers](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/sessions.md). |
121
+ | `api_version` | `String` | Default value is `nil`. When `nil` is passed in, api version is inferred from [`ShopifyAPI::Context.setup`](https://github.com/Shopify/shopify-api-ruby/blob/main/README.md#setup-shopify-context).|
122
+
123
+ Examples:
124
+ ```ruby
125
+ # Create a default client with `ShopifyAPI::Context.api_version`
126
+ # and the active session from `ShopifyAPI::Context.active_session`
127
+ client = ShopifyAPI::Clients::Rest::Admin.new
128
+
129
+ # Create a client with a specific session "my_session"
130
+ client = ShopifyAPI::Clients::Rest::Admin.new(session: my_session)
131
+
132
+ # Create a client with active session from `ShopifyAPI::Context.active_session`
133
+ # and a specific api_version - "unstable"
134
+ client = ShopifyAPI::Clients::Rest::Admin.new(api_version: "unstable")
135
+
136
+ # Create a client with a specific session "my_session" and api_version "unstable"
137
+ client = ShopifyAPI::Clients::Rest::Admin.new(session: my_session, api_version: "unstable")
138
+ ```
139
+
140
+ ### Methods
141
+
142
+ The `ShopifyAPI::Clients::Rest::Admin` client offers the 4 core request methods:
143
+ - `get`
144
+ - `delete`
145
+ - `post`
146
+ - `put`
147
+
148
+ #### Input Parameters
149
+
150
+ Each method can take the parameters outlined in the table below.
8
151
 
9
152
  | Parameter | Type | Required in Methods | Default Value | Notes |
10
153
  | -------------- | -------------------------------------------------------- | :-----------------: | :-----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -14,16 +157,40 @@ The Rest Admin client offers the 4 core request methods: `get`, `delete`, `post`
14
157
  | `extraHeaders` | `Hash(any(Symbol, String), any(String, Integer, Float))` | none | none | Any additional headers you want to send with your request |
15
158
  | `tries` | `Integer` | None | `1` | The maximum number of times to try the request _(must be >= 0)_ |
16
159
 
17
- **Note:** _These paramaters can still be used in all methods regardless of if they are required._
160
+ **Note:** _These parameters can still be used in all methods regardless of if they are required._
18
161
 
19
- ## Usage Examples:
162
+ #### Output
163
+ ##### Success
164
+ If the request is successful these methods will all return a [`ShopifyAPI::Clients::HttpResponse`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/clients/http_response.rb) object, which has the following methods:
165
+ | Methods | Type | Notes |
166
+ |---------|------|-------|
167
+ | `code` |`Integer`| HTTP Response code, e.g. `200`|
168
+ | `header` |`Hash{String, [String]}` | HTTP Response headers |
169
+ | `body` | `Hash{String, Untyped}` | HTTP Response body |
170
+ | `prev_page_info` | `String` | See [Pagination](#pagination)|
171
+ | `next_page_info` | `String` | See [Pagination](#pagination)|
20
172
 
21
- ### Perform a `GET` request:
173
+ ##### Failure
174
+ If the request has failed, an error will be raised describing what went wrong.
175
+ You can rescue [`ShopifyAPI::Errors::HttpResponseError`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/errors/http_response_error.rb)
176
+ and output error messages with `errors.full_messages`
177
+
178
+ See example:
22
179
 
23
180
  ```ruby
24
- # Load the current session to get the `accessToken`.
25
- session = ShopifyAPI::Utils::SessionUtils.load_current_session(headers, cookies, is_online)
181
+ client = ShopifyAPI::Clients::Rest::Admin.new(session: session)
182
+ response = client.get(path: "NOT-REAL")
183
+ some_function(response.body)
184
+ rescue ShopifyAPI::Errors::HttpResponseError => e
185
+ puts fulfillment.errors.full_messages
186
+ # {"errors"=>"Not Found"}
187
+ # If you report this error, please include this id: bce76672-40c6-4047-b598-46208ab076f0.
188
+ ```
189
+ ### Usage Examples
190
+
191
+ #### Perform a `GET` request
26
192
 
193
+ ```ruby
27
194
  # Create a new client.
28
195
  client = ShopifyAPI::Clients::Rest::Admin.new(session: session)
29
196
 
@@ -33,13 +200,11 @@ response = client.get(path: "products")
33
200
  # Do something with the returned data
34
201
  some_function(response.body)
35
202
  ```
203
+ _For more information on the `products` endpoint, [check out our API reference guide](https://shopify.dev/docs/api/admin-rest/latest/resources/product)._
36
204
 
37
- ### Perform a `POST` request:
205
+ #### Perform a `POST` request
38
206
 
39
207
  ```ruby
40
- # Load the current session to get the `accessToken`.
41
- session = ShopifyAPI::Utils::SessionUtils.load_current_session(headers, cookies, is_online)
42
-
43
208
  # Create a new client.
44
209
  client = ShopifyAPI::Clients::Rest::Admin.new(session: session)
45
210
 
@@ -54,49 +219,61 @@ body = {
54
219
  }
55
220
 
56
221
  # Use `client.post` to send your request to the specified Shopify Admin REST API endpoint.
222
+ # This POST request will create a new product.
57
223
  client.post({
58
224
  path: "products",
59
225
  body: body,
60
226
  });
61
227
  ```
62
228
 
63
- _for more information on the `products` endpoint, [check out our API reference guide](https://shopify.dev/docs/api/admin-rest/unstable/resources/product)._
229
+ _For more information on the `products` endpoint, [check out our API reference guide](https://shopify.dev/docs/api/admin-rest/latest/resources/product)._
64
230
 
65
- ### Override the `api_version`:
231
+ #### Perform a `PUT` request
232
+ ```ruby
233
+ # Create a new client.
234
+ client = ShopifyAPI::Clients::Rest::Admin.new
66
235
 
67
- ```ruby
68
- # To experiment with prerelease features, pass the api_version "unstable".
69
- client = ShopifyAPI::Clients::Rest::Admin.new(session: session, api_version: "unstable")
236
+ # Update product title
237
+ body = {
238
+ product: {
239
+ title: "My cool product"
240
+ }
241
+ }
242
+
243
+ # Use `client.put` to send your request to the specified Shopify Admin REST API endpoint.
244
+ # This will update product title for product with ID <id>
245
+ client.put(path: "products/<id>.json", body: body)
70
246
  ```
71
247
 
72
- ## Pagination
248
+ _For more information on the `products` endpoint, [check out our API reference guide](https://shopify.dev/docs/api/admin-rest/latest/resources/product)._
249
+
250
+ ### Pagination
73
251
 
74
252
  This library also supports cursor-based pagination for REST Admin API requests. [Learn more about REST request pagination](https://shopify.dev/docs/api/usage/pagination-rest).
75
253
 
76
- After making a request, the `next_page_info` and `prev_page_info` can be found on the response object and passed as the page_info query param in other requests.
254
+ #### REST Admin Client
255
+ After making a request, the `next_page_info` and `prev_page_info` can be found on the response object and passed as the `page_info` query param in other requests.
77
256
 
78
257
  An example of this is shown below:
79
258
 
80
259
  ```ruby
81
- session = ShopifyAPI::Utils::SessionUtils.load_current_session(headers, cookies, is_online)
82
260
  client = ShopifyAPI::Clients::Rest::Admin.new(session: session)
83
261
 
84
262
  response = client.get(path: "products", query: { limit: 10 })
263
+ next_page_info = response.next_page_info
85
264
 
86
- loop do
87
- some_function(response.body)
88
- break unless response.next_page_info
89
- response = client.get(path: "products", query: { limit: 10, page_info: response.next_page_info })
265
+ if next_page_info
266
+ next_page_response =client.get(path: "products", query: { limit: 10, page_info: next_page_info })
267
+ some_function(next_page_response)
90
268
  end
91
269
  ```
92
270
 
93
- Similarly, when using REST resources the `next_page_info` and `prev_page_info` can be found on the Resource class and passed as the page_info query param in other requests.
271
+ #### REST Resource
272
+ Similarly, when using REST resources the `next_page_info` and `prev_page_info` can be found on the Resource class and passed as the `page_info` query param in other requests.
94
273
 
95
274
  An example of this is shown below:
96
275
 
97
276
  ```ruby
98
- session = ShopifyAPI::Utils::SessionUtils.load_current_session(headers, cookies, is_online)
99
-
100
277
  products = ShopifyAPI::Product.all(session: session, limit: 10)
101
278
 
102
279
  loop do
@@ -106,39 +283,4 @@ loop do
106
283
  end
107
284
  ```
108
285
 
109
- The next/previous page_info strings can also be retrieved from the response object and added to a request query to retrieve the next/previous pages.
110
-
111
- An example of this is shown below:
112
-
113
- ```ruby
114
- session = ShopifyAPI::Utils::SessionUtils.load_current_session(headers, cookies, is_online)
115
- client = ShopifyAPI::Clients::Rest::Admin.new(session: session)
116
-
117
- response = client.get(path: "products", query: { limit: 10 })
118
- next_page_info = response.next_page_info
119
-
120
- if next_page_info
121
- next_page_response =client.get(path: "products", query: { limit: 10, page_info: next_page_info })
122
- some_function(next_page_response)
123
- end
124
- ```
125
-
126
- ### Error Messages
127
-
128
- You can rescue `ShopifyAPI::Errors::HttpResponseError` and output error messages with `errors.full_messages`
129
-
130
- See example:
131
-
132
- ```ruby
133
- fulfillment = ShopifyAPI::Fulfillment.new(session: @session)
134
- fulfillment.order_id = 2776493818000
135
- ...
136
- fulfillment.tracking_company = "Jack Black's Pack, Stack and Track"
137
- fulfillment.save()
138
- rescue ShopifyAPI::Errors::HttpResponseError => e
139
- puts fulfillment.errors.full_messages
140
- # {"base"=>["Line items are already fulfilled"]}
141
- # If you report this error, please include this id: e712dde0-1270-4258-8cdb-d198792c917e.
142
- ```
143
-
144
286
  [Back to guide index](../README.md)
@@ -2,13 +2,16 @@
2
2
 
3
3
  The `shopify_api` gem provides webhook functionality to make it easy to both subscribe to and process webhooks. To implement in your app follow the steps outlined below.
4
4
 
5
+ ## Use with Rails
6
+ If using in the Rails framework, we highly recommend you use the [shopify_app](https://github.com/Shopify/shopify_app) gem to interact with this gem. That gem handles [webhooks with a declarative configuration](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/webhooks.md).
7
+
5
8
  ## Create a Webhook Handler
6
9
 
7
10
  If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the handle method which accepts the following named parameters: topic: `String`, shop: `String`, and body: `Hash[String, untyped]`. An example implementation is shown below:
8
11
 
9
12
  ```ruby
10
13
  module WebhookHandler
11
- include ShopifyAPI::Webhooks::Handler
14
+ extend ShopifyAPI::Webhooks::Handler
12
15
 
13
16
  class << self
14
17
  def handle(topic:, shop:, body:)
@@ -25,7 +28,10 @@ end
25
28
  The next step is to add all the webhooks you would like to subscribe to for any shop to the webhook registry. To do this you can call `ShopifyAPI::Webhooks::Registry.add_registration` for each webhook you would like to handle. `add_registration` accepts a topic string, a delivery_method symbol (currently supporting `:http`, `:event_bridge`, and `:pub_sub`), a webhook path (the relative path for an http webhook) and a handler. This only needs to be done once when the app is started and we recommend doing this at the same time that you setup `ShopifyAPI::Context`. An example is shown below to register an http webhook:
26
29
 
27
30
  ```ruby
28
- registration = ShopifyAPI::Webhooks::Registry.add_registration(topic: "orders/create", delivery_method: :http, handler: WebhookHandler)
31
+ registration = ShopifyAPI::Webhooks::Registry.add_registration(topic: "orders/create",
32
+ delivery_method: :http,
33
+ handler: WebhookHandler,
34
+ path: 'callback/orders/create')
29
35
  ```
30
36
  If you are only interested in particular fields, you can optionally filter the data sent by Shopify by specifying the `fields` parameter. Note that you will still receive a webhook request from Shopify every time the resource is updated, but only the specified fields will be sent:
31
37
 
@@ -34,8 +40,20 @@ registration = ShopifyAPI::Webhooks::Registry.add_registration(
34
40
  topic: "orders/create",
35
41
  delivery_method: :http,
36
42
  handler: WebhookHandler,
43
+ path: 'callback/orders/create',
37
44
  fields: ["number","note"] # this can also be a single comma separated string
38
- )
45
+ )
46
+ ```
47
+
48
+ If you are storing metafields on an object you are receiving webhooks for, you can specify them on registration to make sure that they are also sent through the `metafieldNamespaces` parameter. Note if you are also using the `fields` parameter you will need to add `metafields` into that as well.
49
+
50
+ ```ruby
51
+ registration = ShopifyAPI::Webhooks::Registry.add_registration(
52
+ topic: "orders/create",
53
+ delivery_method: :http,
54
+ handler: WebhookHandler,
55
+ metafieldNamespaces: ["custom"]
56
+ )
39
57
  ```
40
58
 
41
59
  **Note**: The webhooks you register with Shopify are saved in the Shopify platform, but the local `ShopifyAPI::Webhooks::Registry` needs to be reloaded whenever your server restarts.
@@ -75,7 +93,7 @@ ShopifyAPI::Webhooks::Registry.register(topic: "<specific-topic>", session: shop
75
93
 
76
94
  This will return a single `ShopifyAPI::Webhooks::RegisterResult`.
77
95
 
78
- ## Unregister a Webhook
96
+ ## Unregister a Webhook
79
97
 
80
98
  To unregister a topic from a shop you can simply call:
81
99
  ```ruby
@@ -5,6 +5,7 @@ module ShopifyAPI
5
5
  module AdminVersions
6
6
  SUPPORTED_ADMIN_VERSIONS = T.let([
7
7
  "unstable",
8
+ "2023-10",
8
9
  "2023-07",
9
10
  "2023-04",
10
11
  "2023-01",
@@ -14,7 +15,7 @@ module ShopifyAPI
14
15
  "2022-01",
15
16
  ], T::Array[String])
16
17
 
17
- LATEST_SUPPORTED_ADMIN_VERSION = T.let("2023-07", String)
18
+ LATEST_SUPPORTED_ADMIN_VERSION = T.let("2023-10", String)
18
19
  end
19
20
 
20
21
  SUPPORTED_ADMIN_VERSIONS = ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS
@@ -75,8 +75,8 @@ module ShopifyAPI
75
75
  def decode_token(token, api_secret_key)
76
76
  JWT.decode(token, api_secret_key, true,
77
77
  { exp_leeway: JWT_EXPIRATION_LEEWAY, algorithm: "HS256" })[0]
78
- rescue
79
- raise ShopifyAPI::Errors::InvalidJwtTokenError, "Failed to parse session token '#{token}'"
78
+ rescue JWT::DecodeError => err
79
+ raise ShopifyAPI::Errors::InvalidJwtTokenError, "Error decoding session token: #{err.message}"
80
80
  end
81
81
  end
82
82
  end
@@ -70,15 +70,25 @@ module ShopifyAPI
70
70
  raise Errors::InvalidOauthError,
71
71
  "Invalid state in OAuth callback." unless state == auth_query.state
72
72
 
73
- # TODO: replace this call with the HTTP client once it is built
73
+ null_session = Auth::Session.new(shop: auth_query.shop)
74
74
  body = { client_id: Context.api_key, client_secret: Context.api_secret_key, code: auth_query.code }
75
- response = HTTParty.post("https://#{auth_query.shop}/admin/oauth/access_token", body: body)
76
- unless response.ok?
75
+
76
+ client = Clients::HttpClient.new(session: null_session, base_path: "/admin/oauth")
77
+ response = begin
78
+ client.request(
79
+ Clients::HttpRequest.new(
80
+ http_method: :post,
81
+ path: "access_token",
82
+ body: body,
83
+ body_type: "application/json",
84
+ ),
85
+ )
86
+ rescue ShopifyAPI::Errors::HttpResponseError => e
77
87
  raise Errors::RequestAccessTokenError,
78
- "Cannot complete OAuth process. Received a #{response.code} error while requesting access token."
88
+ "Cannot complete OAuth process. Received a #{e.code} error while requesting access token."
79
89
  end
80
- session_params = response.to_h
81
90
 
91
+ session_params = T.cast(response.body, T::Hash[String, T.untyped]).to_h
82
92
  session = create_new_session(session_params, auth_query.shop)
83
93
 
84
94
  cookie = if Context.embedded?
@@ -13,7 +13,9 @@ module ShopifyAPI
13
13
  session ||= Context.active_session
14
14
  raise Errors::NoActiveSessionError, "No passed or active session" unless session
15
15
 
16
- @base_uri = T.let("https://#{session.shop}", String)
16
+ api_host = Context.api_host
17
+
18
+ @base_uri = T.let("https://#{api_host || session.shop}", String)
17
19
  @base_uri_and_path = T.let("#{@base_uri}#{base_path}", String)
18
20
 
19
21
  user_agent_prefix = Context.user_agent_prefix.nil? ? "" : "#{Context.user_agent_prefix} | "
@@ -23,6 +25,8 @@ module ShopifyAPI
23
25
  "Accept": "application/json",
24
26
  }, T::Hash[T.any(Symbol, String), T.untyped])
25
27
 
28
+ @headers["Host"] = session.shop unless api_host.nil?
29
+
26
30
  unless session.access_token.nil? || T.must(session.access_token).empty?
27
31
  @headers["X-Shopify-Access-Token"] = T.cast(session.access_token, String)
28
32
  end
@@ -8,11 +8,14 @@ module ShopifyAPI
8
8
  @api_key = T.let("", String)
9
9
  @api_secret_key = T.let("", String)
10
10
  @api_version = T.let(LATEST_SUPPORTED_ADMIN_VERSION, String)
11
+ @api_host = T.let(nil, T.nilable(String))
11
12
  @scope = T.let(Auth::AuthScopes.new, Auth::AuthScopes)
12
13
  @is_private = T.let(false, T::Boolean)
13
14
  @private_shop = T.let(nil, T.nilable(String))
14
15
  @is_embedded = T.let(true, T::Boolean)
15
- @logger = T.let(::Logger.new($stdout), ::Logger)
16
+ # Logger can either be a Logger or an ActiveSupport::BroadcastLogger, which is new in Rails 7.1.0. To avoid adding a
17
+ # dependency Active Support >= 7.1.0, we go with T.untyped
18
+ @logger = T.let(::Logger.new($stdout), T.untyped)
16
19
  @log_level = T.let(:info, Symbol)
17
20
  @notified_missing_resources_folder = T.let({}, T::Hash[String, T::Boolean])
18
21
  @active_session = T.let(Concurrent::ThreadLocalVar.new { nil }, T.nilable(Concurrent::ThreadLocalVar))
@@ -33,12 +36,13 @@ module ShopifyAPI
33
36
  is_private: T::Boolean,
34
37
  is_embedded: T::Boolean,
35
38
  log_level: T.any(String, Symbol),
36
- logger: ::Logger,
39
+ logger: T.untyped,
37
40
  host_name: T.nilable(String),
38
41
  host: T.nilable(String),
39
42
  private_shop: T.nilable(String),
40
43
  user_agent_prefix: T.nilable(String),
41
44
  old_api_secret_key: T.nilable(String),
45
+ api_host: T.nilable(String),
42
46
  ).void
43
47
  end
44
48
  def setup(
@@ -54,7 +58,8 @@ module ShopifyAPI
54
58
  host: ENV["HOST"] || "https://#{host_name}",
55
59
  private_shop: nil,
56
60
  user_agent_prefix: nil,
57
- old_api_secret_key: nil
61
+ old_api_secret_key: nil,
62
+ api_host: nil
58
63
  )
59
64
  unless ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS.include?(api_version)
60
65
  raise Errors::UnsupportedVersionError,
@@ -64,6 +69,7 @@ module ShopifyAPI
64
69
  @api_key = api_key
65
70
  @api_secret_key = api_secret_key
66
71
  @api_version = api_version
72
+ @api_host = api_host
67
73
  @host = T.let(host, T.nilable(String))
68
74
  @is_private = is_private
69
75
  @scope = Auth::AuthScopes.new(scope)
@@ -116,7 +122,7 @@ module ShopifyAPI
116
122
  sig { returns(Auth::AuthScopes) }
117
123
  attr_reader :scope
118
124
 
119
- sig { returns(::Logger) }
125
+ sig { returns(T.untyped) }
120
126
  attr_reader :logger
121
127
 
122
128
  sig { returns(Symbol) }
@@ -128,7 +134,7 @@ module ShopifyAPI
128
134
  end
129
135
 
130
136
  sig { returns(T.nilable(String)) }
131
- attr_reader :private_shop, :user_agent_prefix, :old_api_secret_key, :host
137
+ attr_reader :private_shop, :user_agent_prefix, :old_api_secret_key, :host, :api_host
132
138
 
133
139
  sig { returns(T::Boolean) }
134
140
  def embedded?
@@ -97,9 +97,9 @@ module ShopifyAPI
97
97
  class_name.underscore
98
98
  end
99
99
 
100
- sig { returns(String) }
101
- def json_response_body_name
102
- class_name
100
+ sig { returns(T::Array[String]) }
101
+ def json_response_body_names
102
+ [class_name]
103
103
  end
104
104
 
105
105
  sig { returns(T.nilable(String)) }
@@ -210,14 +210,16 @@ module ShopifyAPI
210
210
 
211
211
  body = T.cast(response.body, T::Hash[String, T.untyped])
212
212
 
213
- response_name = json_response_body_name
213
+ response_names = json_response_body_names
214
214
 
215
- if body.key?(response_name.pluralize) || (body.key?(response_name) && body[response_name].is_a?(Array))
216
- (body[response_name.pluralize] || body[response_name]).each do |entry|
217
- objects << create_instance(data: entry, session: session)
215
+ response_names.each do |response_name|
216
+ if body.key?(response_name.pluralize) || (body.key?(response_name) && body[response_name].is_a?(Array))
217
+ (body[response_name.pluralize] || body[response_name]).each do |entry|
218
+ objects << create_instance(data: entry, session: session)
219
+ end
220
+ elsif body.key?(response_name)
221
+ objects << create_instance(data: body[response_name], session: session)
218
222
  end
219
- elsif body.key?(response_name)
220
- objects << create_instance(data: body[response_name], session: session)
221
223
  end
222
224
 
223
225
  objects
@@ -347,10 +349,8 @@ module ShopifyAPI
347
349
  )
348
350
 
349
351
  if update_object
350
- self.class.create_instance(
351
- data: response.body[self.class.class_name.downcase],
352
- session: @session, instance: self
353
- )
352
+ response_name = self.class.json_response_body_names & response.body.keys
353
+ self.class.create_instance(data: response.body[response_name.first], session: @session, instance: self)
354
354
  end
355
355
  rescue ShopifyAPI::Errors::HttpResponseError => e
356
356
  @errors.errors << e
@@ -365,10 +365,18 @@ module ShopifyAPI
365
365
  self.class.read_only_attributes&.include?("@#{attribute}".to_sym)
366
366
  end
367
367
 
368
- HashDiff::Comparison.new(
368
+ diff = HashDiff::Comparison.new(
369
369
  deep_stringify_keys(original_state_for_update),
370
370
  deep_stringify_keys(to_hash(true)),
371
371
  ).left_diff
372
+
373
+ diff.each do |attribute, value|
374
+ if value.is_a?(Hash) && value[0] == HashDiff::NO_VALUE
375
+ diff[attribute] = send(attribute)
376
+ end
377
+ end
378
+
379
+ diff
372
380
  end
373
381
 
374
382
  sig { returns(Symbol) }
@@ -51,10 +51,12 @@ module ShopifyAPI
51
51
 
52
52
  class << self
53
53
  sig do
54
- returns(String)
54
+ returns(T::Array[String])
55
55
  end
56
- def json_response_body_name()
57
- "fulfillment_order"
56
+ def json_response_body_names()
57
+ [
58
+ "fulfillment_order"
59
+ ]
58
60
  end
59
61
 
60
62
  sig do
@@ -87,6 +87,16 @@ module ShopifyAPI
87
87
  "address"
88
88
  end
89
89
 
90
+ sig do
91
+ returns(T::Array[String])
92
+ end
93
+ def json_response_body_names()
94
+ [
95
+ "customer_address",
96
+ "address"
97
+ ]
98
+ end
99
+
90
100
  sig do
91
101
  params(
92
102
  id: T.any(Integer, String),
@@ -31,6 +31,16 @@ module ShopifyAPI
31
31
  attr_reader :fulfillment_order_id
32
32
 
33
33
  class << self
34
+ sig do
35
+ returns(T::Array[String])
36
+ end
37
+ def json_response_body_names()
38
+ [
39
+ "submitted_fulfillment_order",
40
+ "fulfillment_order"
41
+ ]
42
+ end
43
+
34
44
  end
35
45
 
36
46
  sig do