diff --git a/CHANGELOG.md b/CHANGELOG.md index 127bab169..eb6fda94d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased +- Preserve in-place changes to nested REST attributes when saving resources. ## 16.3.1 (2026-10-22) - [#1472](https://github.com/Shopify/shopify-api-ruby/pull/1472) `ShopifyAPI::Auth.embedded_app_url` now raises `ShopifyAPI::Errors::InvalidShopError` unless the decoded `host` is a trusted Shopify Admin host (see `ShopifyAPI::Utils::ShopValidator`). URLs for supported Admin hosts are unchanged. diff --git a/lib/shopify_api/rest/base.rb b/lib/shopify_api/rest/base.rb index 3cc4ebd18..c69961039 100644 --- a/lib/shopify_api/rest/base.rb +++ b/lib/shopify_api/rest/base.rb @@ -3,6 +3,7 @@ require "active_support/inflector" require "active_support/core_ext/hash/indifferent_access" +require "active_support/core_ext/object/deep_dup" module ShopifyAPI module Rest @@ -301,6 +302,7 @@ def create_instance(data:, session:, instance: nil) end end + instance.original_state = instance.original_state.deep_dup instance end end diff --git a/test/clients/base_rest_resource_test.rb b/test/clients/base_rest_resource_test.rb index 78a2e19c8..c090e35e3 100644 --- a/test/clients/base_rest_resource_test.rb +++ b/test/clients/base_rest_resource_test.rb @@ -315,6 +315,55 @@ def test_saves_removing_children assert_requested(stubbed_request) end + def test_saves_in_place_mutations_to_nested_attributes + resource = TestHelpers::FakeResource.create_instance( + session: @session, + data: { + "id" => 1, + "details" => { "address" => { "first_name" => "John" }, "tags" => ["original"] }, + }, + ) + stubbed_request = stub_request(:put, "#{@prefix}/fake_resources/1.json") + .with(body: hash_including( + "fake_resource" => hash_including( + "details" => { "address" => { "first_name" => "Jane" } }, + ), + )) + .to_return(status: 200) + + resource.details["address"]["first_name"] = "Jane" + resource.save + + assert_requested(stubbed_request) + end + + def test_preserves_nested_mutations_until_save_succeeds + resource = TestHelpers::FakeResource.create_instance( + session: @session, + data: { "id" => 1, "details" => { "address" => { "first_name" => "John" } } }, + ) + resource.details["address"]["first_name"] = "Jane" + + changed_attributes = { "fake_resource" => { "details" => { "address" => { "first_name" => "Jane" } }, "id" => 1 } } + changed_request = stub_request(:put, "#{@prefix}/fake_resources/1.json") + .with(body: changed_attributes) + .to_return(status: 422, body: { errors: "Invalid details" }.to_json) + .then + .to_return(status: 200, body: { fake_resource: { id: 1, details: { address: { first_name: "Jane" } } } }.to_json) + + assert_raises(ShopifyAPI::Errors::HttpResponseError) { resource.save! } + resource.save! + + assert_requested(changed_request, times: 2) + + stubbed_request = stub_request(:put, "#{@prefix}/fake_resources/1.json") + .with(body: { "fake_resource" => { "id" => 1 } }) + .to_return(status: 200) + resource.save + + assert_requested(stubbed_request) + end + def test_loads_unknown_attribute_with_special_character body = { fake_resource: { id: 1, attribute: "attribute", "unknown?": "some-value" } }.to_json