Skip to content

Commit b0d58a3

Browse files
committed
Extend webhook registration to support filters
With this change, we can register webhooks specifying the filter parameter that allows to define some conditions on the topic fields to reduce the number of webhooks received. Other than adding this feature for already possible registrations, this extends the number of webhooks that can be registered with this library. In fact, there are some webhook registrations that require the filter parameter to be passed to allow the registrationi (this is partly an assumption because I couldn't find proper documentation around this topic). An example is METAOBJECTS_CREATE, which, without the filter param set, ``` mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) { webhookSubscription { topic filter } userErrors { field message } } } { "topic": "METAOBJECTS_CREATE", "webhookSubscription": { "callbackUrl": "https://test5.com" } } ``` gives the following error: ``` { "data": { "webhookSubscriptionCreate": { "webhookSubscription": null, "userErrors": [ { "field": [ "webhookSubscription" ], "message": "The specified filter is invalid, please ensure you specify the field(s) you wish to filter on." } ] } } } ``` While with the filter on, works as expected. --- This change has been made available to 2024-07 and 2024-10, which are the versions with the filter parameter available, according to the GraphQL documentation. --- Refs: - https://shopify.dev/docs/apps/build/webhooks/customize/filters - https://shopify.dev/docs/api/admin-graphql/2024-07/input-objects/WebhookSubscriptionInput - https://shopify.dev/docs/api/admin-graphql/2024-10/input-objects/WebhookSubscriptionInput
1 parent 451f983 commit b0d58a3

File tree

11 files changed

+249
-18
lines changed

11 files changed

+249
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Note: For changes to the API, see https://shopify.dev/changelog?filter=api
44
## Unreleased
55

6+
- [#1347](https://github.com/Shopify/shopify-api-ruby/pull/1347) Extend webhook registration to support filters
67
- [#1344](https://github.com/Shopify/shopify-api-ruby/pull/1344) Allow ShopifyAPI::Webhooks::Registry to update a webhook when fields or metafield_namespaces are changed.
78
- [#1343](https://github.com/Shopify/shopify-api-ruby/pull/1343) Make ShopifyAPI::Context::scope parameter optional. `scope` defaults to empty list `[]`.
89

docs/usage/webhooks.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ registration = ShopifyAPI::Webhooks::Registry.add_registration(
8282
)
8383
```
8484

85+
If you need to filter the webhooks you want to receive, you can use a [webhooks filter](https://shopify.dev/docs/apps/build/webhooks/customize/filters), which can be specified on registration through the `filter` parameter.
86+
87+
```ruby
88+
registration = ShopifyAPI::Webhooks::Registry.add_registration(
89+
topic: "products/update",
90+
delivery_method: :http,
91+
handler: WebhookHandler,
92+
filter: "variants.price:>=10.00"
93+
)
94+
```
95+
8596
**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.
8697

8798
### EventBridge and PubSub Webhooks

lib/shopify_api/rest/resources/2024_07/webhook.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
2323
@api_version = T.let(nil, T.nilable(String))
2424
@created_at = T.let(nil, T.nilable(String))
2525
@fields = T.let(nil, T.nilable(T::Array[T.untyped]))
26+
@filter = T.let(nil, T.nilable(String))
2627
@format = T.let(nil, T.nilable(String))
2728
@id = T.let(nil, T.nilable(Integer))
2829
@metafield_namespaces = T.let(nil, T.nilable(T::Array[T.untyped]))

lib/shopify_api/rest/resources/2024_10/webhook.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
2323
@api_version = T.let(nil, T.nilable(String))
2424
@created_at = T.let(nil, T.nilable(String))
2525
@fields = T.let(nil, T.nilable(T::Array[T.untyped]))
26+
@filter = T.let(nil, T.nilable(String))
2627
@format = T.let(nil, T.nilable(String))
2728
@id = T.let(nil, T.nilable(Integer))
2829
@metafield_namespaces = T.let(nil, T.nilable(T::Array[T.untyped]))

lib/shopify_api/webhooks/registration.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,23 @@ class Registration
2222
sig { returns(T.nilable(T::Array[String])) }
2323
attr_reader :metafield_namespaces
2424

25+
sig { returns(T.nilable(String)) }
26+
attr_reader :filter
27+
2528
sig do
2629
params(topic: String, path: String, handler: T.nilable(T.any(Handler, WebhookHandler)),
2730
fields: T.nilable(T.any(String, T::Array[String])),
28-
metafield_namespaces: T.nilable(T::Array[String])).void
31+
metafield_namespaces: T.nilable(T::Array[String]),
32+
filter: T.nilable(String)).void
2933
end
30-
def initialize(topic:, path:, handler: nil, fields: nil, metafield_namespaces: nil)
34+
def initialize(topic:, path:, handler: nil, fields: nil, metafield_namespaces: nil, filter: nil)
3135
@topic = T.let(topic.gsub("/", "_").upcase, String)
3236
@path = path
3337
@handler = handler
3438
fields_array = fields.is_a?(String) ? fields.split(FIELDS_DELIMITER) : fields
3539
@fields = T.let(fields_array&.map(&:strip)&.compact, T.nilable(T::Array[String]))
3640
@metafield_namespaces = T.let(metafield_namespaces&.map(&:strip)&.compact, T.nilable(T::Array[String]))
41+
@filter = filter
3742
end
3843

3944
sig { abstract.returns(String) }
@@ -54,6 +59,7 @@ def build_check_query; end
5459
current_address: T.nilable(String),
5560
fields: T::Array[String],
5661
metafield_namespaces: T::Array[String],
62+
filter: T.nilable(String),
5763
})
5864
end
5965
def parse_check_result(body); end
@@ -88,6 +94,7 @@ def subscription_response_attributes
8894
attributes = ["id"]
8995
attributes << "includeFields" if @fields
9096
attributes << "metafieldNamespaces" if @metafield_namespaces
97+
attributes << "filter" if @filter
9198
attributes
9299
end
93100
end

lib/shopify_api/webhooks/registrations/event_bridge.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def callback_address
1414

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

2021
sig { override.params(webhook_id: T.nilable(String)).returns(String) }
@@ -32,6 +33,7 @@ def build_check_query
3233
id
3334
includeFields
3435
metafieldNamespaces
36+
filter
3537
endpoint {
3638
__typename
3739
... on WebhookEventBridgeEndpoint {
@@ -51,23 +53,26 @@ def build_check_query
5153
current_address: T.nilable(String),
5254
fields: T::Array[String],
5355
metafield_namespaces: T::Array[String],
56+
filter: T.nilable(String),
5457
})
5558
end
5659
def parse_check_result(body)
5760
edges = body.dig("data", "webhookSubscriptions", "edges") || {}
5861
webhook_id = nil
5962
fields = []
6063
metafield_namespaces = []
64+
filter = nil
6165
current_address = nil
6266
unless edges.empty?
6367
node = edges[0]["node"]
6468
webhook_id = node["id"].to_s
6569
current_address = node["endpoint"]["arn"].to_s
6670
fields = node["includeFields"] || []
6771
metafield_namespaces = node["metafieldNamespaces"] || []
72+
filter = node["filter"].to_s
6873
end
6974
{ webhook_id: webhook_id, current_address: current_address, fields: fields,
70-
metafield_namespaces: metafield_namespaces, }
75+
metafield_namespaces: metafield_namespaces, filter: filter, }
7176
end
7277
end
7378
end

lib/shopify_api/webhooks/registrations/http.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def callback_address
2020

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

2627
sig { override.params(webhook_id: T.nilable(String)).returns(String) }
@@ -38,6 +39,7 @@ def build_check_query
3839
id
3940
includeFields
4041
metafieldNamespaces
42+
filter
4143
endpoint {
4244
__typename
4345
... on WebhookHttpEndpoint {
@@ -57,13 +59,15 @@ def build_check_query
5759
current_address: T.nilable(String),
5860
fields: T::Array[String],
5961
metafield_namespaces: T::Array[String],
62+
filter: T.nilable(String),
6063
})
6164
end
6265
def parse_check_result(body)
6366
edges = body.dig("data", "webhookSubscriptions", "edges") || {}
6467
webhook_id = nil
6568
fields = []
6669
metafield_namespaces = []
70+
filter = nil
6771
current_address = nil
6872
unless edges.empty?
6973
node = edges[0]["node"]
@@ -76,9 +80,10 @@ def parse_check_result(body)
7680
end
7781
fields = node["includeFields"] || []
7882
metafield_namespaces = node["metafieldNamespaces"] || []
83+
filter = node["filter"].to_s
7984
end
8085
{ webhook_id: webhook_id, current_address: current_address, fields: fields,
81-
metafield_namespaces: metafield_namespaces, }
86+
metafield_namespaces: metafield_namespaces, filter: filter, }
8287
end
8388
end
8489
end

lib/shopify_api/webhooks/registrations/pub_sub.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def subscription_args
1818
project = project_topic_pair[0]
1919
topic = project_topic_pair[1]
2020
{ pubSubProject: project, pubSubTopic: topic, includeFields: fields,
21-
metafieldNamespaces: metafield_namespaces, }.compact
21+
metafieldNamespaces: metafield_namespaces, filter: filter, }.compact
2222
end
2323

2424
sig { override.params(webhook_id: T.nilable(String)).returns(String) }
@@ -36,6 +36,7 @@ def build_check_query
3636
id
3737
includeFields
3838
metafieldNamespaces
39+
filter
3940
endpoint {
4041
__typename
4142
... on WebhookPubSubEndpoint {
@@ -56,13 +57,15 @@ def build_check_query
5657
current_address: T.nilable(String),
5758
fields: T::Array[String],
5859
metafield_namespaces: T::Array[String],
60+
filter: T.nilable(String),
5961
})
6062
end
6163
def parse_check_result(body)
6264
edges = body.dig("data", "webhookSubscriptions", "edges") || {}
6365
webhook_id = nil
6466
fields = []
6567
metafield_namespaces = []
68+
filter = nil
6669
current_address = nil
6770
unless edges.empty?
6871
node = edges[0]["node"]
@@ -71,9 +74,10 @@ def parse_check_result(body)
7174
"pubsub://#{node["endpoint"]["pubSubProject"]}:#{node["endpoint"]["pubSubTopic"]}"
7275
fields = node["includeFields"] || []
7376
metafield_namespaces = node["metafieldNamespaces"] || []
77+
filter = node["filter"].to_s
7478
end
7579
{ webhook_id: webhook_id, current_address: current_address, fields: fields,
76-
metafield_namespaces: metafield_namespaces, }
80+
metafield_namespaces: metafield_namespaces, filter: filter, }
7781
end
7882
end
7983
end

lib/shopify_api/webhooks/registry.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,25 @@ class << self
1919
path: String,
2020
handler: T.nilable(T.any(Handler, WebhookHandler)),
2121
fields: T.nilable(T.any(String, T::Array[String])),
22+
filter: T.nilable(String),
2223
metafield_namespaces: T.nilable(T::Array[String])).void
2324
end
24-
def add_registration(topic:, delivery_method:, path:, handler: nil, fields: nil, metafield_namespaces: nil)
25+
def add_registration(topic:, delivery_method:, path:, handler: nil, fields: nil, filter: nil,
26+
metafield_namespaces: nil)
2527
@registry[topic] = case delivery_method
2628
when :pub_sub
2729
Registrations::PubSub.new(topic: topic, path: path, fields: fields,
28-
metafield_namespaces: metafield_namespaces)
30+
metafield_namespaces: metafield_namespaces, filter: filter)
2931
when :event_bridge
3032
Registrations::EventBridge.new(topic: topic, path: path, fields: fields,
31-
metafield_namespaces: metafield_namespaces)
33+
metafield_namespaces: metafield_namespaces, filter: filter)
3234
when :http
3335
unless handler
3436
raise Errors::InvalidWebhookRegistrationError, "Cannot create an Http registration without a handler."
3537
end
3638

3739
Registrations::Http.new(topic: topic, path: path, handler: handler,
38-
fields: fields, metafield_namespaces: metafield_namespaces)
40+
fields: fields, metafield_namespaces: metafield_namespaces, filter: filter)
3941
else
4042
raise Errors::InvalidWebhookRegistrationError,
4143
"Unsupported delivery method #{delivery_method}. Allowed values: {:http, :pub_sub, :event_bridge}."
@@ -223,10 +225,12 @@ def webhook_registration_needed?(client, registration)
223225
parsed_check_result = registration.parse_check_result(T.cast(check_response.body, T::Hash[String, T.untyped]))
224226
registration_fields = registration.fields || []
225227
registration_metafield_namespaces = registration.metafield_namespaces || []
228+
registration_filter = registration.filter
226229

227230
must_register = parsed_check_result[:current_address] != registration.callback_address ||
228231
parsed_check_result[:fields].sort != registration_fields.sort ||
229-
parsed_check_result[:metafield_namespaces].sort != registration_metafield_namespaces.sort
232+
parsed_check_result[:metafield_namespaces].sort != registration_metafield_namespaces.sort ||
233+
parsed_check_result[:filter] != registration_filter
230234

231235
{ webhook_id: parsed_check_result[:webhook_id], must_register: must_register }
232236
end

test/webhooks/registry_test.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def setup
5353
address,
5454
fields: "field1, field2",
5555
metafield_namespaces: ["namespace1", "namespace2"],
56+
filter: "id:*",
5657
)
5758

5859
# Then
@@ -72,6 +73,7 @@ def setup
7273
address,
7374
fields: ["field1", "field2"],
7475
metafield_namespaces: ["namespace1", "namespace2"],
76+
filter: "id:*",
7577
)
7678

7779
# Then
@@ -156,6 +158,27 @@ def setup
156158
update_registration_response.body)
157159
end
158160

161+
define_method("test_#{protocol}_update_registration_filter_with_address_#{address}") do
162+
# Given
163+
setup_queries_and_responses(
164+
[queries[protocol][:check_query], queries[protocol][:register_update_query_with_filter]],
165+
[queries[protocol][:check_existing_response],
166+
queries[protocol][:register_update_with_filter_response],],
167+
)
168+
169+
# When
170+
update_registration_response = add_and_register_webhook(
171+
protocol,
172+
address,
173+
filter: "id:*",
174+
)
175+
176+
# Then
177+
assert(update_registration_response.success)
178+
assert_equal(queries[protocol][:register_update_with_filter_response],
179+
update_registration_response.body)
180+
end
181+
159182
define_method("test_raises_on_#{protocol}_registration_check_error_with_address_#{address}") do
160183
# Given
161184
ShopifyAPI::Webhooks::Registry.clear
@@ -412,7 +435,7 @@ def setup_queries_and_responses(queries, responses)
412435
end
413436
end
414437

415-
def add_and_register_webhook(protocol, address, fields: nil, metafield_namespaces: nil)
438+
def add_and_register_webhook(protocol, address, fields: nil, metafield_namespaces: nil, filter: nil)
416439
ShopifyAPI::Webhooks::Registry.add_registration(
417440
topic: @topic,
418441
delivery_method: protocol,
@@ -423,6 +446,7 @@ def add_and_register_webhook(protocol, address, fields: nil, metafield_namespace
423446
),
424447
fields: fields,
425448
metafield_namespaces: metafield_namespaces,
449+
filter: filter,
426450
)
427451
update_registration_response = ShopifyAPI::Webhooks::Registry.register_all(
428452
session: @session,

0 commit comments

Comments
 (0)