shopify_api 16.1.0 → 16.3.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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/investigating-github-issues/SKILL.md +171 -0
  3. data/.claude/skills/investigating-github-issues/references/investigation-report-template.md +90 -0
  4. data/.claude/skills/shared/references/version-maintenance-policy.md +20 -0
  5. data/.github/CODEOWNERS +1 -1
  6. data/.github/workflows/gardener-investigate-issue.yml +226 -0
  7. data/.github/workflows/gardener-notify-event.yml +35 -0
  8. data/.github/workflows/gardener-notify-slack.yml +116 -0
  9. data/CHANGELOG.md +11 -0
  10. data/Gemfile.lock +9 -3
  11. data/REST_RESOURCES.md +9 -12
  12. data/docs/usage/oauth.md +4 -5
  13. data/lib/shopify_api/admin_versions.rb +2 -0
  14. data/lib/shopify_api/auth/client_credentials.rb +3 -2
  15. data/lib/shopify_api/auth/refresh_token.rb +3 -2
  16. data/lib/shopify_api/auth/token_exchange.rb +18 -8
  17. data/lib/shopify_api/clients/graphql/storefront.rb +3 -2
  18. data/lib/shopify_api/context.rb +5 -1
  19. data/lib/shopify_api/errors/invalid_shop_error.rb +9 -0
  20. data/lib/shopify_api/errors/rest_resource_not_loaded_error.rb +14 -0
  21. data/lib/shopify_api/rest/resources/2026_04/abandoned_checkout.rb +194 -0
  22. data/lib/shopify_api/rest/resources/2026_04/access_scope.rb +62 -0
  23. data/lib/shopify_api/rest/resources/2026_04/apple_pay_certificate.rb +109 -0
  24. data/lib/shopify_api/rest/resources/2026_04/application_charge.rb +113 -0
  25. data/lib/shopify_api/rest/resources/2026_04/application_credit.rb +95 -0
  26. data/lib/shopify_api/rest/resources/2026_04/article.rb +269 -0
  27. data/lib/shopify_api/rest/resources/2026_04/asset.rb +122 -0
  28. data/lib/shopify_api/rest/resources/2026_04/assigned_fulfillment_order.rb +92 -0
  29. data/lib/shopify_api/rest/resources/2026_04/balance.rb +58 -0
  30. data/lib/shopify_api/rest/resources/2026_04/blog.rb +166 -0
  31. data/lib/shopify_api/rest/resources/2026_04/cancellation_request.rb +87 -0
  32. data/lib/shopify_api/rest/resources/2026_04/carrier_service.rb +120 -0
  33. data/lib/shopify_api/rest/resources/2026_04/checkout.rb +213 -0
  34. data/lib/shopify_api/rest/resources/2026_04/collect.rb +146 -0
  35. data/lib/shopify_api/rest/resources/2026_04/collection.rb +114 -0
  36. data/lib/shopify_api/rest/resources/2026_04/collection_listing.rb +159 -0
  37. data/lib/shopify_api/rest/resources/2026_04/comment.rb +287 -0
  38. data/lib/shopify_api/rest/resources/2026_04/country.rb +141 -0
  39. data/lib/shopify_api/rest/resources/2026_04/currency.rb +61 -0
  40. data/lib/shopify_api/rest/resources/2026_04/custom_collection.rb +191 -0
  41. data/lib/shopify_api/rest/resources/2026_04/customer.rb +328 -0
  42. data/lib/shopify_api/rest/resources/2026_04/deprecated_api_call.rb +61 -0
  43. data/lib/shopify_api/rest/resources/2026_04/discount_code.rb +226 -0
  44. data/lib/shopify_api/rest/resources/2026_04/dispute.rb +115 -0
  45. data/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb +121 -0
  46. data/lib/shopify_api/rest/resources/2026_04/dispute_file_upload.rb +85 -0
  47. data/lib/shopify_api/rest/resources/2026_04/draft_order.rb +279 -0
  48. data/lib/shopify_api/rest/resources/2026_04/event.rb +152 -0
  49. data/lib/shopify_api/rest/resources/2026_04/fulfillment.rb +235 -0
  50. data/lib/shopify_api/rest/resources/2026_04/fulfillment_event.rb +167 -0
  51. data/lib/shopify_api/rest/resources/2026_04/fulfillment_order.rb +326 -0
  52. data/lib/shopify_api/rest/resources/2026_04/fulfillment_request.rb +116 -0
  53. data/lib/shopify_api/rest/resources/2026_04/fulfillment_service.rb +131 -0
  54. data/lib/shopify_api/rest/resources/2026_04/gift_card.rb +222 -0
  55. data/lib/shopify_api/rest/resources/2026_04/gift_card_adjustment.rb +122 -0
  56. data/lib/shopify_api/rest/resources/2026_04/image.rb +161 -0
  57. data/lib/shopify_api/rest/resources/2026_04/inventory_item.rb +112 -0
  58. data/lib/shopify_api/rest/resources/2026_04/inventory_level.rb +183 -0
  59. data/lib/shopify_api/rest/resources/2026_04/location.rb +171 -0
  60. data/lib/shopify_api/rest/resources/2026_04/locations_for_move.rb +60 -0
  61. data/lib/shopify_api/rest/resources/2026_04/marketing_event.rb +213 -0
  62. data/lib/shopify_api/rest/resources/2026_04/metafield.rb +348 -0
  63. data/lib/shopify_api/rest/resources/2026_04/mobile_platform_application.rb +120 -0
  64. data/lib/shopify_api/rest/resources/2026_04/order.rb +503 -0
  65. data/lib/shopify_api/rest/resources/2026_04/order_risk.rb +148 -0
  66. data/lib/shopify_api/rest/resources/2026_04/page.rb +198 -0
  67. data/lib/shopify_api/rest/resources/2026_04/payment.rb +98 -0
  68. data/lib/shopify_api/rest/resources/2026_04/payment_gateway.rb +147 -0
  69. data/lib/shopify_api/rest/resources/2026_04/payment_transaction.rb +117 -0
  70. data/lib/shopify_api/rest/resources/2026_04/payout.rb +101 -0
  71. data/lib/shopify_api/rest/resources/2026_04/policy.rb +73 -0
  72. data/lib/shopify_api/rest/resources/2026_04/price_rule.rb +227 -0
  73. data/lib/shopify_api/rest/resources/2026_04/product.rb +227 -0
  74. data/lib/shopify_api/rest/resources/2026_04/product_listing.rb +200 -0
  75. data/lib/shopify_api/rest/resources/2026_04/product_resource_feedback.rb +92 -0
  76. data/lib/shopify_api/rest/resources/2026_04/province.rb +136 -0
  77. data/lib/shopify_api/rest/resources/2026_04/recurring_application_charge.rb +184 -0
  78. data/lib/shopify_api/rest/resources/2026_04/redirect.rb +143 -0
  79. data/lib/shopify_api/rest/resources/2026_04/refund.rb +158 -0
  80. data/lib/shopify_api/rest/resources/2026_04/resource_feedback.rb +77 -0
  81. data/lib/shopify_api/rest/resources/2026_04/script_tag.rb +159 -0
  82. data/lib/shopify_api/rest/resources/2026_04/shipping_zone.rb +87 -0
  83. data/lib/shopify_api/rest/resources/2026_04/shop.rb +231 -0
  84. data/lib/shopify_api/rest/resources/2026_04/smart_collection.rb +220 -0
  85. data/lib/shopify_api/rest/resources/2026_04/storefront_access_token.rb +91 -0
  86. data/lib/shopify_api/rest/resources/2026_04/tender_transaction.rb +97 -0
  87. data/lib/shopify_api/rest/resources/2026_04/theme.rb +127 -0
  88. data/lib/shopify_api/rest/resources/2026_04/transaction.rb +194 -0
  89. data/lib/shopify_api/rest/resources/2026_04/usage_charge.rb +106 -0
  90. data/lib/shopify_api/rest/resources/2026_04/user.rb +142 -0
  91. data/lib/shopify_api/rest/resources/2026_04/variant.rb +212 -0
  92. data/lib/shopify_api/rest/resources/2026_04/webhook.rb +173 -0
  93. data/lib/shopify_api/rest/resources/2026_07/abandoned_checkout.rb +194 -0
  94. data/lib/shopify_api/rest/resources/2026_07/access_scope.rb +62 -0
  95. data/lib/shopify_api/rest/resources/2026_07/apple_pay_certificate.rb +109 -0
  96. data/lib/shopify_api/rest/resources/2026_07/application_charge.rb +113 -0
  97. data/lib/shopify_api/rest/resources/2026_07/application_credit.rb +95 -0
  98. data/lib/shopify_api/rest/resources/2026_07/article.rb +269 -0
  99. data/lib/shopify_api/rest/resources/2026_07/asset.rb +122 -0
  100. data/lib/shopify_api/rest/resources/2026_07/assigned_fulfillment_order.rb +92 -0
  101. data/lib/shopify_api/rest/resources/2026_07/balance.rb +58 -0
  102. data/lib/shopify_api/rest/resources/2026_07/blog.rb +166 -0
  103. data/lib/shopify_api/rest/resources/2026_07/cancellation_request.rb +87 -0
  104. data/lib/shopify_api/rest/resources/2026_07/carrier_service.rb +120 -0
  105. data/lib/shopify_api/rest/resources/2026_07/checkout.rb +213 -0
  106. data/lib/shopify_api/rest/resources/2026_07/collect.rb +146 -0
  107. data/lib/shopify_api/rest/resources/2026_07/collection.rb +114 -0
  108. data/lib/shopify_api/rest/resources/2026_07/collection_listing.rb +159 -0
  109. data/lib/shopify_api/rest/resources/2026_07/comment.rb +287 -0
  110. data/lib/shopify_api/rest/resources/2026_07/country.rb +141 -0
  111. data/lib/shopify_api/rest/resources/2026_07/currency.rb +61 -0
  112. data/lib/shopify_api/rest/resources/2026_07/custom_collection.rb +191 -0
  113. data/lib/shopify_api/rest/resources/2026_07/customer.rb +328 -0
  114. data/lib/shopify_api/rest/resources/2026_07/deprecated_api_call.rb +61 -0
  115. data/lib/shopify_api/rest/resources/2026_07/discount_code.rb +226 -0
  116. data/lib/shopify_api/rest/resources/2026_07/dispute.rb +115 -0
  117. data/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb +121 -0
  118. data/lib/shopify_api/rest/resources/2026_07/dispute_file_upload.rb +85 -0
  119. data/lib/shopify_api/rest/resources/2026_07/draft_order.rb +279 -0
  120. data/lib/shopify_api/rest/resources/2026_07/event.rb +152 -0
  121. data/lib/shopify_api/rest/resources/2026_07/fulfillment.rb +235 -0
  122. data/lib/shopify_api/rest/resources/2026_07/fulfillment_event.rb +167 -0
  123. data/lib/shopify_api/rest/resources/2026_07/fulfillment_order.rb +326 -0
  124. data/lib/shopify_api/rest/resources/2026_07/fulfillment_request.rb +116 -0
  125. data/lib/shopify_api/rest/resources/2026_07/fulfillment_service.rb +131 -0
  126. data/lib/shopify_api/rest/resources/2026_07/gift_card.rb +222 -0
  127. data/lib/shopify_api/rest/resources/2026_07/gift_card_adjustment.rb +122 -0
  128. data/lib/shopify_api/rest/resources/2026_07/image.rb +161 -0
  129. data/lib/shopify_api/rest/resources/2026_07/inventory_item.rb +112 -0
  130. data/lib/shopify_api/rest/resources/2026_07/inventory_level.rb +183 -0
  131. data/lib/shopify_api/rest/resources/2026_07/location.rb +171 -0
  132. data/lib/shopify_api/rest/resources/2026_07/locations_for_move.rb +60 -0
  133. data/lib/shopify_api/rest/resources/2026_07/marketing_event.rb +213 -0
  134. data/lib/shopify_api/rest/resources/2026_07/metafield.rb +348 -0
  135. data/lib/shopify_api/rest/resources/2026_07/mobile_platform_application.rb +120 -0
  136. data/lib/shopify_api/rest/resources/2026_07/order.rb +503 -0
  137. data/lib/shopify_api/rest/resources/2026_07/order_risk.rb +148 -0
  138. data/lib/shopify_api/rest/resources/2026_07/page.rb +198 -0
  139. data/lib/shopify_api/rest/resources/2026_07/payment.rb +98 -0
  140. data/lib/shopify_api/rest/resources/2026_07/payment_gateway.rb +147 -0
  141. data/lib/shopify_api/rest/resources/2026_07/payment_transaction.rb +117 -0
  142. data/lib/shopify_api/rest/resources/2026_07/payout.rb +101 -0
  143. data/lib/shopify_api/rest/resources/2026_07/policy.rb +73 -0
  144. data/lib/shopify_api/rest/resources/2026_07/price_rule.rb +227 -0
  145. data/lib/shopify_api/rest/resources/2026_07/product.rb +227 -0
  146. data/lib/shopify_api/rest/resources/2026_07/product_listing.rb +200 -0
  147. data/lib/shopify_api/rest/resources/2026_07/product_resource_feedback.rb +92 -0
  148. data/lib/shopify_api/rest/resources/2026_07/province.rb +136 -0
  149. data/lib/shopify_api/rest/resources/2026_07/recurring_application_charge.rb +184 -0
  150. data/lib/shopify_api/rest/resources/2026_07/redirect.rb +143 -0
  151. data/lib/shopify_api/rest/resources/2026_07/refund.rb +158 -0
  152. data/lib/shopify_api/rest/resources/2026_07/resource_feedback.rb +77 -0
  153. data/lib/shopify_api/rest/resources/2026_07/script_tag.rb +159 -0
  154. data/lib/shopify_api/rest/resources/2026_07/shipping_zone.rb +87 -0
  155. data/lib/shopify_api/rest/resources/2026_07/shop.rb +231 -0
  156. data/lib/shopify_api/rest/resources/2026_07/smart_collection.rb +220 -0
  157. data/lib/shopify_api/rest/resources/2026_07/storefront_access_token.rb +91 -0
  158. data/lib/shopify_api/rest/resources/2026_07/tender_transaction.rb +97 -0
  159. data/lib/shopify_api/rest/resources/2026_07/theme.rb +127 -0
  160. data/lib/shopify_api/rest/resources/2026_07/transaction.rb +194 -0
  161. data/lib/shopify_api/rest/resources/2026_07/usage_charge.rb +106 -0
  162. data/lib/shopify_api/rest/resources/2026_07/user.rb +142 -0
  163. data/lib/shopify_api/rest/resources/2026_07/variant.rb +212 -0
  164. data/lib/shopify_api/rest/resources/2026_07/webhook.rb +173 -0
  165. data/lib/shopify_api/utils/shop_validator.rb +118 -0
  166. data/lib/shopify_api/version.rb +1 -1
  167. data/lib/shopify_api/webhooks/request.rb +17 -10
  168. data/lib/shopify_api.rb +69 -0
  169. data/shopify_api.gemspec +2 -0
  170. metadata +183 -2
@@ -0,0 +1,212 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ ########################################################################################################################
5
+ # This file is auto-generated. If you have an issue, please create a GitHub issue. #
6
+ ########################################################################################################################
7
+
8
+ module ShopifyAPI
9
+ class Variant < ShopifyAPI::Rest::Base
10
+ extend T::Sig
11
+
12
+ @prev_page_info = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
13
+ @next_page_info = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
14
+
15
+ @api_call_limit = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
16
+ @retry_request_after = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
17
+
18
+ sig { params(session: T.nilable(ShopifyAPI::Auth::Session), from_hash: T.nilable(T::Hash[T.untyped, T.untyped])).void }
19
+ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
20
+
21
+ @barcode = T.let(nil, T.nilable(String))
22
+ @compare_at_price = T.let(nil, T.nilable(String))
23
+ @created_at = T.let(nil, T.nilable(String))
24
+ @fulfillment_service = T.let(nil, T.nilable(String))
25
+ @grams = T.let(nil, T.nilable(Integer))
26
+ @id = T.let(nil, T.nilable(Integer))
27
+ @image_id = T.let(nil, T.nilable(Integer))
28
+ @inventory_item_id = T.let(nil, T.nilable(Integer))
29
+ @inventory_management = T.let(nil, T.nilable(String))
30
+ @inventory_policy = T.let(nil, T.nilable(String))
31
+ @inventory_quantity = T.let(nil, T.nilable(T.any(Integer, String)))
32
+ @old_inventory_quantity = T.let(nil, T.nilable(T.any(Integer, String)))
33
+ @option = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
34
+ @position = T.let(nil, T.nilable(Integer))
35
+ @presentment_prices = T.let(nil, T.nilable(T::Array[T.untyped]))
36
+ @price = T.let(nil, T.nilable(String))
37
+ @product_id = T.let(nil, T.nilable(Integer))
38
+ @requires_shipping = T.let(nil, T.nilable(T::Boolean))
39
+ @sku = T.let(nil, T.nilable(String))
40
+ @tax_code = T.let(nil, T.nilable(String))
41
+ @taxable = T.let(nil, T.nilable(T::Boolean))
42
+ @title = T.let(nil, T.nilable(String))
43
+ @updated_at = T.let(nil, T.nilable(String))
44
+ @weight = T.let(nil, T.nilable(Float))
45
+ @weight_unit = T.let(nil, T.nilable(String))
46
+
47
+ super(session: session, from_hash: from_hash)
48
+ end
49
+
50
+ @has_one = T.let({}, T::Hash[Symbol, Class])
51
+ @has_many = T.let({}, T::Hash[Symbol, Class])
52
+ @paths = T.let([
53
+ {http_method: :delete, operation: :delete, ids: [:product_id, :id], path: "products/<product_id>/variants/<id>.json"},
54
+ {http_method: :get, operation: :count, ids: [:product_id], path: "products/<product_id>/variants/count.json"},
55
+ {http_method: :get, operation: :get, ids: [:product_id], path: "products/<product_id>/variants.json"},
56
+ {http_method: :get, operation: :get, ids: [:id], path: "variants/<id>.json"},
57
+ {http_method: :post, operation: :post, ids: [:product_id], path: "products/<product_id>/variants.json"},
58
+ {http_method: :put, operation: :put, ids: [:id], path: "variants/<id>.json"}
59
+ ], T::Array[T::Hash[String, T.any(T::Array[Symbol], String, Symbol)]])
60
+ @read_only_attributes = T.let([
61
+ :inventory_quantity
62
+ ], T::Array[Symbol])
63
+
64
+ sig { returns(T.nilable(String)) }
65
+ attr_reader :barcode
66
+ sig { returns(T.nilable(String)) }
67
+ attr_reader :compare_at_price
68
+ sig { returns(T.nilable(String)) }
69
+ attr_reader :created_at
70
+ sig { returns(T.nilable(String)) }
71
+ attr_reader :fulfillment_service
72
+ sig { returns(T.nilable(Integer)) }
73
+ attr_reader :grams
74
+ sig { returns(T.nilable(Integer)) }
75
+ attr_reader :id
76
+ sig { returns(T.nilable(Integer)) }
77
+ attr_reader :image_id
78
+ sig { returns(T.nilable(Integer)) }
79
+ attr_reader :inventory_item_id
80
+ sig { returns(T.nilable(String)) }
81
+ attr_reader :inventory_management
82
+ sig { returns(T.nilable(String)) }
83
+ attr_reader :inventory_policy
84
+ sig { returns(T.nilable(T.any(Integer, String))) }
85
+ attr_reader :inventory_quantity
86
+ sig { returns(T.nilable(T.any(Integer, String))) }
87
+ attr_reader :old_inventory_quantity
88
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
89
+ attr_reader :option
90
+ sig { returns(T.nilable(Integer)) }
91
+ attr_reader :position
92
+ sig { returns(T.nilable(T::Array[T::Hash[T.untyped, T.untyped]])) }
93
+ attr_reader :presentment_prices
94
+ sig { returns(T.nilable(String)) }
95
+ attr_reader :price
96
+ sig { returns(T.nilable(Integer)) }
97
+ attr_reader :product_id
98
+ sig { returns(T.nilable(T::Boolean)) }
99
+ attr_reader :requires_shipping
100
+ sig { returns(T.nilable(String)) }
101
+ attr_reader :sku
102
+ sig { returns(T.nilable(String)) }
103
+ attr_reader :tax_code
104
+ sig { returns(T.nilable(T::Boolean)) }
105
+ attr_reader :taxable
106
+ sig { returns(T.nilable(String)) }
107
+ attr_reader :title
108
+ sig { returns(T.nilable(String)) }
109
+ attr_reader :updated_at
110
+ sig { returns(T.nilable(Float)) }
111
+ attr_reader :weight
112
+ sig { returns(T.nilable(String)) }
113
+ attr_reader :weight_unit
114
+
115
+ class << self
116
+ sig do
117
+ params(
118
+ id: T.any(Integer, String),
119
+ fields: T.untyped,
120
+ session: Auth::Session
121
+ ).returns(T.nilable(Variant))
122
+ end
123
+ def find(
124
+ id:,
125
+ fields: nil,
126
+ session: ShopifyAPI::Context.active_session
127
+ )
128
+ result = base_find(
129
+ session: session,
130
+ ids: {id: id},
131
+ params: {fields: fields},
132
+ )
133
+ T.cast(result[0], T.nilable(Variant))
134
+ end
135
+
136
+ sig do
137
+ params(
138
+ id: T.any(Integer, String),
139
+ product_id: T.nilable(T.any(Integer, String)),
140
+ session: Auth::Session
141
+ ).returns(T.untyped)
142
+ end
143
+ def delete(
144
+ id:,
145
+ product_id: nil,
146
+ session: ShopifyAPI::Context.active_session
147
+ )
148
+ request(
149
+ http_method: :delete,
150
+ operation: :delete,
151
+ session: session,
152
+ ids: {id: id, product_id: product_id},
153
+ params: {},
154
+ )
155
+ end
156
+
157
+ sig do
158
+ params(
159
+ product_id: T.nilable(T.any(Integer, String)),
160
+ limit: T.untyped,
161
+ presentment_currencies: T.untyped,
162
+ since_id: T.untyped,
163
+ fields: T.untyped,
164
+ session: Auth::Session,
165
+ kwargs: T.untyped
166
+ ).returns(T::Array[Variant])
167
+ end
168
+ def all(
169
+ product_id: nil,
170
+ limit: nil,
171
+ presentment_currencies: nil,
172
+ since_id: nil,
173
+ fields: nil,
174
+ session: ShopifyAPI::Context.active_session,
175
+ **kwargs
176
+ )
177
+ response = base_find(
178
+ session: session,
179
+ ids: {product_id: product_id},
180
+ params: {limit: limit, presentment_currencies: presentment_currencies, since_id: since_id, fields: fields}.merge(kwargs).compact,
181
+ )
182
+
183
+ T.cast(response, T::Array[Variant])
184
+ end
185
+
186
+ sig do
187
+ params(
188
+ product_id: T.nilable(T.any(Integer, String)),
189
+ session: Auth::Session,
190
+ kwargs: T.untyped
191
+ ).returns(T.untyped)
192
+ end
193
+ def count(
194
+ product_id: nil,
195
+ session: ShopifyAPI::Context.active_session,
196
+ **kwargs
197
+ )
198
+ request(
199
+ http_method: :get,
200
+ operation: :count,
201
+ session: session,
202
+ ids: {product_id: product_id},
203
+ params: {}.merge(kwargs).compact,
204
+ body: {},
205
+ entity: nil,
206
+ )
207
+ end
208
+
209
+ end
210
+
211
+ end
212
+ end
@@ -0,0 +1,173 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ ########################################################################################################################
5
+ # This file is auto-generated. If you have an issue, please create a GitHub issue. #
6
+ ########################################################################################################################
7
+
8
+ module ShopifyAPI
9
+ class Webhook < ShopifyAPI::Rest::Base
10
+ extend T::Sig
11
+
12
+ @prev_page_info = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
13
+ @next_page_info = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
14
+
15
+ @api_call_limit = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
16
+ @retry_request_after = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
17
+
18
+ sig { params(session: T.nilable(ShopifyAPI::Auth::Session), from_hash: T.nilable(T::Hash[T.untyped, T.untyped])).void }
19
+ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
20
+
21
+ @address = T.let(nil, T.nilable(String))
22
+ @topic = T.let(nil, T.nilable(String))
23
+ @api_version = T.let(nil, T.nilable(String))
24
+ @created_at = T.let(nil, T.nilable(String))
25
+ @fields = T.let(nil, T.nilable(T::Array[T.untyped]))
26
+ @filter = T.let(nil, T.nilable(String))
27
+ @format = T.let(nil, T.nilable(String))
28
+ @id = T.let(nil, T.nilable(Integer))
29
+ @metafield_namespaces = T.let(nil, T.nilable(T::Array[T.untyped]))
30
+ @private_metafield_namespaces = T.let(nil, T.nilable(T::Array[T.untyped]))
31
+ @updated_at = T.let(nil, T.nilable(String))
32
+
33
+ super(session: session, from_hash: from_hash)
34
+ end
35
+
36
+ @has_one = T.let({}, T::Hash[Symbol, Class])
37
+ @has_many = T.let({}, T::Hash[Symbol, Class])
38
+ @paths = T.let([
39
+ {http_method: :delete, operation: :delete, ids: [:id], path: "webhooks/<id>.json"},
40
+ {http_method: :get, operation: :count, ids: [], path: "webhooks/count.json"},
41
+ {http_method: :get, operation: :get, ids: [], path: "webhooks.json"},
42
+ {http_method: :get, operation: :get, ids: [:id], path: "webhooks/<id>.json"},
43
+ {http_method: :post, operation: :post, ids: [], path: "webhooks.json"},
44
+ {http_method: :put, operation: :put, ids: [:id], path: "webhooks/<id>.json"}
45
+ ], T::Array[T::Hash[String, T.any(T::Array[Symbol], String, Symbol)]])
46
+
47
+ sig { returns(T.nilable(String)) }
48
+ attr_reader :address
49
+ sig { returns(T.nilable(String)) }
50
+ attr_reader :topic
51
+ sig { returns(T.nilable(String)) }
52
+ attr_reader :api_version
53
+ sig { returns(T.nilable(String)) }
54
+ attr_reader :created_at
55
+ sig { returns(T.nilable(T::Array[String])) }
56
+ attr_reader :fields
57
+ sig { returns(T.nilable(String)) }
58
+ attr_reader :format
59
+ sig { returns(T.nilable(Integer)) }
60
+ attr_reader :id
61
+ sig { returns(T.nilable(T::Array[String])) }
62
+ attr_reader :metafield_namespaces
63
+ sig { returns(T.nilable(T::Array[String])) }
64
+ attr_reader :private_metafield_namespaces
65
+ sig { returns(T.nilable(String)) }
66
+ attr_reader :updated_at
67
+
68
+ class << self
69
+ sig do
70
+ params(
71
+ id: T.any(Integer, String),
72
+ fields: T.untyped,
73
+ session: Auth::Session
74
+ ).returns(T.nilable(Webhook))
75
+ end
76
+ def find(
77
+ id:,
78
+ fields: nil,
79
+ session: ShopifyAPI::Context.active_session
80
+ )
81
+ result = base_find(
82
+ session: session,
83
+ ids: {id: id},
84
+ params: {fields: fields},
85
+ )
86
+ T.cast(result[0], T.nilable(Webhook))
87
+ end
88
+
89
+ sig do
90
+ params(
91
+ id: T.any(Integer, String),
92
+ session: Auth::Session
93
+ ).returns(T.untyped)
94
+ end
95
+ def delete(
96
+ id:,
97
+ session: ShopifyAPI::Context.active_session
98
+ )
99
+ request(
100
+ http_method: :delete,
101
+ operation: :delete,
102
+ session: session,
103
+ ids: {id: id},
104
+ params: {},
105
+ )
106
+ end
107
+
108
+ sig do
109
+ params(
110
+ address: T.untyped,
111
+ created_at_max: T.untyped,
112
+ created_at_min: T.untyped,
113
+ fields: T.untyped,
114
+ limit: T.untyped,
115
+ since_id: T.untyped,
116
+ topic: T.untyped,
117
+ updated_at_min: T.untyped,
118
+ updated_at_max: T.untyped,
119
+ session: Auth::Session,
120
+ kwargs: T.untyped
121
+ ).returns(T::Array[Webhook])
122
+ end
123
+ def all(
124
+ address: nil,
125
+ created_at_max: nil,
126
+ created_at_min: nil,
127
+ fields: nil,
128
+ limit: nil,
129
+ since_id: nil,
130
+ topic: nil,
131
+ updated_at_min: nil,
132
+ updated_at_max: nil,
133
+ session: ShopifyAPI::Context.active_session,
134
+ **kwargs
135
+ )
136
+ response = base_find(
137
+ session: session,
138
+ ids: {},
139
+ params: {address: address, created_at_max: created_at_max, created_at_min: created_at_min, fields: fields, limit: limit, since_id: since_id, topic: topic, updated_at_min: updated_at_min, updated_at_max: updated_at_max}.merge(kwargs).compact,
140
+ )
141
+
142
+ T.cast(response, T::Array[Webhook])
143
+ end
144
+
145
+ sig do
146
+ params(
147
+ address: T.untyped,
148
+ topic: T.untyped,
149
+ session: Auth::Session,
150
+ kwargs: T.untyped
151
+ ).returns(T.untyped)
152
+ end
153
+ def count(
154
+ address: nil,
155
+ topic: nil,
156
+ session: ShopifyAPI::Context.active_session,
157
+ **kwargs
158
+ )
159
+ request(
160
+ http_method: :get,
161
+ operation: :count,
162
+ session: session,
163
+ ids: {},
164
+ params: {address: address, topic: topic}.merge(kwargs).compact,
165
+ body: {},
166
+ entity: nil,
167
+ )
168
+ end
169
+
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,118 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "addressable/uri"
5
+
6
+ module ShopifyAPI
7
+ module Utils
8
+ module ShopValidator
9
+ TRUSTED_SHOPIFY_DOMAINS = T.let(
10
+ [
11
+ "shopify.com",
12
+ "myshopify.io",
13
+ "myshopify.com",
14
+ "spin.dev",
15
+ "shop.dev",
16
+ ].freeze,
17
+ T::Array[String],
18
+ )
19
+
20
+ class << self
21
+ extend T::Sig
22
+
23
+ sig do
24
+ params(
25
+ shop_domain: String,
26
+ myshopify_domain: T.nilable(String),
27
+ ).returns(T.nilable(String))
28
+ end
29
+ def sanitize_shop_domain(shop_domain, myshopify_domain: nil)
30
+ uri = uri_from_shop_domain(shop_domain, myshopify_domain)
31
+ return nil if uri.nil? || uri.host.nil? || uri.host.empty?
32
+
33
+ trusted_domains(myshopify_domain).each do |trusted_domain|
34
+ host = T.cast(uri.host, String)
35
+ uri_domain = uri.domain
36
+ next if uri_domain.nil?
37
+
38
+ no_shop_name_in_subdomain = host == trusted_domain
39
+ from_trusted_domain = trusted_domain == uri_domain
40
+
41
+ if unified_admin?(uri) && from_trusted_domain
42
+ return myshopify_domain_from_unified_admin(uri)
43
+ end
44
+ return nil if no_shop_name_in_subdomain || host.empty?
45
+ return host if from_trusted_domain
46
+ end
47
+ nil
48
+ end
49
+
50
+ sig do
51
+ params(
52
+ shop: String,
53
+ myshopify_domain: T.nilable(String),
54
+ ).returns(String)
55
+ end
56
+ def sanitize!(shop, myshopify_domain: nil)
57
+ host = sanitize_shop_domain(shop, myshopify_domain: myshopify_domain)
58
+ if host.nil? || host.empty?
59
+ raise Errors::InvalidShopError,
60
+ "shop must be a trusted Shopify domain (see ShopValidator::TRUSTED_SHOPIFY_DOMAINS), got: #{shop.inspect}"
61
+ end
62
+
63
+ host
64
+ end
65
+
66
+ private
67
+
68
+ sig { params(myshopify_domain: T.nilable(String)).returns(T::Array[String]) }
69
+ def trusted_domains(myshopify_domain)
70
+ trusted = TRUSTED_SHOPIFY_DOMAINS.dup
71
+ if myshopify_domain && !myshopify_domain.to_s.empty?
72
+ trusted << myshopify_domain
73
+ trusted.uniq!
74
+ end
75
+ trusted
76
+ end
77
+
78
+ sig do
79
+ params(
80
+ shop_domain: String,
81
+ myshopify_domain: T.nilable(String),
82
+ ).returns(T.nilable(Addressable::URI))
83
+ end
84
+ def uri_from_shop_domain(shop_domain, myshopify_domain)
85
+ name = shop_domain.to_s.downcase.strip
86
+ return nil if name.empty?
87
+ return nil if name.include?("@")
88
+
89
+ if myshopify_domain && !myshopify_domain.to_s.empty? &&
90
+ !name.include?(myshopify_domain.to_s) && !name.include?(".")
91
+ name += ".#{myshopify_domain}"
92
+ end
93
+
94
+ uri = Addressable::URI.parse(name)
95
+ if uri.scheme.nil?
96
+ name = "https://#{name}"
97
+ uri = Addressable::URI.parse(name)
98
+ end
99
+
100
+ uri
101
+ rescue Addressable::URI::InvalidURIError
102
+ nil
103
+ end
104
+
105
+ sig { params(uri: Addressable::URI).returns(T::Boolean) }
106
+ def unified_admin?(uri)
107
+ T.cast(uri.host, String).split(".").first == "admin"
108
+ end
109
+
110
+ sig { params(uri: Addressable::URI).returns(String) }
111
+ def myshopify_domain_from_unified_admin(uri)
112
+ shop = uri.path.to_s.split("/").last
113
+ "#{shop}.myshopify.com"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ShopifyAPI
5
- VERSION = "16.1.0"
5
+ VERSION = "16.3.0"
6
6
  end
@@ -9,27 +9,27 @@ module ShopifyAPI
9
9
 
10
10
  sig { override.returns(String) }
11
11
  def hmac
12
- Digest.hexencode(Base64.decode64(T.cast(@headers["x-shopify-hmac-sha256"], String)))
12
+ Digest.hexencode(Base64.decode64(T.cast(shopify_header("hmac-sha256"), String)))
13
13
  end
14
14
 
15
15
  sig { returns(String) }
16
16
  def topic
17
- T.cast(@headers["x-shopify-topic"], String)
17
+ T.cast(shopify_header("topic"), String)
18
18
  end
19
19
 
20
20
  sig { returns(String) }
21
21
  def shop
22
- T.cast(@headers["x-shopify-shop-domain"], String)
22
+ T.cast(shopify_header("shop-domain"), String)
23
23
  end
24
24
 
25
25
  sig { returns(String) }
26
26
  def api_version
27
- T.cast(@headers["x-shopify-api-version"], String)
27
+ T.cast(shopify_header("api-version"), String)
28
28
  end
29
29
 
30
30
  sig { returns(String) }
31
31
  def webhook_id
32
- T.cast(@headers["x-shopify-webhook-id"], String)
32
+ T.cast(shopify_header("webhook-id"), String)
33
33
  end
34
34
 
35
35
  sig { override.returns(String) }
@@ -48,11 +48,11 @@ module ShopifyAPI
48
48
  headers = headers.to_h { |k, v| [k.to_s.downcase.sub("http_", "").gsub("_", "-"), v] }
49
49
 
50
50
  missing_headers = []
51
- [
52
- "x-shopify-topic",
53
- "x-shopify-hmac-sha256",
54
- "x-shopify-shop-domain",
55
- ].each { |header| missing_headers << header unless headers.include?(header) }
51
+ ["topic", "hmac-sha256", "shop-domain"].each do |name|
52
+ unless headers.key?("shopify-#{name}") || headers.key?("x-shopify-#{name}")
53
+ missing_headers << "shopify-#{name} or x-shopify-#{name}"
54
+ end
55
+ end
56
56
  unless missing_headers.empty?
57
57
  raise Errors::InvalidWebhookError,
58
58
  "Missing one or more of the required HTTP headers to process webhooks: #{missing_headers}"
@@ -61,6 +61,13 @@ module ShopifyAPI
61
61
  @headers = headers
62
62
  @raw_body = raw_body
63
63
  end
64
+
65
+ private
66
+
67
+ sig { params(name: String).returns(T.untyped) }
68
+ def shopify_header(name)
69
+ @headers["shopify-#{name}"] || @headers["x-shopify-#{name}"]
70
+ end
64
71
  end
65
72
  end
66
73
  end
data/lib/shopify_api.rb CHANGED
@@ -8,6 +8,7 @@ require "sorbet-runtime"
8
8
  require "securerandom"
9
9
  require "cgi"
10
10
  require "uri"
11
+ require "logger"
11
12
  require "openssl"
12
13
  require "httparty"
13
14
  require "zeitwerk"
@@ -22,3 +23,71 @@ loader = Zeitwerk::Loader.for_gem
22
23
  loader.inflector = ShopifyAPI::Inflector.new(__FILE__)
23
24
  loader.ignore("#{__dir__}/shopify_api/rest/resources")
24
25
  loader.setup
26
+
27
+ module ShopifyAPI
28
+ REST_RESOURCES_PATH = T.let("#{__dir__}/shopify_api/rest/resources", String)
29
+
30
+ class << self
31
+ extend T::Sig
32
+
33
+ # REST resources are only autoloaded for API versions this gem bundles (see
34
+ # Context.load_rest_resources). Without this hook, using a version whose
35
+ # resources aren't bundled - a newly released version, or `unstable` - fails
36
+ # with a bare "uninitialized constant", which reads like a gem bug instead
37
+ # of a version-coverage gap. GraphQL and the REST client are unaffected.
38
+ sig { params(name: Symbol).returns(T.untyped) }
39
+ def const_missing(name)
40
+ if bundled_rest_resource_names.include?(name.to_s)
41
+ # Pass `name` through so NameError#name still returns the missing
42
+ # constant, as it does for the NameError Ruby would have raised.
43
+ raise Errors::RestResourceNotLoadedError.new(rest_resource_not_loaded_message(name), name)
44
+ end
45
+
46
+ super
47
+ end
48
+
49
+ private
50
+
51
+ # Constant names of every REST resource bundled for any API version, derived
52
+ # the same way Zeitwerk derives them so the two cannot drift.
53
+ sig { returns(T::Array[String]) }
54
+ def bundled_rest_resource_names
55
+ @bundled_rest_resource_names ||= T.let(
56
+ Dir.glob("#{REST_RESOURCES_PATH}/*/*.rb").map do |path|
57
+ Zeitwerk::Inflector.new.camelize(File.basename(path, ".rb"), path)
58
+ end.uniq,
59
+ T.nilable(T::Array[String]),
60
+ )
61
+ end
62
+
63
+ sig { returns(T::Array[String]) }
64
+ def bundled_rest_resource_versions
65
+ @bundled_rest_resource_versions ||= T.let(
66
+ Dir.glob("#{REST_RESOURCES_PATH}/*")
67
+ .select { |path| File.directory?(path) }
68
+ .map { |path| File.basename(path).tr("_", "-") }
69
+ .sort,
70
+ T.nilable(T::Array[String]),
71
+ )
72
+ end
73
+
74
+ sig { params(name: Symbol).returns(String) }
75
+ def rest_resource_not_loaded_message(name)
76
+ api_version = Context.api_version
77
+
78
+ reason = if api_version.empty?
79
+ "ShopifyAPI::Context has not been set up, so no REST resources have been loaded. " \
80
+ "Call ShopifyAPI::Context.setup before using REST resources."
81
+ elsif api_version == "unstable"
82
+ "REST resources are not bundled for the \"unstable\" API version. Set api_version to a " \
83
+ "stable version to use REST resources, or use the GraphQL Admin API."
84
+ else
85
+ "shopify_api #{VERSION} does not bundle REST resources for API version \"#{api_version}\" " \
86
+ "(latest bundled: \"#{bundled_rest_resource_versions.last}\"). Upgrade the gem, set " \
87
+ "api_version to a bundled version, or use the GraphQL Admin API."
88
+ end
89
+
90
+ "uninitialized constant ShopifyAPI::#{name}. #{reason}"
91
+ end
92
+ end
93
+ end
data/shopify_api.gemspec CHANGED
@@ -33,10 +33,12 @@ Gem::Specification.new do |s|
33
33
  s.required_ruby_version = ">= 3.2"
34
34
 
35
35
  s.add_runtime_dependency("activesupport")
36
+ s.add_runtime_dependency("addressable", "~> 2.7")
36
37
  s.add_runtime_dependency("concurrent-ruby")
37
38
  s.add_runtime_dependency("hash_diff")
38
39
  s.add_runtime_dependency("httparty")
39
40
  s.add_runtime_dependency("jwt")
41
+ s.add_runtime_dependency("logger")
40
42
  s.add_runtime_dependency("oj")
41
43
  s.add_runtime_dependency("openssl")
42
44
  s.add_runtime_dependency("securerandom")