From db5f7aea4e05268fa8953194ac9b4fb09a1e8f8a Mon Sep 17 00:00:00 2001 From: gobinathal Date: Wed, 28 Feb 2024 00:19:49 +0530 Subject: [PATCH] add support for transaction --- lib/firebase.rb | 47 +++++++++++++++++++----- lib/firebase/response.rb | 4 +++ spec/firebase_spec.rb | 78 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 115 insertions(+), 14 deletions(-) diff --git a/lib/firebase.rb b/lib/firebase.rb index 0143ebc..d3dc396 100644 --- a/lib/firebase.rb +++ b/lib/firebase.rb @@ -36,24 +36,24 @@ def initialize(base_uri, auth=nil, scope=%w(https://www.googleapis.com/auth/fire # Writes and returns the data # Firebase.set('users/info', { 'name' => 'Oscar' }) => { 'name' => 'Oscar' } - def set(path, data, query={}) - process :put, path, data, query + def set(path, data, query={}, header={}) + process :put, path, data, query, header end # Returns the data at path - def get(path, query={}) - process :get, path, nil, query + def get(path, query={}, header={}) + process :get, path, nil, query, header end # Writes the data, returns the key name of the data added # Firebase.push('users', { 'age' => 18}) => {"name":"-INOQPH-aV_psbk3ZXEX"} - def push(path, data, query={}) - process :post, path, data, query + def push(path, data, query={}, header={}) + process :post, path, data, query, header end # Deletes the data at path and returs true - def delete(path, query={}) - process :delete, path, nil, query + def delete(path, query={}, header={}) + process :delete, path, nil, query, header end # Write the data at path but does not delete ommited children. Returns the data @@ -62,9 +62,37 @@ def update(path, data, query={}) process :patch, path, data, query end + # Writes the data at path in a transactional manner. So the set fails if the value changes in the meantime. Returns the data + # Supply a block to return the new value of the node + # firebase_client.transaction("users/info", max_retries: 0) do |user_info_snapshot| + # user_info_snapshot["name"] = "new_name" + # user_info_snapshot + # end + def transaction(path, max_retries: nil, &block) + response = get path, {}, { "X-Firebase-ETag": true } + return response unless response.success? + + etag_value = response.etag + data = response.body + + data = block.call(data) + + no_of_retries = 0 + loop do + response = set path, data, {}, { "if-match": etag_value } + break if response.success? + + no_of_retries += 1 + etag_value = response.etag + break if max_retries && no_of_retries > max_retries + end + + response + end + private - def process(verb, path, data=nil, query={}) + def process(verb, path, data=nil, query={}, header= {}) if path[0] == '/' raise(ArgumentError.new("Invalid path: #{path}. Path must be relative")) end @@ -78,6 +106,7 @@ def process(verb, path, data=nil, query={}) Firebase::Response.new @request.request(verb, "#{path}.json", { :body => data.to_json, :query => (@secret ? { :auth => @secret }.merge(query) : query), + :header => header, :follow_redirect => true }) end diff --git a/lib/firebase/response.rb b/lib/firebase/response.rb index 55e5f3c..8b63559 100644 --- a/lib/firebase/response.rb +++ b/lib/firebase/response.rb @@ -21,5 +21,9 @@ def success? def code response.status end + + def etag + response.headers['ETag'] + end end end diff --git a/spec/firebase_spec.rb b/spec/firebase_spec.rb index 94479be..a8754eb 100644 --- a/spec/firebase_spec.rb +++ b/spec/firebase_spec.rb @@ -26,14 +26,14 @@ describe "set" do it "writes and returns the data" do - expect(@firebase).to receive(:process).with(:put, 'users/info', data, {}) + expect(@firebase).to receive(:process).with(:put, 'users/info', data, {}, {}) @firebase.set('users/info', data) end end describe "get" do it "returns the data" do - expect(@firebase).to receive(:process).with(:get, 'users/info', nil, {}) + expect(@firebase).to receive(:process).with(:get, 'users/info', nil, {}, {}) @firebase.get('users/info') end @@ -42,7 +42,7 @@ :orderBy => '"$key"', :startAt => '"A1"' } - expect(@firebase).to receive(:process).with(:get, 'users/info', nil, params) + expect(@firebase).to receive(:process).with(:get, 'users/info', nil, params, {}) @firebase.get('users/info', params) end @@ -73,14 +73,14 @@ describe "push" do it "writes the data" do - expect(@firebase).to receive(:process).with(:post, 'users', data, {}) + expect(@firebase).to receive(:process).with(:post, 'users', data, {}, {}) @firebase.push('users', data) end end describe "delete" do it "returns true" do - expect(@firebase).to receive(:process).with(:delete, 'users/info', nil, {}) + expect(@firebase).to receive(:process).with(:delete, 'users/info', nil, {}, {}) @firebase.delete('users/info') end end @@ -92,12 +92,80 @@ end end + describe '#transaction' do + let(:etag_value) { 'i0Ir/zYOL6grKBc07+n2ncm/6as=' } + let(:new_data) { {'name' => 'Oscar Wilde'} } + let(:path) { 'users/info' } + let(:mock_etag_response) do + Firebase::Response.new(double({ + :body => data.to_json, + :status => 200, + :headers => { + 'ETag' => etag_value + } + })) + end + let(:mock_set_success_response) do + Firebase::Response.new(double({ + :body => new_data.to_json, + :status => 200 + })) + end + + after(:each) do |example| + block_data = nil + resp = @firebase.transaction(path, max_retries: example.metadata[:max_retries]) do |data| + block_data = JSON.parse data.to_json + new_data + end + expect(block_data).to eql(data) + if example.metadata[:max_retries].nil? + expect(resp.body).to eql(new_data) + else + expect(resp.body).to eql(data) + expect(resp.success?).to eql(false) + end + end + + context "data at path does not change" do + it 'updates and returns the data' do + expect(@firebase).to receive(:get).with(path, {}, { "X-Firebase-ETag": true }).and_return(mock_etag_response) + expect(@firebase).to receive(:set).with(path, new_data, {}, { "if-match": etag_value }).and_return(mock_set_success_response) + end + end + + context "data at path changes" do + let(:new_etag_value) { 'i0Ir/zYOL6grKBc09+n2ncm/7as=' } + + before(:each) do + mock_set_error_response = Firebase::Response.new(double({ + :body => data.to_json, + :status => 412, + :headers => { + 'ETag' => new_etag_value + } + })) + expect(@firebase).to receive(:get).with(path, {}, { "X-Firebase-ETag": true }).and_return(mock_etag_response) + expect(@firebase).to receive(:set).with(path, new_data, {}, { "if-match": etag_value }).and_return(mock_set_error_response) + end + + it "retries incase the data at path changes in the meantime" do + expect(@firebase).to receive(:set).with(path, new_data, {}, { "if-match": new_etag_value }).and_return(mock_set_success_response) + end + + it "does not retry after max_retries is reached", max_retries: 0 do + expect(@firebase).not_to receive(:set).with(path, new_data, {}, { "if-match": new_etag_value }) + end + end + end + describe "http processing" do it "sends custom auth query" do firebase = Firebase::Client.new('https://test.firebaseio.com', 'secret') expect(firebase.request).to receive(:request).with(:get, "todos.json", { :body => nil, :query => {:auth => "secret", :foo => 'bar'}, + :header => {}, :follow_redirect => true }) firebase.get('todos', :foo => 'bar')