From 175f68b0ed99cee3ce5183c7e730f4af0c146e9b Mon Sep 17 00:00:00 2001 From: Mathew Hartley Date: Wed, 11 Oct 2023 03:26:11 +1300 Subject: [PATCH] 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. --- CHANGELOG.md | 1 + docs/usage/webhooks.md | 11 +++ lib/shopify_api/webhooks/registration.rb | 23 +++++- .../webhooks/registrations/event_bridge.rb | 2 +- .../webhooks/registrations/http.rb | 2 +- .../webhooks/registrations/pub_sub.rb | 3 +- lib/shopify_api/webhooks/registry.rb | 14 ++-- test/webhooks/registry_test.rb | 33 +++++++- test/webhooks/webhook_registration_queries.rb | 78 +++++++++++++++++++ 9 files changed, 152 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6921ef72f..448e79ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/usage/webhooks.md b/docs/usage/webhooks.md index 2df29e036..203e23cb8 100644 --- a/docs/usage/webhooks.md +++ b/docs/usage/webhooks.md @@ -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 diff --git a/lib/shopify_api/webhooks/registration.rb b/lib/shopify_api/webhooks/registration.rb index dd938c322..c8a2e6c91 100644 --- a/lib/shopify_api/webhooks/registration.rb +++ b/lib/shopify_api/webhooks/registration.rb @@ -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) } @@ -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 @@ -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 diff --git a/lib/shopify_api/webhooks/registrations/event_bridge.rb b/lib/shopify_api/webhooks/registrations/event_bridge.rb index e5e441849..51076861b 100644 --- a/lib/shopify_api/webhooks/registrations/event_bridge.rb +++ b/lib/shopify_api/webhooks/registrations/event_bridge.rb @@ -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) } diff --git a/lib/shopify_api/webhooks/registrations/http.rb b/lib/shopify_api/webhooks/registrations/http.rb index a5c69189d..a8446ac20 100644 --- a/lib/shopify_api/webhooks/registrations/http.rb +++ b/lib/shopify_api/webhooks/registrations/http.rb @@ -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) } diff --git a/lib/shopify_api/webhooks/registrations/pub_sub.rb b/lib/shopify_api/webhooks/registrations/pub_sub.rb index 79caf0e0c..c1a229341 100644 --- a/lib/shopify_api/webhooks/registrations/pub_sub.rb +++ b/lib/shopify_api/webhooks/registrations/pub_sub.rb @@ -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) } diff --git a/lib/shopify_api/webhooks/registry.rb b/lib/shopify_api/webhooks/registry.rb index da7a77efd..b19265b5e 100644 --- a/lib/shopify_api/webhooks/registry.rb +++ b/lib/shopify_api/webhooks/registry.rb @@ -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}." diff --git a/test/webhooks/registry_test.rb b/test/webhooks/registry_test.rb index 8b09889eb..2c966fc1b 100644 --- a/test/webhooks/registry_test.rb +++ b/test/webhooks/registry_test.rb @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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]) }) @@ -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, diff --git a/test/webhooks/webhook_registration_queries.rb b/test/webhooks/webhook_registration_queries.rb index 1085fd13b..8d4659ae8 100644 --- a/test/webhooks/webhook_registration_queries.rb +++ b/test/webhooks/webhook_registration_queries.rb @@ -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" => { @@ -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" => { @@ -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" => { @@ -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" => { @@ -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" => { @@ -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" => {