From 3337be0035c9c2c40cada3e21f4dc0ff4a7b66a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 12 Oct 2026 19:07:06 +0300 Subject: [PATCH 1/2] Preserve nested REST attribute changes on save --- CHANGELOG.md | 1 + lib/shopify_api/rest/base.rb | 2 ++ test/clients/base_rest_resource_test.rb | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f20cf2f..18dda64eb 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.0 (2026-08-04) - [#1443](https://github.com/Shopify/shopify-api-ruby/pull/1443) Add `ShopifyAPI::Utils::ShopValidator` with `sanitize_shop_domain` and `sanitize!`. 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..5a72c77e0 100644 --- a/test/clients/base_rest_resource_test.rb +++ b/test/clients/base_rest_resource_test.rb @@ -315,6 +315,28 @@ 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_loads_unknown_attribute_with_special_character body = { fake_resource: { id: 1, attribute: "attribute", "unknown?": "some-value" } }.to_json From 2b4ba6cd13d9532c280ad8cdad9d071fd7596c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 29 Oct 2026 01:10:33 +0300 Subject: [PATCH 2/2] Test nested REST changes survive failed saves Generated-by: Codex --- test/clients/base_rest_resource_test.rb | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/clients/base_rest_resource_test.rb b/test/clients/base_rest_resource_test.rb index 5a72c77e0..c090e35e3 100644 --- a/test/clients/base_rest_resource_test.rb +++ b/test/clients/base_rest_resource_test.rb @@ -337,6 +337,33 @@ def test_saves_in_place_mutations_to_nested_attributes 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