Skip to content

Commit

Permalink
Extend webhook registration to support metafield_namespaces (#1186)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
fourseven authored Oct 10, 2023
1 parent 81c1aed commit 175f68b
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
- [#1183](https://github.com/Shopify/shopify-api-ruby/pull/1189) Added string array support for fields parameter in Webhook::Registry
- [1208](https://github.com/Shopify/shopify-api-ruby/pull/1208) Fix CustomerAddress and FulfillmentRequest methods
- [1225](https://github.com/Shopify/shopify-api-ruby/pull/1225) Support for 2023_10 API version
- [#1186](https://github.com/Shopify/shopify-api-ruby/pull/1186) Extend webhook registration to support metafield_namespaces

## 13.1.0

Expand Down
11 changes: 11 additions & 0 deletions docs/usage/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ registration = ShopifyAPI::Webhooks::Registry.add_registration(
)
```

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.

```ruby
registration = ShopifyAPI::Webhooks::Registry.add_registration(
topic: "orders/create",
delivery_method: :http,
handler: WebhookHandler,
metafieldNamespaces: ["custom"]
)
```

**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.

### EventBridge and PubSub Webhooks
Expand Down
23 changes: 19 additions & 4 deletions lib/shopify_api/webhooks/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ class Registration
sig { returns(T.nilable(T::Array[String])) }
attr_reader :fields

sig { returns(T.nilable(T::Array[String])) }
attr_reader :metafield_namespaces

sig do
params(topic: String, path: String, handler: T.nilable(Handler),
fields: T.nilable(T.any(String, T::Array[String]))).void
fields: T.nilable(T.any(String, T::Array[String])),
metafield_namespaces: T.nilable(T::Array[String])).void
end
def initialize(topic:, path:, handler: nil, fields: nil)
def initialize(topic:, path:, handler: nil, fields: nil, metafield_namespaces: nil)
@topic = T.let(topic.gsub("/", "_").upcase, String)
@path = path
@handler = handler
fields_array = fields.is_a?(String) ? fields.split(FIELDS_DELIMITER) : fields
@fields = T.let(fields_array&.map(&:strip)&.compact, T.nilable(T::Array[String]))
@metafield_namespaces = T.let(metafield_namespaces&.map(&:strip)&.compact, T.nilable(T::Array[String]))
end

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

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

<<~QUERY
Expand All @@ -62,12 +67,22 @@ def build_register_query(webhook_id: nil)
message
}
webhookSubscription {
id#{@fields.nil? ? "" : "\n includeFields"}
#{subscription_response_attributes.join("\n ")}
}
}
}
QUERY
end

private

sig { returns(T::Array[String]) }
def subscription_response_attributes
attributes = ["id"]
attributes << "includeFields" if @fields
attributes << "metafieldNamespaces" if @metafield_namespaces
attributes
end
end
end
end
2 changes: 1 addition & 1 deletion lib/shopify_api/webhooks/registrations/event_bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def callback_address

sig { override.returns(T::Hash[Symbol, String]) }
def subscription_args
{ arn: callback_address, includeFields: fields }.compact
{ arn: callback_address, includeFields: fields, metafieldNamespaces: metafield_namespaces }.compact
end

sig { override.params(webhook_id: T.nilable(String)).returns(String) }
Expand Down
2 changes: 1 addition & 1 deletion lib/shopify_api/webhooks/registrations/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def callback_address

sig { override.returns(T::Hash[Symbol, String]) }
def subscription_args
{ callbackUrl: callback_address, includeFields: fields }.compact
{ callbackUrl: callback_address, includeFields: fields, metafieldNamespaces: metafield_namespaces }.compact
end

sig { override.params(webhook_id: T.nilable(String)).returns(String) }
Expand Down
3 changes: 2 additions & 1 deletion lib/shopify_api/webhooks/registrations/pub_sub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def subscription_args
project_topic_pair = callback_address.gsub(%r{^pubsub://}, "").split(":")
project = project_topic_pair[0]
topic = project_topic_pair[1]
{ pubSubProject: project, pubSubTopic: topic, includeFields: fields }.compact
{ pubSubProject: project, pubSubTopic: topic, includeFields: fields,
metafieldNamespaces: metafield_namespaces, }.compact
end

sig { override.params(webhook_id: T.nilable(String)).returns(String) }
Expand Down
14 changes: 9 additions & 5 deletions lib/shopify_api/webhooks/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@ class << self
delivery_method: Symbol,
path: String,
handler: T.nilable(Handler),
fields: T.nilable(T.any(String, T::Array[String]))).void
fields: T.nilable(T.any(String, T::Array[String])),
metafield_namespaces: T.nilable(T::Array[String])).void
end
def add_registration(topic:, delivery_method:, path:, handler: nil, fields: nil)
def add_registration(topic:, delivery_method:, path:, handler: nil, fields: nil, metafield_namespaces: nil)
@registry[topic] = case delivery_method
when :pub_sub
Registrations::PubSub.new(topic: topic, path: path, fields: fields)
Registrations::PubSub.new(topic: topic, path: path, fields: fields,
metafield_namespaces: metafield_namespaces)
when :event_bridge
Registrations::EventBridge.new(topic: topic, path: path, fields: fields)
Registrations::EventBridge.new(topic: topic, path: path, fields: fields,
metafield_namespaces: metafield_namespaces)
when :http
unless handler
raise Errors::InvalidWebhookRegistrationError, "Cannot create an Http registration without a handler."
end

Registrations::Http.new(topic: topic, path: path, handler: handler, fields: fields)
Registrations::Http.new(topic: topic, path: path, handler: handler,
fields: fields, metafield_namespaces: metafield_namespaces)
else
raise Errors::InvalidWebhookRegistrationError,
"Unsupported delivery method #{delivery_method}. Allowed values: {:http, :pub_sub, :event_bridge}."
Expand Down
33 changes: 30 additions & 3 deletions test/webhooks/registry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ def test_http_registration_with_fields_array_add_and_update
do_registration_test(:http, "test-webhooks", fields: ["field1", "field2"])
end

def test_http_registration_with_metafield_namespaces_add_and_update
do_registration_test(:http, "test-webhooks", metafield_namespaces: ["namespace1", "namespace2"])
end

def test_raises_on_http_registration_check_error
do_registration_check_error_test(:http, "test-webhooks")
end
Expand All @@ -114,6 +118,11 @@ def test_pubsub_registration_with_fields_array_add_and_update
do_registration_test(:pub_sub, "pubsub://my-project-id:my-topic-id", fields: ["field1", "field2"])
end

def test_pubsub_registration_with_metafield_namespaces_add_and_update
do_registration_test(:pub_sub, "pubsub://my-project-id:my-topic-id",
metafield_namespaces: ["namespace1", "namespace2"])
end

def test_raises_on_pubsub_registration_check_error
do_registration_check_error_test(:pub_sub, "pubsub://my-project-id:my-topic-id")
end
Expand All @@ -130,6 +139,10 @@ def test_eventbridge_registration_with_fields_array_add_and_update
do_registration_test(:event_bridge, "test-webhooks", fields: ["field1", "field2"])
end

def test_eventbridge_registration_with_metafield_namespaces_add_and_update
do_registration_test(:event_bridge, "test-webhooks", metafield_namespaces: ["namespace1", "namespace2"])
end

def test_raises_on_eventbridge_registration_check_error
do_registration_check_error_test(:event_bridge, "test-webhooks")
end
Expand Down Expand Up @@ -251,7 +264,7 @@ def test_get_webhook_id_with_graphql_errors

private

def do_registration_test(delivery_method, path, fields: nil)
def do_registration_test(delivery_method, path, fields: nil, metafield_namespaces: nil)
ShopifyAPI::Webhooks::Registry.clear

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

add_query_type = fields.nil? ? :register_add_query : :register_add_query_with_fields
add_response_type = fields.nil? ? :register_add_response : :register_add_with_fields_response
add_query_type = if fields.present?
:register_add_query_with_fields
elsif metafield_namespaces.present?
:register_add_query_with_metafield_namespaces
else
:register_add_query
end
add_response_type = if fields.present?
:register_add_with_fields_response
elsif metafield_namespaces.present?
:register_add_with_metafield_namespaces_response
else
:register_add_response
end

stub_request(:post, @url)
.with(body: JSON.dump({ query: queries[delivery_method][add_query_type], variables: nil }))
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][add_response_type]) })
Expand All @@ -275,6 +301,7 @@ def do_registration_test(delivery_method, path, fields: nil)
end,
),
fields: fields,
metafield_namespaces: metafield_namespaces,
)
registration_response = ShopifyAPI::Webhooks::Registry.register_all(
session: @session,
Expand Down
78 changes: 78 additions & 0 deletions test/webhooks/webhook_registration_queries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ def queries
}
}
QUERY
register_add_query_with_metafield_namespaces:
<<~QUERY,
mutation webhookSubscription {
webhookSubscriptionCreate(topic: SOME_TOPIC, webhookSubscription: {callbackUrl: "https://app-address.com/test-webhooks", metafieldNamespaces: ["namespace1", "namespace2"]}) {
userErrors {
field
message
}
webhookSubscription {
id
metafieldNamespaces
}
}
}
QUERY

register_add_response: {
"data" => {
Expand All @@ -82,6 +97,17 @@ def queries
},
},
},
register_add_with_metafield_namespaces_response: {
"data" => {
"webhookSubscriptionCreate" => {
"userErrors" => [],
"webhookSubscription" => {
"id" => "gid://shopify/WebhookSubscription/12345",
"metafieldNamespaces" => ["namespace1", "namespace2"],
},
},
},
},
check_existing_response: {
"data" => {
"webhookSubscriptions" => {
Expand Down Expand Up @@ -175,6 +201,21 @@ def queries
}
}
QUERY
register_add_query_with_metafield_namespaces:
<<~QUERY,
mutation webhookSubscription {
eventBridgeWebhookSubscriptionCreate(topic: SOME_TOPIC, webhookSubscription: {arn: "test-webhooks", metafieldNamespaces: ["namespace1", "namespace2"]}) {
userErrors {
field
message
}
webhookSubscription {
id
metafieldNamespaces
}
}
}
QUERY
register_add_response: {
"data" => {
"eventBridgeWebhookSubscriptionCreate" => {
Expand All @@ -194,6 +235,17 @@ def queries
},
},
},
register_add_with_metafield_namespaces_response: {
"data" => {
"eventBridgeWebhookSubscriptionCreate" => {
"userErrors" => [],
"webhookSubscription" => {
"id" => "gid://shopify/WebhookSubscription/12345",
"metafieldNamespaces" => ["namespace1", "namespace2"],
},
},
},
},
check_existing_response: {
"data" => {
"webhookSubscriptions" => {
Expand Down Expand Up @@ -288,6 +340,21 @@ def queries
}
}
QUERY
register_add_query_with_metafield_namespaces:
<<~QUERY,
mutation webhookSubscription {
pubSubWebhookSubscriptionCreate(topic: SOME_TOPIC, webhookSubscription: {pubSubProject: "my-project-id", pubSubTopic: "my-topic-id", metafieldNamespaces: ["namespace1", "namespace2"]}) {
userErrors {
field
message
}
webhookSubscription {
id
metafieldNamespaces
}
}
}
QUERY
register_add_response: {
"data" => {
"pubSubWebhookSubscriptionCreate" => {
Expand All @@ -307,6 +374,17 @@ def queries
},
},
},
register_add_with_metafield_namespaces_response: {
"data" => {
"pubSubWebhookSubscriptionCreate" => {
"userErrors" => [],
"webhookSubscription" => {
"id" => "gid://shopify/WebhookSubscription/12345",
"metafieldNamespaces" => ["namespace1", "namespace2"],
},
},
},
},
check_existing_response: {
"data" => {
"webhookSubscriptions" => {
Expand Down

0 comments on commit 175f68b

Please sign in to comment.