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.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -1
- data/.github/workflows/build.yml +4 -0
- data/BREAKING_CHANGES_FOR_V10.md +231 -0
- data/CHANGELOG.md +14 -1
- data/CONTRIBUTING.md +25 -0
- data/Gemfile.lock +8 -15
- data/README.md +10 -34
- data/ROADMAP.md +10 -0
- data/docs/README.md +0 -1
- data/docs/getting_started.md +20 -3
- data/docs/usage/custom_apps.md +75 -0
- data/docs/usage/graphql.md +63 -18
- data/docs/usage/oauth.md +160 -27
- data/docs/usage/rest.md +204 -62
- data/docs/usage/webhooks.md +22 -4
- data/lib/shopify_api/admin_versions.rb +2 -1
- data/lib/shopify_api/auth/jwt_payload.rb +2 -2
- data/lib/shopify_api/auth/oauth.rb +15 -5
- data/lib/shopify_api/clients/http_client.rb +5 -1
- data/lib/shopify_api/context.rb +11 -5
- data/lib/shopify_api/rest/base.rb +22 -14
- data/lib/shopify_api/rest/resources/2022_10/assigned_fulfillment_order.rb +5 -3
- data/lib/shopify_api/rest/resources/2022_10/customer_address.rb +10 -0
- data/lib/shopify_api/rest/resources/2022_10/fulfillment_request.rb +10 -0
- data/lib/shopify_api/rest/resources/2022_10/order_risk.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_01/assigned_fulfillment_order.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_01/balance.rb +4 -0
- data/lib/shopify_api/rest/resources/2023_01/customer_address.rb +10 -0
- data/lib/shopify_api/rest/resources/2023_01/fulfillment_request.rb +10 -0
- data/lib/shopify_api/rest/resources/2023_01/order.rb +3 -0
- data/lib/shopify_api/rest/resources/2023_01/order_risk.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_01/shop.rb +0 -3
- data/lib/shopify_api/rest/resources/2023_01/variant.rb +1 -5
- data/lib/shopify_api/rest/resources/2023_04/assigned_fulfillment_order.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_04/balance.rb +4 -0
- data/lib/shopify_api/rest/resources/2023_04/customer_address.rb +10 -0
- data/lib/shopify_api/rest/resources/2023_04/fulfillment_request.rb +10 -0
- data/lib/shopify_api/rest/resources/2023_04/order.rb +3 -0
- data/lib/shopify_api/rest/resources/2023_04/order_risk.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_04/shop.rb +0 -3
- data/lib/shopify_api/rest/resources/2023_04/variant.rb +1 -5
- data/lib/shopify_api/rest/resources/2023_07/assigned_fulfillment_order.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_07/balance.rb +4 -0
- data/lib/shopify_api/rest/resources/2023_07/customer_address.rb +10 -0
- data/lib/shopify_api/rest/resources/2023_07/fulfillment_request.rb +10 -0
- data/lib/shopify_api/rest/resources/2023_07/order.rb +3 -0
- data/lib/shopify_api/rest/resources/2023_07/order_risk.rb +5 -3
- data/lib/shopify_api/rest/resources/2023_07/report.rb +121 -0
- data/lib/shopify_api/rest/resources/2023_07/shop.rb +0 -3
- data/lib/shopify_api/rest/resources/2023_07/variant.rb +1 -5
- data/lib/shopify_api/rest/resources/2023_10/abandoned_checkout.rb +190 -0
- data/lib/shopify_api/rest/resources/2023_10/access_scope.rb +58 -0
- data/lib/shopify_api/rest/resources/2023_10/apple_pay_certificate.rb +105 -0
- data/lib/shopify_api/rest/resources/2023_10/application_charge.rb +109 -0
- data/lib/shopify_api/rest/resources/2023_10/application_credit.rb +91 -0
- data/lib/shopify_api/rest/resources/2023_10/article.rb +265 -0
- data/lib/shopify_api/rest/resources/2023_10/asset.rb +118 -0
- data/lib/shopify_api/rest/resources/2023_10/assigned_fulfillment_order.rb +88 -0
- data/lib/shopify_api/rest/resources/2023_10/balance.rb +54 -0
- data/lib/shopify_api/rest/resources/2023_10/blog.rb +162 -0
- data/lib/shopify_api/rest/resources/2023_10/cancellation_request.rb +83 -0
- data/lib/shopify_api/rest/resources/2023_10/carrier_service.rb +116 -0
- data/lib/shopify_api/rest/resources/2023_10/checkout.rb +209 -0
- data/lib/shopify_api/rest/resources/2023_10/collect.rb +142 -0
- data/lib/shopify_api/rest/resources/2023_10/collection.rb +110 -0
- data/lib/shopify_api/rest/resources/2023_10/collection_listing.rb +155 -0
- data/lib/shopify_api/rest/resources/2023_10/comment.rb +283 -0
- data/lib/shopify_api/rest/resources/2023_10/country.rb +137 -0
- data/lib/shopify_api/rest/resources/2023_10/currency.rb +57 -0
- data/lib/shopify_api/rest/resources/2023_10/custom_collection.rb +187 -0
- data/lib/shopify_api/rest/resources/2023_10/customer.rb +329 -0
- data/lib/shopify_api/rest/resources/2023_10/customer_address.rb +211 -0
- data/lib/shopify_api/rest/resources/2023_10/deprecated_api_call.rb +57 -0
- data/lib/shopify_api/rest/resources/2023_10/discount_code.rb +222 -0
- data/lib/shopify_api/rest/resources/2023_10/dispute.rb +111 -0
- data/lib/shopify_api/rest/resources/2023_10/dispute_evidence.rb +117 -0
- data/lib/shopify_api/rest/resources/2023_10/dispute_file_upload.rb +81 -0
- data/lib/shopify_api/rest/resources/2023_10/draft_order.rb +275 -0
- data/lib/shopify_api/rest/resources/2023_10/event.rb +148 -0
- data/lib/shopify_api/rest/resources/2023_10/fulfillment.rb +231 -0
- data/lib/shopify_api/rest/resources/2023_10/fulfillment_event.rb +166 -0
- data/lib/shopify_api/rest/resources/2023_10/fulfillment_order.rb +312 -0
- data/lib/shopify_api/rest/resources/2023_10/fulfillment_request.rb +97 -0
- data/lib/shopify_api/rest/resources/2023_10/fulfillment_service.rb +130 -0
- data/lib/shopify_api/rest/resources/2023_10/gift_card.rb +218 -0
- data/lib/shopify_api/rest/resources/2023_10/gift_card_adjustment.rb +118 -0
- data/lib/shopify_api/rest/resources/2023_10/image.rb +157 -0
- data/lib/shopify_api/rest/resources/2023_10/inventory_item.rb +108 -0
- data/lib/shopify_api/rest/resources/2023_10/inventory_level.rb +179 -0
- data/lib/shopify_api/rest/resources/2023_10/location.rb +167 -0
- data/lib/shopify_api/rest/resources/2023_10/locations_for_move.rb +56 -0
- data/lib/shopify_api/rest/resources/2023_10/marketing_event.rb +209 -0
- data/lib/shopify_api/rest/resources/2023_10/metafield.rb +344 -0
- data/lib/shopify_api/rest/resources/2023_10/mobile_platform_application.rb +110 -0
- data/lib/shopify_api/rest/resources/2023_10/order.rb +491 -0
- data/lib/shopify_api/rest/resources/2023_10/order_risk.rb +144 -0
- data/lib/shopify_api/rest/resources/2023_10/page.rb +194 -0
- data/lib/shopify_api/rest/resources/2023_10/payment.rb +140 -0
- data/lib/shopify_api/rest/resources/2023_10/payment_gateway.rb +143 -0
- data/lib/shopify_api/rest/resources/2023_10/payment_transaction.rb +107 -0
- data/lib/shopify_api/rest/resources/2023_10/payout.rb +97 -0
- data/lib/shopify_api/rest/resources/2023_10/policy.rb +69 -0
- data/lib/shopify_api/rest/resources/2023_10/price_rule.rb +223 -0
- data/lib/shopify_api/rest/resources/2023_10/product.rb +223 -0
- data/lib/shopify_api/rest/resources/2023_10/product_listing.rb +196 -0
- data/lib/shopify_api/rest/resources/2023_10/product_resource_feedback.rb +88 -0
- data/lib/shopify_api/rest/resources/2023_10/province.rb +132 -0
- data/lib/shopify_api/rest/resources/2023_10/recurring_application_charge.rb +172 -0
- data/lib/shopify_api/rest/resources/2023_10/redirect.rb +139 -0
- data/lib/shopify_api/rest/resources/2023_10/refund.rb +151 -0
- data/lib/shopify_api/rest/resources/2023_10/report.rb +121 -0
- data/lib/shopify_api/rest/resources/2023_10/resource_feedback.rb +73 -0
- data/lib/shopify_api/rest/resources/2023_10/script_tag.rb +155 -0
- data/lib/shopify_api/rest/resources/2023_10/shipping_zone.rb +83 -0
- data/lib/shopify_api/rest/resources/2023_10/shop.rb +218 -0
- data/lib/shopify_api/rest/resources/2023_10/smart_collection.rb +216 -0
- data/lib/shopify_api/rest/resources/2023_10/storefront_access_token.rb +87 -0
- data/lib/shopify_api/rest/resources/2023_10/tender_transaction.rb +93 -0
- data/lib/shopify_api/rest/resources/2023_10/theme.rb +123 -0
- data/lib/shopify_api/rest/resources/2023_10/transaction.rb +184 -0
- data/lib/shopify_api/rest/resources/2023_10/usage_charge.rb +102 -0
- data/lib/shopify_api/rest/resources/2023_10/user.rb +138 -0
- data/lib/shopify_api/rest/resources/2023_10/variant.rb +208 -0
- data/lib/shopify_api/rest/resources/2023_10/webhook.rb +168 -0
- data/lib/shopify_api/version.rb +1 -1
- data/lib/shopify_api/webhooks/registration.rb +19 -4
- data/lib/shopify_api/webhooks/registrations/event_bridge.rb +1 -1
- data/lib/shopify_api/webhooks/registrations/http.rb +1 -1
- data/lib/shopify_api/webhooks/registrations/pub_sub.rb +2 -1
- data/lib/shopify_api/webhooks/registry.rb +25 -5
- data/shopify_api.gemspec +0 -1
- metadata +81 -20
- data/.github/workflows/stale.yml +0 -43
- data/docs/issues.md +0 -39
- 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
|
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
|
-
|
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
|
-
|
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
|
160
|
+
**Note:** _These parameters can still be used in all methods regardless of if they are required._
|
18
161
|
|
19
|
-
|
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
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
231
|
+
#### Perform a `PUT` request
|
232
|
+
```ruby
|
233
|
+
# Create a new client.
|
234
|
+
client = ShopifyAPI::Clients::Rest::Admin.new
|
66
235
|
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
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)
|
data/docs/usage/webhooks.md
CHANGED
@@ -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
|
-
|
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",
|
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-
|
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, "
|
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
|
-
|
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
|
-
|
76
|
-
|
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 #{
|
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
|
-
|
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
|
data/lib/shopify_api/context.rb
CHANGED
@@ -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
|
-
|
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:
|
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(
|
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
|
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
|
-
|
213
|
+
response_names = json_response_body_names
|
214
214
|
|
215
|
-
|
216
|
-
(
|
217
|
-
|
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.
|
351
|
-
|
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
|
57
|
-
|
56
|
+
def json_response_body_names()
|
57
|
+
[
|
58
|
+
"fulfillment_order"
|
59
|
+
]
|
58
60
|
end
|
59
61
|
|
60
62
|
sig do
|
@@ -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
|