-
Notifications
You must be signed in to change notification settings - Fork 476
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes attribute diffing logic when updating REST resources with `.sav…
…e` (#1282) * Adds attributes comparator and test case * Moves hash_diff to comparator * Adds changelog entry * Updates docs/usage/rest.md to explain how updating resource works Co-authored-by: Paulo Margarido <[email protected]> * Updates rest.md to omit implementation details --------- Co-authored-by: Paulo Margarido <[email protected]>
- Loading branch information
Showing
6 changed files
with
306 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,29 +69,72 @@ Typical methods provided for each resources are: | |
Full list of methods can be found on each of the resource class. | ||
- Path: | ||
- https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/rest/resources/#{version}/#{resource}.rb | ||
- Example for `Order` resource on `2023-04` version: | ||
- https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/rest/resources/2023_04/order.rb | ||
- Example for `Order` resource on `2024-01` version: | ||
- https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/rest/resources/2024_01/order.rb | ||
|
||
### Usage Examples | ||
⚠️ Reference documentation on [shopify.dev](https://shopify.dev/docs/api/admin-rest) contains more examples on how to use each REST Resources. | ||
### The `save` method | ||
|
||
```Ruby | ||
# Find and update a customer email | ||
customer = ShopifyAPI::Customer.find(id: customer_id) | ||
customer.email = "[email protected]" | ||
customer.save! | ||
The `save` or `save!` method on a resource allows you to `create` or `update` that resource. | ||
|
||
#### Create a new resource | ||
|
||
To create a new resource using the `save` or `save!` method, you can initialize the resource with a hash of values or simply assigning them manually. For example: | ||
|
||
```Ruby | ||
# Create a new product from hash | ||
product_properties = { | ||
title: "My awesome product" | ||
} | ||
product = ShopifyAPI::Product.new(from_hash: product_properties) | ||
product.save! | ||
|
||
# Create a product manually | ||
# Create a new product manually | ||
product = ShopifyAPI::Product.new | ||
product.title = "Another one" | ||
product.save! | ||
``` | ||
|
||
#### Update an existing resource | ||
|
||
To update an existing resource using the `save` or `save!` method, you'll need to fetch the resource from Shopify first. Then, you can manually assign new values to the resource before calling `save` or `save!`. For example: | ||
|
||
```Ruby | ||
# Update a product's title | ||
product = ShopifyAPI::Product.find(id: product_id) | ||
product.title = "My new title" | ||
product.save! | ||
|
||
# Remove a line item from a draft order | ||
draft_order = ShopifyAPI::DraftOrder.find(id: draft_order_id) | ||
|
||
new_line_items = draft_order.line_items.reject { |line_item| line_item["id"] == 12345 } | ||
draft_order.line_items = new_line_items | ||
|
||
draft_order.save! | ||
``` | ||
|
||
> [!IMPORTANT] | ||
> If you need to unset an existing value, | ||
> please explicitly set that attribute to `nil` or empty values such as `[]` or `{}`. For example: | ||
> | ||
> ```Ruby | ||
> # Removes shipping address from draft_order | ||
> draft_order.shipping_address = {} | ||
> draft_order.save! | ||
> ``` | ||
> | ||
> This is because only modified values are sent to the API, so if `shipping_address` is not "modified" to `{}`. It won't be part of the PUT request payload | ||
When updating a resource, only the modified attributes, the resource's primary key, and required parameters are sent to the API. The primary key is usually the `id` attribute of the resource, but it can vary if the `primary_key` method is overwritten in the resource's class. The required parameters are identified using the path parameters of the `PUT` endpoint of the resource. | ||
### Usage Examples | ||
⚠️ The [API reference documentation](https://shopify.dev/docs/api/admin-rest) contains more examples on how to use each REST Resources. | ||
```Ruby | ||
# Find and update a customer email | ||
customer = ShopifyAPI::Customer.find(id: customer_id) | ||
customer.email = "[email protected]" | ||
customer.save! | ||
# Get all orders | ||
orders = ShopifyAPI::Orders.all | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "hash_diff" | ||
|
||
module ShopifyAPI | ||
module Utils | ||
module AttributesComparator | ||
class << self | ||
extend T::Sig | ||
|
||
sig do | ||
params( | ||
original_attributes: T::Hash[String, T.untyped], | ||
updated_attributes: T::Hash[String, T.untyped], | ||
).returns(T::Hash[String, T.untyped]) | ||
end | ||
def compare(original_attributes, updated_attributes) | ||
attributes_diff = HashDiff::Comparison.new( | ||
original_attributes, | ||
updated_attributes, | ||
).left_diff | ||
|
||
update_value = build_update_value( | ||
attributes_diff, | ||
reference_values: updated_attributes, | ||
) | ||
|
||
update_value | ||
end | ||
|
||
sig do | ||
params( | ||
diff: T::Hash[String, T.untyped], | ||
path: T::Array[String], | ||
reference_values: T::Hash[String, T.untyped], | ||
).returns(T::Hash[String, T.untyped]) | ||
end | ||
def build_update_value(diff, path: [], reference_values: {}) | ||
new_hash = {} | ||
|
||
diff.each do |key, value| | ||
current_path = path + [key.to_s] | ||
|
||
if value.is_a?(Hash) | ||
has_numbered_key = value.keys.any? { |k| k.is_a?(Integer) } | ||
ref_value = T.unsafe(reference_values).dig(*current_path) | ||
|
||
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) | ||
|
||
# Only add to new_hash if the user intentionally updates | ||
# to empty value like `{}` or `[]`. For example: | ||
# | ||
# original = { "a" => { "foo" => 1 } } | ||
# updated = { "a" => {} } | ||
# diff = { "a" => { "foo" => HashDiff::NO_VALUE } } | ||
# key = "a", new_value = {}, ref_value = {} | ||
# new_hash = { "a" => {} } | ||
# | ||
# In addition, we omit cases where after removing `HashDiff::NO_VALUE` | ||
# we only have `{}` left. For example: | ||
# | ||
# original = { "a" => { "foo" => 1, "bar" => 2} } | ||
# updated = { "a" => { "foo" => 1 } } | ||
# diff = { "a" => { "bar" => HashDiff::NO_VALUE } } | ||
# key = "a", new_value = {}, ref_value = { "foo" => 1 } | ||
# new_hash = {} | ||
# | ||
# new_hash is empty because nothing changes | ||
new_hash[key] = new_value if !new_value.empty? || ref_value.empty? | ||
end | ||
elsif value != HashDiff::NO_VALUE | ||
new_hash[key] = value | ||
end | ||
end | ||
|
||
new_hash | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.