Skip to content

Commit b10f372

Browse files
authored
Merge pull request #1347 from nebulab/kennyadsl/add-filter-support
Extend webhook registration to support filters
2 parents 451f983 + b0d58a3 commit b10f372

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)