Skip to content

Commit 175f68b

Browse files
authored
Extend webhook registration to support metafield_namespaces (#1186)
I've added the ability to specify metafield_namespaces in webhook registration to allow these to be received from apps using this API. This is mostly following the patterns already there for fields. Have updated: - docs, adding in mention of the new parameter as well as ensuring it's referenced if using `fields` as well - Updated the base Registration and child classes to have another keyword argument in the initializer - Extended the tests, there's more complexity now as before it was just a boolean "are fields present" - but happy to adjust as needed. - Changelog for this PR shopify_app will also need adjusting to pass through the `metafield_namespaces` parameter but I can create that PR if there's appetite from this one.
1 parent 81c1aed commit 175f68b

File tree

9 files changed

+152
-15
lines changed

9 files changed

+152
-15
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
77
- [#1183](https://github.com/Shopify/shopify-api-ruby/pull/1189) Added string array support for fields parameter in Webhook::Registry
88
- [1208](https://github.com/Shopify/shopify-api-ruby/pull/1208) Fix CustomerAddress and FulfillmentRequest methods
99
- [1225](https://github.com/Shopify/shopify-api-ruby/pull/1225) Support for 2023_10 API version
10+
- [#1186](https://github.com/Shopify/shopify-api-ruby/pull/1186) Extend webhook registration to support metafield_namespaces
1011

1112
## 13.1.0
1213

docs/usage/webhooks.md

+11
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ registration = ShopifyAPI::Webhooks::Registry.add_registration(
4545
)
4646
```
4747

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+
)
57+
```
58+
4859
**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.
4960

5061
### EventBridge and PubSub Webhooks

lib/shopify_api/webhooks/registration.rb

+19-4
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ class Registration
1919
sig { returns(T.nilable(T::Array[String])) }
2020
attr_reader :fields
2121

22+
sig { returns(T.nilable(T::Array[String])) }
23+
attr_reader :metafield_namespaces
24+
2225
sig do
2326
params(topic: String, path: String, handler: T.nilable(Handler),
24-
fields: T.nilable(T.any(String, T::Array[String]))).void
27+
fields: T.nilable(T.any(String, T::Array[String])),
28+
metafield_namespaces: T.nilable(T::Array[String])).void
2529
end
26-
def initialize(topic:, path:, handler: nil, fields: nil)
30+
def initialize(topic:, path:, handler: nil, fields: nil, metafield_namespaces: nil)
2731
@topic = T.let(topic.gsub("/", "_").upcase, String)
2832
@path = path
2933
@handler = handler
3034
fields_array = fields.is_a?(String) ? fields.split(FIELDS_DELIMITER) : fields
3135
@fields = T.let(fields_array&.map(&:strip)&.compact, T.nilable(T::Array[String]))
36+
@metafield_namespaces = T.let(metafield_namespaces&.map(&:strip)&.compact, T.nilable(T::Array[String]))
3237
end
3338

3439
sig { abstract.returns(String) }
@@ -51,7 +56,7 @@ def build_register_query(webhook_id: nil)
5156
identifier = webhook_id ? "id: \"#{webhook_id}\"" : "topic: #{@topic}"
5257

5358
subscription_args_string = subscription_args.map do |k, v|
54-
"#{k}: #{k == :includeFields ? v : '"' + v + '"'}"
59+
"#{k}: #{[:includeFields, :metafieldNamespaces].include?(k) ? v : %("#{v}")}"
5560
end.join(", ")
5661

5762
<<~QUERY
@@ -62,12 +67,22 @@ def build_register_query(webhook_id: nil)
6267
message
6368
}
6469
webhookSubscription {
65-
id#{@fields.nil? ? "" : "\n includeFields"}
70+
#{subscription_response_attributes.join("\n ")}
6671
}
6772
}
6873
}
6974
QUERY
7075
end
76+
77+
private
78+
79+
sig { returns(T::Array[String]) }
80+
def subscription_response_attributes
81+
attributes = ["id"]
82+
attributes << "includeFields" if @fields
83+
attributes << "metafieldNamespaces" if @metafield_namespaces
84+
attributes
85+
end
7186
end
7287
end
7388
end

lib/shopify_api/webhooks/registrations/event_bridge.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def callback_address
1414

1515
sig { override.returns(T::Hash[Symbol, String]) }
1616
def subscription_args
17-
{ arn: callback_address, includeFields: fields }.compact
17+
{ arn: callback_address, includeFields: fields, metafieldNamespaces: metafield_namespaces }.compact
1818
end
1919

2020
sig { override.params(webhook_id: T.nilable(String)).returns(String) }

lib/shopify_api/webhooks/registrations/http.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def callback_address
2020

2121
sig { override.returns(T::Hash[Symbol, String]) }
2222
def subscription_args
23-
{ callbackUrl: callback_address, includeFields: fields }.compact
23+
{ callbackUrl: callback_address, includeFields: fields, metafieldNamespaces: metafield_namespaces }.compact
2424
end
2525

2626
sig { override.params(webhook_id: T.nilable(String)).returns(String) }

lib/shopify_api/webhooks/registrations/pub_sub.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ def subscription_args
1717
project_topic_pair = callback_address.gsub(%r{^pubsub://}, "").split(":")
1818
project = project_topic_pair[0]
1919
topic = project_topic_pair[1]
20-
{ pubSubProject: project, pubSubTopic: topic, includeFields: fields }.compact
20+
{ pubSubProject: project, pubSubTopic: topic, includeFields: fields,
21+
metafieldNamespaces: metafield_namespaces, }.compact
2122
end
2223

2324
sig { override.params(webhook_id: T.nilable(String)).returns(String) }

lib/shopify_api/webhooks/registry.rb

+9-5
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,24 @@ class << self
1313
delivery_method: Symbol,
1414
path: String,
1515
handler: T.nilable(Handler),
16-
fields: T.nilable(T.any(String, T::Array[String]))).void
16+
fields: T.nilable(T.any(String, T::Array[String])),
17+
metafield_namespaces: T.nilable(T::Array[String])).void
1718
end
18-
def add_registration(topic:, delivery_method:, path:, handler: nil, fields: nil)
19+
def add_registration(topic:, delivery_method:, path:, handler: nil, fields: nil, metafield_namespaces: nil)
1920
@registry[topic] = case delivery_method
2021
when :pub_sub
21-
Registrations::PubSub.new(topic: topic, path: path, fields: fields)
22+
Registrations::PubSub.new(topic: topic, path: path, fields: fields,
23+
metafield_namespaces: metafield_namespaces)
2224
when :event_bridge
23-
Registrations::EventBridge.new(topic: topic, path: path, fields: fields)
25+
Registrations::EventBridge.new(topic: topic, path: path, fields: fields,
26+
metafield_namespaces: metafield_namespaces)
2427
when :http
2528
unless handler
2629
raise Errors::InvalidWebhookRegistrationError, "Cannot create an Http registration without a handler."
2730
end
2831

29-
Registrations::Http.new(topic: topic, path: path, handler: handler, fields: fields)
32+
Registrations::Http.new(topic: topic, path: path, handler: handler,
33+
fields: fields, metafield_namespaces: metafield_namespaces)
3034
else
3135
raise Errors::InvalidWebhookRegistrationError,
3236
"Unsupported delivery method #{delivery_method}. Allowed values: {:http, :pub_sub, :event_bridge}."

test/webhooks/registry_test.rb

+30-3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def test_http_registration_with_fields_array_add_and_update
9898
do_registration_test(:http, "test-webhooks", fields: ["field1", "field2"])
9999
end
100100

101+
def test_http_registration_with_metafield_namespaces_add_and_update
102+
do_registration_test(:http, "test-webhooks", metafield_namespaces: ["namespace1", "namespace2"])
103+
end
104+
101105
def test_raises_on_http_registration_check_error
102106
do_registration_check_error_test(:http, "test-webhooks")
103107
end
@@ -114,6 +118,11 @@ def test_pubsub_registration_with_fields_array_add_and_update
114118
do_registration_test(:pub_sub, "pubsub://my-project-id:my-topic-id", fields: ["field1", "field2"])
115119
end
116120

121+
def test_pubsub_registration_with_metafield_namespaces_add_and_update
122+
do_registration_test(:pub_sub, "pubsub://my-project-id:my-topic-id",
123+
metafield_namespaces: ["namespace1", "namespace2"])
124+
end
125+
117126
def test_raises_on_pubsub_registration_check_error
118127
do_registration_check_error_test(:pub_sub, "pubsub://my-project-id:my-topic-id")
119128
end
@@ -130,6 +139,10 @@ def test_eventbridge_registration_with_fields_array_add_and_update
130139
do_registration_test(:event_bridge, "test-webhooks", fields: ["field1", "field2"])
131140
end
132141

142+
def test_eventbridge_registration_with_metafield_namespaces_add_and_update
143+
do_registration_test(:event_bridge, "test-webhooks", metafield_namespaces: ["namespace1", "namespace2"])
144+
end
145+
133146
def test_raises_on_eventbridge_registration_check_error
134147
do_registration_check_error_test(:event_bridge, "test-webhooks")
135148
end
@@ -251,7 +264,7 @@ def test_get_webhook_id_with_graphql_errors
251264

252265
private
253266

254-
def do_registration_test(delivery_method, path, fields: nil)
267+
def do_registration_test(delivery_method, path, fields: nil, metafield_namespaces: nil)
255268
ShopifyAPI::Webhooks::Registry.clear
256269

257270
check_query_body = { query: queries[delivery_method][:check_query], variables: nil }
@@ -260,8 +273,21 @@ def do_registration_test(delivery_method, path, fields: nil)
260273
.with(body: JSON.dump(check_query_body))
261274
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][:check_empty_response]) })
262275

263-
add_query_type = fields.nil? ? :register_add_query : :register_add_query_with_fields
264-
add_response_type = fields.nil? ? :register_add_response : :register_add_with_fields_response
276+
add_query_type = if fields.present?
277+
:register_add_query_with_fields
278+
elsif metafield_namespaces.present?
279+
:register_add_query_with_metafield_namespaces
280+
else
281+
:register_add_query
282+
end
283+
add_response_type = if fields.present?
284+
:register_add_with_fields_response
285+
elsif metafield_namespaces.present?
286+
:register_add_with_metafield_namespaces_response
287+
else
288+
:register_add_response
289+
end
290+
265291
stub_request(:post, @url)
266292
.with(body: JSON.dump({ query: queries[delivery_method][add_query_type], variables: nil }))
267293
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][add_response_type]) })
@@ -275,6 +301,7 @@ def do_registration_test(delivery_method, path, fields: nil)
275301
end,
276302
),
277303
fields: fields,
304+
metafield_namespaces: metafield_namespaces,
278305
)
279306
registration_response = ShopifyAPI::Webhooks::Registry.register_all(
280307
session: @session,

test/webhooks/webhook_registration_queries.rb

+78
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ def queries
6262
}
6363
}
6464
QUERY
65+
register_add_query_with_metafield_namespaces:
66+
<<~QUERY,
67+
mutation webhookSubscription {
68+
webhookSubscriptionCreate(topic: SOME_TOPIC, webhookSubscription: {callbackUrl: "https://app-address.com/test-webhooks", metafieldNamespaces: ["namespace1", "namespace2"]}) {
69+
userErrors {
70+
field
71+
message
72+
}
73+
webhookSubscription {
74+
id
75+
metafieldNamespaces
76+
}
77+
}
78+
}
79+
QUERY
6580

6681
register_add_response: {
6782
"data" => {
@@ -82,6 +97,17 @@ def queries
8297
},
8398
},
8499
},
100+
register_add_with_metafield_namespaces_response: {
101+
"data" => {
102+
"webhookSubscriptionCreate" => {
103+
"userErrors" => [],
104+
"webhookSubscription" => {
105+
"id" => "gid://shopify/WebhookSubscription/12345",
106+
"metafieldNamespaces" => ["namespace1", "namespace2"],
107+
},
108+
},
109+
},
110+
},
85111
check_existing_response: {
86112
"data" => {
87113
"webhookSubscriptions" => {
@@ -175,6 +201,21 @@ def queries
175201
}
176202
}
177203
QUERY
204+
register_add_query_with_metafield_namespaces:
205+
<<~QUERY,
206+
mutation webhookSubscription {
207+
eventBridgeWebhookSubscriptionCreate(topic: SOME_TOPIC, webhookSubscription: {arn: "test-webhooks", metafieldNamespaces: ["namespace1", "namespace2"]}) {
208+
userErrors {
209+
field
210+
message
211+
}
212+
webhookSubscription {
213+
id
214+
metafieldNamespaces
215+
}
216+
}
217+
}
218+
QUERY
178219
register_add_response: {
179220
"data" => {
180221
"eventBridgeWebhookSubscriptionCreate" => {
@@ -194,6 +235,17 @@ def queries
194235
},
195236
},
196237
},
238+
register_add_with_metafield_namespaces_response: {
239+
"data" => {
240+
"eventBridgeWebhookSubscriptionCreate" => {
241+
"userErrors" => [],
242+
"webhookSubscription" => {
243+
"id" => "gid://shopify/WebhookSubscription/12345",
244+
"metafieldNamespaces" => ["namespace1", "namespace2"],
245+
},
246+
},
247+
},
248+
},
197249
check_existing_response: {
198250
"data" => {
199251
"webhookSubscriptions" => {
@@ -288,6 +340,21 @@ def queries
288340
}
289341
}
290342
QUERY
343+
register_add_query_with_metafield_namespaces:
344+
<<~QUERY,
345+
mutation webhookSubscription {
346+
pubSubWebhookSubscriptionCreate(topic: SOME_TOPIC, webhookSubscription: {pubSubProject: "my-project-id", pubSubTopic: "my-topic-id", metafieldNamespaces: ["namespace1", "namespace2"]}) {
347+
userErrors {
348+
field
349+
message
350+
}
351+
webhookSubscription {
352+
id
353+
metafieldNamespaces
354+
}
355+
}
356+
}
357+
QUERY
291358
register_add_response: {
292359
"data" => {
293360
"pubSubWebhookSubscriptionCreate" => {
@@ -307,6 +374,17 @@ def queries
307374
},
308375
},
309376
},
377+
register_add_with_metafield_namespaces_response: {
378+
"data" => {
379+
"pubSubWebhookSubscriptionCreate" => {
380+
"userErrors" => [],
381+
"webhookSubscription" => {
382+
"id" => "gid://shopify/WebhookSubscription/12345",
383+
"metafieldNamespaces" => ["namespace1", "namespace2"],
384+
},
385+
},
386+
},
387+
},
310388
check_existing_response: {
311389
"data" => {
312390
"webhookSubscriptions" => {

0 commit comments

Comments
 (0)