Skip to content

Commit

Permalink
Add support for updating REST attributes as blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Aug 1, 2024
1 parent 38c55af commit 00b4788
Show file tree
Hide file tree
Showing 15 changed files with 73 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api

- [#1327](https://github.com/Shopify/shopify-api-ruby/pull/1327) Support `?debug=true` parameter in GraphQL client requests
- [#1308](https://github.com/Shopify/shopify-api-ruby/pull/1308) Support hash_with_indifferent_access when creating REST objects from Shopify responses. Closes #1296
- [#1332](https://github.com/Shopify/shopify-api-ruby/pull/1332) Fixed an issue where `Customer` REST API PUT requests didn't send all of the fields in the `email_marketing_consent` attribute

## 14.4.0

Expand Down
5 changes: 5 additions & 0 deletions lib/shopify_api/rest/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Base
@paths = T.let([], T::Array[T::Hash[Symbol, T.any(T::Array[Symbol], String, Symbol)]])
@custom_prefix = T.let(nil, T.nilable(String))
@read_only_attributes = T.let([], T.nilable(T::Array[Symbol]))
@block_update_attributes = T.let([], T::Array[Symbol])
@aliased_properties = T.let({}, T::Hash[String, String])

sig { returns(T::Hash[Symbol, T.untyped]) }
Expand Down Expand Up @@ -62,6 +63,9 @@ class << self
sig { returns(T::Hash[Symbol, T::Class[T.anything]]) }
attr_reader :has_one

sig { returns(T.nilable(T::Array[Symbol])) }
attr_reader :block_update_attributes

sig { returns(T.nilable(T::Hash[T.any(Symbol, String), String])) }
attr_accessor :headers

Expand Down Expand Up @@ -418,6 +422,7 @@ def attributes_to_update
ShopifyAPI::Utils::AttributesComparator.compare(
stringified_updatable_attributes,
stringified_new_attributes,
block_update_attributes: self.class.block_update_attributes || [],
)
end

Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2022_04/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2022_07/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2022_10/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2023_01/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2023_04/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2023_07/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2023_10/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2024_01/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_api/rest/resources/2024_04/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
2 changes: 2 additions & 0 deletions lib/shopify_api/rest/resources/2024_07/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
@accepts_marketing_updated_at = T.let(nil, T.nilable(String))
@addresses = T.let(nil, T.nilable(T::Array[T.untyped]))
@created_at = T.let(nil, T.nilable(String))

@currency = T.let(nil, T.nilable(String))
@default_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
@email = T.let(nil, T.nilable(String))
Expand Down Expand Up @@ -55,6 +56,7 @@ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
metafield: Metafield
}, T::Hash[Symbol, Class])
@has_many = T.let({}, T::Hash[Symbol, Class])
@block_update_attributes = [:email_marketing_consent]
@paths = T.let([
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
Expand Down
25 changes: 21 additions & 4 deletions lib/shopify_api/utils/attributes_comparator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ class << self
params(
original_attributes: T::Hash[String, T.untyped],
updated_attributes: T::Hash[String, T.untyped],
block_update_attributes: T::Array[Symbol],
).returns(T::Hash[String, T.untyped])
end
def compare(original_attributes, updated_attributes)
def compare(original_attributes, updated_attributes, block_update_attributes: [])
attributes_diff = HashDiff::Comparison.new(
original_attributes,
updated_attributes,
Expand All @@ -24,6 +25,7 @@ def compare(original_attributes, updated_attributes)
update_value = build_update_value(
attributes_diff,
reference_values: updated_attributes,
block_update_attributes: block_update_attributes,
)

update_value
Expand All @@ -34,9 +36,10 @@ def compare(original_attributes, updated_attributes)
diff: T::Hash[String, T.untyped],
path: T::Array[String],
reference_values: T::Hash[String, T.untyped],
block_update_attributes: T::Array[Symbol],
).returns(T::Hash[String, T.untyped])
end
def build_update_value(diff, path: [], reference_values: {})
def build_update_value(diff, path: [], reference_values: {}, block_update_attributes: [])
new_hash = {}

diff.each do |key, value|
Expand All @@ -49,7 +52,19 @@ def build_update_value(diff, path: [], reference_values: {})
if has_numbered_key && ref_value.is_a?(Array)
new_hash[key] = ref_value
else
new_value = build_update_value(value, path: current_path, reference_values: reference_values)
new_value = build_update_value(
value,
path: current_path,
reference_values: reference_values,
block_update_attributes: block_update_attributes,
)

block_update = block_update_attributes.include?(key.to_sym)

# If the key is in block_update_attributes, we use the entire reference value
# so we update the hash as a whole.
if !new_value.empty? && !ref_value.empty? && block_update
new_hash[key] = ref_value

# Only add to new_hash if the user intentionally updates
# to empty value like `{}` or `[]`. For example:
Expand All @@ -70,7 +85,9 @@ def build_update_value(diff, path: [], reference_values: {})
# new_hash = {}
#
# new_hash is empty because nothing changes
new_hash[key] = new_value if !new_value.empty? || ref_value.empty?
elsif !new_value.empty? || ref_value.empty?
new_hash[key] = new_value
end
end
elsif value != HashDiff::NO_VALUE
new_hash[key] = value
Expand Down
27 changes: 27 additions & 0 deletions test/clients/base_rest_resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,33 @@ def test_put_request_for_has_one_association_works
customer.save
end

def test_put_request_with_block_update_attributes
stub_request(:get, "https://test-shop.myshopify.com/admin/api/#{ShopifyAPI::Context.api_version}/customers/207119551.json")
.to_return(status: 200, body: JSON.generate({
"customer" => { "id" => 207119551, "email" => "bob.norman@mail.example.com", "accepts_marketing" => false, "created_at" => "2023-02-02T09:42:27-05:00", "updated_at" => "2023-02-02T09:42:27-05:00", "first_name" => "Bob", "last_name" => "Norman", "orders_count" => 1, "state" => "disabled", "total_spent" => "199.65", "last_order_id" => 450789469, "note" => nil, "verified_email" => true, "multipass_identifier" => nil, "tax_exempt" => false, "tags" => "L\u00E9on, No\u00EBl", "last_order_name" => "#1001", "currency" => "USD", "phone" => "+16136120707", "addresses" => [{ "id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true }], "accepts_marketing_updated_at" => "2005-06-12T11:57:11-04:00", "marketing_opt_in_level" => nil, "tax_exemptions" => [], "email_marketing_consent" => { "state" => "not_subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2004-06-13T11:57:11-04:00" }, "sms_marketing_consent" => { "state" => "not_subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2023-02-02T09:42:27-05:00", "consent_collected_from" => "OTHER" }, "admin_graphql_api_id" => "gid://shopify/Customer/207119551", "default_address" => { "id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true } },
}), headers: {})

customer = ShopifyAPI::Customer.find(
id: 207119551,
session: @session,
)
customer.client.expects(:put).with(
body: { "customer" => {
"id" => 207119551,
"email_marketing_consent" => { "state" => "subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-01T00:00:00.000Z" },
} },
path: "customers/207119551.json",
headers: nil,
)
customer.email_marketing_consent = {
"state" => "subscribed",
"opt_in_level" => "single_opt_in",
"consent_updated_at" => "2024-01-01T00:00:00.000Z",
}

customer.save
end

def test_put_requests_for_resource_with_read_only_attributes
stub_request(:get, "https://test-shop.myshopify.com/admin/api/#{ShopifyAPI::Context.api_version}/variants/169.json")
.to_return(
Expand Down
8 changes: 8 additions & 0 deletions test/utils/attributes_comparator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ def test_attributes_comparator
updated_attributes: { "a" => { 0 => "test3", 1 => "test4" } },
expected: { "a" => { 0 => "test3", 1 => "test4" } },
},
{
name: "applies hashes as a whole when key is in block_update_attributes",
original_attributes: { "a" => { "b" => 1, "c" => "abc" } },
updated_attributes: { "a" => { "b" => nil, "c" => "abc" } },
block_update_attributes: [:a],
expected: { "a" => { "b" => nil, "c" => "abc" } },
},
]

test_cases.each do |test_case|
Expand All @@ -94,6 +101,7 @@ def test_attributes_comparator
ShopifyAPI::Utils::AttributesComparator.compare(
test_case[:original_attributes],
test_case[:updated_attributes],
block_update_attributes: test_case[:block_update_attributes] || [],
),
test_case[:name],
)
Expand Down

0 comments on commit 00b4788

Please sign in to comment.