synamoid 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --format documentation
2
- --color
@@ -1,15 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - ruby-2.0.0-p648
4
- - ruby-2.1.10
5
- - ruby-2.2.5
6
- - ruby-2.3.1
7
- - jruby-9.1.4.0
8
- before_install: gem install bundler -v 1.12.5
9
- install:
10
- - wget http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.zip --quiet -O spec/dynamodb_temp.zip
11
- - unzip -qq spec/dynamodb_temp.zip -d spec/DynamoDBLocal-latest
12
- - rm spec/dynamodb_temp.zip
13
- script:
14
- - bundle install
15
- - bundle exec rake unattended_spec
@@ -1,48 +0,0 @@
1
- # 1.2.1
2
-
3
- * Remove accidental Gemfile.lock; fix .gitignore (#95, @pboling)
4
- * Allow options to put_items (#95, @alexperto)
5
- * Support range key in secondary index queries (#95, @pboling)
6
- * Better handling of options generally (#95, @pboling)
7
- * Support for batch_delete_item API (#95, @pboling)
8
- * Support for batch_write_item API (#95, @alexperto)
9
-
10
- # 1.2.0
11
-
12
- * Add create_table_syncronously, and sync: option to regular create_table (@pboling)
13
- * make required for tables created with secondary indexes
14
- * Expose and fix truncate method on adapter (#52, @pcorpet)
15
- * Enable saving without updating timestamps (#58, @cignoir)
16
- * Fix projected attributes by checking for :include (#56, @yoshida_tetsuhiro)
17
- * Make behavior of association where method closer to AR by cloning instead of modifying (#51, @pcorpet)
18
- * Add boolean field presence validator (#50, @pcorpet)
19
- * Add association build method (#49, @pcorpet)
20
- * Fix association create method (#47, #48, @pcorpet)
21
- * Support range_between (#42, @ayemos)
22
- * Fix problems with range query (#42, @ayemos)
23
- * Don't prefix table names when namespace is nil (#40, @brenden)
24
- * Added basic secondary index support (#34, @sumocoder)
25
- * Fix query attribute behavior for booleans (#35, @amirmanji)
26
- * Ignore unknown fields on model initialize (PR #33, @sumocoder)
27
-
28
- # 1.1.0
29
-
30
- * Added support for optimistic locking on delete (PR #29, @sumocoder)
31
- * upgrade concurrent-ruby requirement to 1.0 (PR #31, @keithmgould)
32
-
33
- # 1.0.0
34
-
35
- * Add support for AWS SDK v2.
36
- * Add support for custom class type for fields.
37
- * Remove partitioning support.
38
- * Remove support for Dynamoid's (pseudo)indexes, now that DynamoDB offers
39
- local and global indexes.
40
- * Rename :float field type to :number.
41
- * Rename Chain#limit to Chain#eval_limit.
42
-
43
- Housekeeping:
44
-
45
- * Switch from `fake_dynamo` for unit tests to DynamoDBLocal. This is the new authoritative
46
- implementation of DynamoDB for testing, and it supports AWS SDK v2.
47
- * Use Travis CI to auto-run unit tests on multiple Rubies.
48
- * Randomize spec order.
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in dynamoid.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1,64 +0,0 @@
1
- require "bundler/gem_tasks"
2
-
3
- begin
4
- Bundler.setup(:default, :development)
5
- rescue Bundler::BundlerError => e
6
- $stderr.puts e.message
7
- $stderr.puts "Run `bundle install` to install missing gems"
8
- exit e.status_code
9
- end
10
-
11
- require "rake"
12
- require "rspec/core/rake_task"
13
- RSpec::Core::RakeTask.new(:spec) do |spec|
14
- spec.pattern = FileList["spec/**/*_spec.rb"]
15
- end
16
-
17
- RSpec::Core::RakeTask.new(:rcov) do |spec|
18
- spec.pattern = "spec/**/*_spec.rb"
19
- spec.rcov = true
20
- end
21
-
22
- desc "Start DynamoDBLocal, run tests, clean up"
23
- task :unattended_spec do |t|
24
-
25
- if system("bin/start_dynamodblocal")
26
- puts "DynamoDBLocal started; proceeding with specs."
27
- else
28
- raise "Unable to start DynamoDBLocal. Cannot run unattended specs."
29
- end
30
-
31
- #Cleanup
32
- at_exit do
33
- unless system("bin/stop_dynamodblocal")
34
- $stderr.puts "Unable to cleanly stop DynamoDBLocal."
35
- end
36
- end
37
-
38
- Rake::Task["spec"].invoke
39
- end
40
-
41
- require "yard"
42
- YARD::Rake::YardocTask.new do |t|
43
- t.files = ["lib/**/*.rb", "README", "LICENSE"] # optional
44
- t.options = ["-m", "markdown"] # optional
45
- end
46
-
47
- desc "Publish documentation to gh-pages"
48
- task :publish do
49
- Rake::Task["yard"].invoke
50
- `git add .`
51
- `git commit -m 'Regenerated documentation'`
52
- `git checkout gh-pages`
53
- `git clean -fdx`
54
- `git checkout master -- doc`
55
- `cp -R doc/* .`
56
- `git rm -rf doc/`
57
- `git add .`
58
- `git commit -m 'Regenerated documentation'`
59
- `git pull`
60
- `git push`
61
- `git checkout master`
62
- end
63
-
64
- task :default => :spec
@@ -1,53 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "dynamoid/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "synamoid"
8
- spec.version = Dynamoid::VERSION
9
-
10
- # Keep in sync with README
11
- spec.authors = [
12
- "Josh Symonds",
13
- "Logan Bowers",
14
- "Craig Heneveld",
15
- "Anatha Kumaran",
16
- "Jason Dew",
17
- "Luis Arias",
18
- "Stefan Neculai",
19
- "Philip White",
20
- "Peeyush Kumar",
21
- "Sumanth Ravipati",
22
- "Pascal Corpet",
23
- "Brian Glusman",
24
- "Peter Boling"
25
- ]
26
- spec.email = ["peter.boling@gmail.com", "brian@stellaservice.com"]
27
-
28
- spec.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
29
- spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
30
- spec.extra_rdoc_files = [
31
- "LICENSE.txt",
32
- "README.md"
33
- ]
34
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features)/}) }
35
- spec.homepage = "http://github.com/Dynamoid/Dynamoid"
36
- spec.licenses = ["MIT"]
37
- spec.bindir = "exe"
38
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
- spec.require_paths = ["lib"]
40
-
41
- spec.add_runtime_dependency(%q<activemodel>, ["~> 5"])
42
- spec.add_runtime_dependency(%q<activemodel-serializers-xml>, ["~> 1"])
43
- spec.add_runtime_dependency(%q<aws-sdk-resources>, ["~> 2"])
44
- spec.add_runtime_dependency(%q<concurrent-ruby>, [">= 1.0"])
45
- spec.add_development_dependency(%q<rake>, [">= 10"])
46
- spec.add_development_dependency(%q<bundler>, ["~> 1.12"])
47
- spec.add_development_dependency(%q<rspec>, [">= 3"])
48
- spec.add_development_dependency(%q<yard>, [">= 0"])
49
- spec.add_development_dependency(%q<github-markup>, [">= 0"])
50
- spec.add_development_dependency(%q<pry>, [">= 0"])
51
- spec.add_development_dependency(%q<coveralls>, [">= 0"])
52
- spec.add_development_dependency(%q<rspec-retry>, [">= 0"])
53
- end
@@ -1,53 +0,0 @@
1
- require "delegate"
2
- require "time"
3
- require "securerandom"
4
- require "active_support"
5
- require "active_support/core_ext"
6
- require "active_support/json"
7
- require "active_support/inflector"
8
- require "active_support/lazy_load_hooks"
9
- require "active_support/time_with_zone"
10
- require "active_model"
11
- require 'activemodel-serializers-xml'
12
- #gem 'active_model_serializers'
13
-
14
- require "dynamoid/version"
15
- require "dynamoid/errors"
16
- require "dynamoid/fields"
17
- require "dynamoid/indexes"
18
- require "dynamoid/associations"
19
- require "dynamoid/persistence"
20
- require "dynamoid/dirty"
21
- require "dynamoid/validations"
22
- require "dynamoid/criteria"
23
- require "dynamoid/finders"
24
- require "dynamoid/identity_map"
25
- require "dynamoid/config"
26
- require "dynamoid/components"
27
- require "dynamoid/document"
28
- require "dynamoid/adapter"
29
-
30
- require "dynamoid/middleware/identity_map"
31
-
32
- module Dynamoid
33
- extend self
34
-
35
- MAX_ITEM_SIZE = 65_536
36
-
37
- def configure
38
- block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
39
- end
40
- alias :config :configure
41
-
42
- def logger
43
- Dynamoid::Config.logger
44
- end
45
-
46
- def included_models
47
- @included_models ||= []
48
- end
49
-
50
- def adapter
51
- @adapter ||= Adapter.new
52
- end
53
- end
@@ -1,190 +0,0 @@
1
- # require only 'concurrent/atom' once this issue is resolved:
2
- # https://github.com/ruby-concurrency/concurrent-ruby/pull/377
3
- require 'concurrent'
4
-
5
- # encoding: utf-8
6
- module Dynamoid
7
-
8
- # Adapter's value-add:
9
- # 1) For the rest of Dynamoid, the gateway to DynamoDB.
10
- # 2) Allows switching `config.adapter` to ease development of a new adapter.
11
- # 3) Caches the list of tables Dynamoid knows about.
12
- class Adapter
13
- def initialize
14
- @adapter_ = Concurrent::Atom.new(nil)
15
- @tables_ = Concurrent::Atom.new(nil)
16
- end
17
-
18
- def tables
19
- if !@tables_.value
20
- @tables_.swap{|value, args| benchmark('Cache Tables') { list_tables } }
21
- end
22
- @tables_.value
23
- end
24
-
25
- # The actual adapter currently in use.
26
- #
27
- # @since 0.2.0
28
- def adapter
29
- if !@adapter_.value
30
- adapter = self.class.adapter_plugin_class.new
31
- adapter.connect! if adapter.respond_to?(:connect!)
32
- @adapter_.compare_and_set(nil, adapter)
33
- clear_cache!
34
- end
35
- @adapter_.value
36
- end
37
-
38
- def clear_cache!
39
- @tables_.swap{|value, args| nil}
40
- end
41
-
42
- # Shows how long it takes a method to run on the adapter. Useful for generating logged output.
43
- #
44
- # @param [Symbol] method the name of the method to appear in the log
45
- # @param [Array] args the arguments to the method to appear in the log
46
- # @yield the actual code to benchmark
47
- #
48
- # @return the result of the yield
49
- #
50
- # @since 0.2.0
51
- def benchmark(method, *args)
52
- start = Time.now
53
- result = yield
54
- Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
55
- return result
56
- end
57
-
58
- # Write an object to the adapter.
59
- #
60
- # @param [String] table the name of the table to write the object to
61
- # @param [Object] object the object itself
62
- # @param [Hash] options Options that are passed to the put_item call
63
- #
64
- # @return [Object] the persisted object
65
- #
66
- # @since 0.2.0
67
- def write(table, object, options = nil)
68
- put_item(table, object, options)
69
- end
70
-
71
- # Read one or many keys from the selected table.
72
- # This method intelligently calls batch_get or get on the underlying adapter
73
- # depending on whether ids is a range or a single key.
74
- # If a range key is present, it will also interpolate that into the ids so
75
- # that the batch get will acquire the correct record.
76
- #
77
- # @param [String] table the name of the table to write the object to
78
- # @param [Array] ids to fetch, can also be a string of just one id
79
- # @param [Hash] options: Passed to the underlying query. The :range_key option is required whenever the table has a range key,
80
- # unless multiple ids are passed in.
81
- #
82
- # @since 0.2.0
83
- def read(table, ids, options = {})
84
- range_key = options.delete(:range_key)
85
-
86
- if ids.respond_to?(:each)
87
- ids = ids.collect{|id| range_key ? [id, range_key] : id}
88
- batch_get_item({table => ids}, options)
89
- else
90
- options[:range_key] = range_key if range_key
91
- get_item(table, ids, options)
92
- end
93
- end
94
-
95
- # Delete an item from a table.
96
- #
97
- # @param [String] table the name of the table to write the object to
98
- # @param [Array] ids to delete, can also be a string of just one id
99
- # @param [Array] range_key of the record to delete, can also be a string of just one range_key
100
- #
101
- def delete(table, ids, options = {})
102
- range_key = options[:range_key] #array of range keys that matches the ids passed in
103
- if ids.respond_to?(:each)
104
- if range_key.respond_to?(:each)
105
- #turn ids into array of arrays each element being hash_key, range_key
106
- ids = ids.each_with_index.map{|id,i| [id,range_key[i]]}
107
- else
108
- ids = range_key ? [[ids, range_key]] : ids
109
- end
110
-
111
- batch_delete_item(table => ids)
112
- else
113
- delete_item(table, ids, options)
114
- end
115
- end
116
-
117
- # Scans a table. Generally quite slow; try to avoid using scan if at all possible.
118
- #
119
- # @param [String] table the name of the table to write the object to
120
- # @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
121
- #
122
- # @since 0.2.0
123
- def scan(table, query, opts = {})
124
- benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
125
- end
126
-
127
- def create_table(table_name, key, options = {})
128
- if !tables.include?(table_name)
129
- benchmark('Create Table') { adapter.create_table(table_name, key, options) }
130
- tables << table_name
131
- end
132
- end
133
-
134
- # @since 0.2.0
135
- def delete_table(table_name, options = {})
136
- if tables.include?(table_name)
137
- benchmark('Delete Table') { adapter.delete_table(table_name, options) }
138
- idx = tables.index(table_name)
139
- tables.delete_at(idx)
140
- end
141
- end
142
-
143
- [:batch_get_item, :delete_item, :get_item, :list_tables, :put_item, :truncate, :batch_write_item, :batch_delete_item].each do |m|
144
- # Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
145
- #
146
- # @since 0.2.0
147
- define_method(m) do |*args|
148
- benchmark("#{m.to_s}", args) {adapter.send(m, *args)}
149
- end
150
- end
151
-
152
- # Delegate all methods that aren't defind here to the underlying adapter.
153
- #
154
- # @since 0.2.0
155
- def method_missing(method, *args, &block)
156
- return benchmark(method, *args) {adapter.send(method, *args, &block)} if adapter.respond_to?(method)
157
- super
158
- end
159
-
160
- # Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
161
- # only really useful for range queries, since it can only find by one hash key at once. Only provide
162
- # one range key to the hash.
163
- #
164
- # @param [String] table_name the name of the table
165
- # @param [Hash] opts the options to query the table with
166
- # @option opts [String] :hash_value the value of the hash key to find
167
- # @option opts [Range] :range_value find the range key within this range
168
- # @option opts [Number] :range_greater_than find range keys greater than this
169
- # @option opts [Number] :range_less_than find range keys less than this
170
- # @option opts [Number] :range_gte find range keys greater than or equal to this
171
- # @option opts [Number] :range_lte find range keys less than or equal to this
172
- #
173
- # @return [Array] an array of all matching items
174
- #
175
- def query(table_name, opts = {})
176
- adapter.query(table_name, opts)
177
- end
178
-
179
- private
180
-
181
- def self.adapter_plugin_class
182
- unless Dynamoid.const_defined?(:AdapterPlugin) && Dynamoid::AdapterPlugin.const_defined?(Dynamoid::Config.adapter.camelcase)
183
- require "dynamoid/adapter_plugin/#{Dynamoid::Config.adapter}"
184
- end
185
-
186
- Dynamoid::AdapterPlugin.const_get(Dynamoid::Config.adapter.camelcase)
187
- end
188
-
189
- end
190
- end
@@ -1,892 +0,0 @@
1
- module Dynamoid
2
- module AdapterPlugin
3
-
4
- # The AwsSdkV2 adapter provides support for the aws-sdk version 2 for ruby.
5
- class AwsSdkV2
6
- EQ = "EQ".freeze
7
- RANGE_MAP = {
8
- range_greater_than: 'GT',
9
- range_less_than: 'LT',
10
- range_gte: 'GE',
11
- range_lte: 'LE',
12
- range_begins_with: 'BEGINS_WITH',
13
- range_between: 'BETWEEN',
14
- range_eq: 'EQ'
15
- }
16
- HASH_KEY = "HASH".freeze
17
- RANGE_KEY = "RANGE".freeze
18
- STRING_TYPE = "S".freeze
19
- NUM_TYPE = "N".freeze
20
- BINARY_TYPE = "B".freeze
21
- TABLE_STATUSES = {
22
- creating: "CREATING",
23
- updating: "UPDATING",
24
- deleting: "DELETING",
25
- active: "ACTIVE"
26
- }.freeze
27
- PARSE_TABLE_STATUS = ->(resp, lookup = :table) {
28
- # lookup is table for describe_table API
29
- # lookup is table_description for create_table API
30
- # because Amazon, damnit.
31
- resp.send(lookup).table_status
32
- }
33
- attr_reader :table_cache
34
-
35
- # Establish the connection to DynamoDB.
36
- #
37
- # @return [Aws::DynamoDB::Client] the DynamoDB connection
38
- def connect!
39
- @client = if Dynamoid::Config.endpoint?
40
- Aws::DynamoDB::Client.new(endpoint: Dynamoid::Config.endpoint)
41
- else
42
- Aws::DynamoDB::Client.new
43
- end
44
- @table_cache = {}
45
- end
46
-
47
- # Return the client object.
48
- #
49
- # @since 1.0.0
50
- def client
51
- @client
52
- end
53
-
54
- # Puts or deletes multiple items in one or more tables
55
- #
56
- # @param [String] table_name the name of the table
57
- # @param [Array] items to be processed
58
- # @param [Hash] additional options
59
- #
60
- #See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
61
- def batch_write_item table_name, objects, options = {}
62
- request_items = []
63
- options ||= {}
64
- objects.each do |o|
65
- request_items << { "put_request" => { item: o } }
66
- end
67
-
68
- begin
69
- client.batch_write_item(
70
- {
71
- request_items: {
72
- table_name => request_items,
73
- },
74
- return_consumed_capacity: "TOTAL",
75
- return_item_collection_metrics: "SIZE"
76
- }.merge!(options)
77
- )
78
- rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
79
- raise Dynamoid::Errors::ConditionalCheckFailedException, e
80
- end
81
- end
82
-
83
- # Get many items at once from DynamoDB. More efficient than getting each item individually.
84
- #
85
- # @example Retrieve IDs 1 and 2 from the table testtable
86
- # Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item({'table1' => ['1', '2']})
87
- #
88
- # @param [Hash] table_ids the hash of tables and IDs to retrieve
89
- # @param [Hash] options to be passed to underlying BatchGet call
90
- #
91
- # @return [Hash] a hash where keys are the table names and the values are the retrieved items
92
- #
93
- # @since 1.0.0
94
- #
95
- # @todo: Provide support for passing options to underlying batch_get_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
96
- def batch_get_item(table_ids, options = {})
97
- request_items = Hash.new{|h, k| h[k] = []}
98
- return request_items if table_ids.all?{|k, v| v.empty?}
99
-
100
- table_ids.each do |t, ids|
101
- next if ids.empty?
102
- tbl = describe_table(t)
103
- hk = tbl.hash_key.to_s
104
- rng = tbl.range_key.to_s
105
-
106
- keys = if rng.present?
107
- Array(ids).map do |h,r|
108
- { hk => h, rng => r }
109
- end
110
- else
111
- Array(ids).map do |id|
112
- { hk => id }
113
- end
114
- end
115
-
116
- request_items[t] = {
117
- keys: keys
118
- }
119
- end
120
-
121
- results = client.batch_get_item(
122
- request_items: request_items
123
- )
124
-
125
- ret = Hash.new([].freeze) # Default for tables where no rows are returned
126
- results.data[:responses].each do |table, rows|
127
- ret[table] = rows.collect { |r| result_item_to_hash(r) }
128
- end
129
- ret
130
- end
131
-
132
- # Delete many items at once from DynamoDB. More efficient than delete each item individually.
133
- #
134
- # @example Delete IDs 1 and 2 from the table testtable
135
- # Dynamoid::AdapterPlugin::AwsSdk.batch_delete_item('table1' => ['1', '2'])
136
- #or
137
- # Dynamoid::AdapterPlugin::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
138
- #
139
- # @param [Hash] options the hash of tables and IDs to delete
140
- #
141
- # @return nil
142
- #
143
- # @todo: Provide support for passing options to underlying delete_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
144
- def batch_delete_item(options)
145
- options.each_pair do |table_name, ids|
146
- table = describe_table(table_name)
147
- ids.each do |id|
148
- client.delete_item(table_name: table_name, key: key_stanza(table, *id))
149
- end
150
- end
151
- nil
152
- end
153
-
154
- # Create a table on DynamoDB. This usually takes a long time to complete.
155
- #
156
- # @param [String] table_name the name of the table to create
157
- # @param [Symbol] key the table's primary key (defaults to :id)
158
- # @param [Hash] options provide a range key here if the table has a composite key
159
- # @option options [Array<Dynamoid::Indexes::Index>] local_secondary_indexes
160
- # @option options [Array<Dynamoid::Indexes::Index>] global_secondary_indexes
161
- # @option options [Symbol] hash_key_type The type of the hash key
162
- # @option options [Boolean] sync Wait for table status to be ACTIVE?
163
- # @since 1.0.0
164
- def create_table(table_name, key = :id, options = {})
165
- Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
166
- read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
167
- write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
168
-
169
- secondary_indexes = options.slice(
170
- :local_secondary_indexes,
171
- :global_secondary_indexes
172
- )
173
- ls_indexes = options[:local_secondary_indexes]
174
- gs_indexes = options[:global_secondary_indexes]
175
-
176
- key_schema = {
177
- :hash_key_schema => { key => (options[:hash_key_type] || :string) },
178
- :range_key_schema => options[:range_key]
179
- }
180
- attribute_definitions = build_all_attribute_definitions(
181
- key_schema,
182
- secondary_indexes
183
- )
184
- key_schema = aws_key_schema(
185
- key_schema[:hash_key_schema],
186
- key_schema[:range_key_schema]
187
- )
188
-
189
- client_opts = {
190
- table_name: table_name,
191
- provisioned_throughput: {
192
- read_capacity_units: read_capacity,
193
- write_capacity_units: write_capacity
194
- },
195
- key_schema: key_schema,
196
- attribute_definitions: attribute_definitions
197
- }
198
-
199
- if ls_indexes.present?
200
- client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
201
- index_to_aws_hash(index)
202
- end
203
- end
204
-
205
- if gs_indexes.present?
206
- client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
207
- index_to_aws_hash(index)
208
- end
209
- end
210
- resp = client.create_table(client_opts)
211
- options[:sync] = true if !options.has_key?(:sync) && ls_indexes.present? || gs_indexes.present?
212
- until_past_table_status(table_name) if options[:sync] &&
213
- (status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
214
- status != TABLE_STATUSES[:creating]
215
- # Response to original create_table, which, if options[:sync]
216
- # may have an outdated table_description.table_status of "CREATING"
217
- resp
218
- rescue Aws::DynamoDB::Errors::ResourceInUseException => e
219
- Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
220
- end
221
-
222
- # Create a table on DynamoDB *synchronously*.
223
- # This usually takes a long time to complete.
224
- # CreateTable is normally an asynchronous operation.
225
- # You can optionally define secondary indexes on the new table,
226
- # as part of the CreateTable operation.
227
- # If you want to create multiple tables with secondary indexes on them,
228
- # you must create the tables sequentially.
229
- # Only one table with secondary indexes can be
230
- # in the CREATING state at any given time.
231
- # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#create_table-instance_method
232
- #
233
- # @param [String] table_name the name of the table to create
234
- # @param [Symbol] key the table's primary key (defaults to :id)
235
- # @param [Hash] options provide a range key here if the table has a composite key
236
- # @option options [Array<Dynamoid::Indexes::Index>] local_secondary_indexes
237
- # @option options [Array<Dynamoid::Indexes::Index>] global_secondary_indexes
238
- # @option options [Symbol] hash_key_type The type of the hash key
239
- # @since 1.2.0
240
- def create_table_synchronously(table_name, key = :id, options = {})
241
- create_table(table_name, key, options.merge(sync: true))
242
- end
243
-
244
- # Removes an item from DynamoDB.
245
- #
246
- # @param [String] table_name the name of the table
247
- # @param [String] key the hash key of the item to delete
248
- # @param [Hash] options provide a range key here if the table has a composite key
249
- #
250
- # @since 1.0.0
251
- #
252
- # @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
253
- def delete_item(table_name, key, options = {})
254
- options ||= {}
255
- range_key = options[:range_key]
256
- conditions = options[:conditions]
257
- table = describe_table(table_name)
258
- client.delete_item(
259
- table_name: table_name,
260
- key: key_stanza(table, key, range_key),
261
- expected: expected_stanza(conditions)
262
- )
263
- rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
264
- raise Dynamoid::Errors::ConditionalCheckFailedException, e
265
- end
266
-
267
- # Deletes an entire table from DynamoDB.
268
- #
269
- # @param [String] table_name the name of the table to destroy
270
- # @option options [Boolean] sync Wait for table status check to raise ResourceNotFoundException
271
- #
272
- # @since 1.0.0
273
- def delete_table(table_name, options = {})
274
- resp = client.delete_table(table_name: table_name)
275
- until_past_table_status(table_name, :deleting) if options[:sync] &&
276
- (status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
277
- status != TABLE_STATUSES[:deleting]
278
- table_cache.delete(table_name)
279
- rescue Aws::DynamoDB::Errors::ResourceInUseException => e
280
- Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
281
- raise e
282
- end
283
-
284
- def delete_table_synchronously(table_name, options = {})
285
- delete_table(table_name, options.merge(sync: true))
286
- end
287
-
288
- # @todo Add a DescribeTable method.
289
-
290
- # Fetches an item from DynamoDB.
291
- #
292
- # @param [String] table_name the name of the table
293
- # @param [String] key the hash key of the item to find
294
- # @param [Hash] options provide a range key here if the table has a composite key
295
- #
296
- # @return [Hash] a hash representing the raw item in DynamoDB
297
- #
298
- # @since 1.0.0
299
- #
300
- # @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#get_item-instance_method
301
- def get_item(table_name, key, options = {})
302
- options ||= {}
303
- table = describe_table(table_name)
304
- range_key = options.delete(:range_key)
305
-
306
- item = client.get_item(table_name: table_name,
307
- key: key_stanza(table, key, range_key)
308
- )[:item]
309
- item ? result_item_to_hash(item) : nil
310
- end
311
-
312
- # Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values
313
- #
314
- # @param [String] table_name the name of the table
315
- # @param [String] key the hash key of the item to find
316
- # @param [Hash] options provide a range key here if the table has a composite key
317
- #
318
- # @return new attributes for the record
319
- #
320
- # @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
321
- def update_item(table_name, key, options = {})
322
- range_key = options.delete(:range_key)
323
- conditions = options.delete(:conditions)
324
- table = describe_table(table_name)
325
-
326
- yield(iu = ItemUpdater.new(table, key, range_key))
327
-
328
- raise "non-empty options: #{options}" unless options.empty?
329
- begin
330
- result = client.update_item(table_name: table_name,
331
- key: key_stanza(table, key, range_key),
332
- attribute_updates: iu.to_h,
333
- expected: expected_stanza(conditions),
334
- return_values: "ALL_NEW"
335
- )
336
- result_item_to_hash(result[:attributes])
337
- rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
338
- raise Dynamoid::Errors::ConditionalCheckFailedException, e
339
- end
340
- end
341
-
342
- # List all tables on DynamoDB.
343
- #
344
- # @since 1.0.0
345
- #
346
- # @todo Provide limit support http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
347
- def list_tables
348
- client.list_tables[:table_names]
349
- end
350
-
351
- # Persists an item on DynamoDB.
352
- #
353
- # @param [String] table_name the name of the table
354
- # @param [Object] object a hash or Dynamoid object to persist
355
- #
356
- # @since 1.0.0
357
- #
358
- # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
359
- def put_item(table_name, object, options = {})
360
- item = {}
361
- options ||= {}
362
-
363
- object.each do |k, v|
364
- next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
365
- item[k.to_s] = v
366
- end
367
-
368
- begin
369
- client.put_item(
370
- {
371
- table_name: table_name,
372
- item: item,
373
- expected: expected_stanza(options)
374
- }.merge!(options)
375
- )
376
- rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
377
- raise Dynamoid::Errors::ConditionalCheckFailedException, e
378
- end
379
- end
380
-
381
- # Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
382
- # only really useful for range queries, since it can only find by one hash key at once. Only provide
383
- # one range key to the hash.
384
- #
385
- # @param [String] table_name the name of the table
386
- # @param [Hash] opts the options to query the table with
387
- # @option opts [String] :hash_value the value of the hash key to find
388
- # @option opts [Number, Number] :range_between find the range key within this range
389
- # @option opts [Number] :range_greater_than find range keys greater than this
390
- # @option opts [Number] :range_less_than find range keys less than this
391
- # @option opts [Number] :range_gte find range keys greater than or equal to this
392
- # @option opts [Number] :range_lte find range keys less than or equal to this
393
- #
394
- # @return [Enumerable] matching items
395
- #
396
- # @since 1.0.0
397
- #
398
- # @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
399
- def query(table_name, opts = {})
400
- table = describe_table(table_name)
401
- hk = (opts[:hash_key].present? ? opts[:hash_key] : table.hash_key).to_s
402
- rng = (opts[:range_key].present? ? opts[:range_key] : table.range_key).to_s
403
- q = opts.slice(
404
- :consistent_read,
405
- :scan_index_forward,
406
- :limit,
407
- :select,
408
- :index_name
409
- )
410
-
411
- opts.delete(:consistent_read)
412
- opts.delete(:scan_index_forward)
413
- opts.delete(:limit)
414
- opts.delete(:select)
415
- opts.delete(:index_name)
416
-
417
- opts.delete(:next_token).tap do |token|
418
- break unless token
419
- q[:exclusive_start_key] = {
420
- hk => token[:hash_key_element],
421
- rng => token[:range_key_element]
422
- }
423
- end
424
-
425
- key_conditions = {
426
- hk => {
427
- # TODO: Provide option for other operators like NE, IN, LE, etc
428
- comparison_operator: EQ,
429
- attribute_value_list: [
430
- opts.delete(:hash_value).freeze
431
- ]
432
- }
433
- }
434
-
435
- opts.each_pair do |k, v|
436
- # TODO: ATM, only few comparison operators are supported, provide support for all operators
437
- next unless(op = RANGE_MAP[k])
438
- key_conditions[rng] = {
439
- comparison_operator: op,
440
- attribute_value_list: [
441
- opts.delete(k).freeze
442
- ].flatten # Flatten as BETWEEN operator specifies array of two elements
443
- }
444
- end
445
-
446
- q[:table_name] = table_name
447
- q[:key_conditions] = key_conditions
448
-
449
- Enumerator.new { |y|
450
- loop do
451
- results = client.query(q)
452
- results.items.each { |row| y << result_item_to_hash(row) }
453
-
454
- if(lk = results.last_evaluated_key)
455
- q[:exclusive_start_key] = lk
456
- else
457
- break
458
- end
459
- end
460
- }
461
- end
462
-
463
- # Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on
464
- # the DynamoDB servers.
465
- #
466
- # @param [String] table_name the name of the table
467
- # @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
468
- #
469
- # @return [Enumerable] matching items
470
- #
471
- # @since 1.0.0
472
- #
473
- # @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
474
- def scan(table_name, scan_hash, select_opts = {})
475
- limit = select_opts.delete(:limit)
476
- batch = select_opts.delete(:batch_size)
477
-
478
- request = { table_name: table_name }
479
- request[:limit] = batch || limit if batch || limit
480
- request[:scan_filter] = scan_hash.reduce({}) do |memo, kvp|
481
- memo[kvp[0].to_s] = {
482
- attribute_value_list: [kvp[1]],
483
- # TODO: Provide support for all comparison operators
484
- comparison_operator: EQ
485
- }
486
- memo
487
- end if scan_hash.present?
488
-
489
- Enumerator.new do |y|
490
- # Batch loop, pulls multiple requests until done using the start_key
491
- loop do
492
- results = client.scan(request)
493
-
494
- results.data[:items].each { |row| y << result_item_to_hash(row) }
495
-
496
- if((lk = results[:last_evaluated_key]) && batch)
497
- request[:exclusive_start_key] = lk
498
- else
499
- break
500
- end
501
- end
502
- end
503
- end
504
-
505
- #
506
- # Truncates all records in the given table
507
- #
508
- # @param [String] table_name the name of the table
509
- #
510
- # @since 1.0.0
511
- def truncate(table_name)
512
- table = describe_table(table_name)
513
- hk = table.hash_key
514
- rk = table.range_key
515
-
516
- scan(table_name, {}, {}).each do |attributes|
517
- opts = {}
518
- opts[:range_key] = attributes[rk.to_sym] if rk
519
- delete_item(table_name, attributes[hk], opts)
520
- end
521
- end
522
-
523
- def count(table_name)
524
- describe_table(table_name, true).item_count
525
- end
526
-
527
- protected
528
-
529
- def check_table_status?(counter, resp, expect_status)
530
- status = PARSE_TABLE_STATUS.call(resp)
531
- again = counter < Dynamoid::Config.sync_retry_max_times &&
532
- status == TABLE_STATUSES[expect_status]
533
- {again: again, status: status, counter: counter}
534
- end
535
-
536
- def until_past_table_status(table_name, status = :creating)
537
- counter = 0
538
- resp = nil
539
- begin
540
- check = {again: true}
541
- while check[:again]
542
- sleep Dynamoid::Config.sync_retry_wait_seconds
543
- resp = client.describe_table({ table_name: table_name })
544
- check = check_table_status?(counter, resp, status)
545
- Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
546
- counter += 1
547
- end
548
- # If you issue a DescribeTable request immediately after a CreateTable
549
- # request, DynamoDB might return a ResourceNotFoundException.
550
- # This is because DescribeTable uses an eventually consistent query,
551
- # and the metadata for your table might not be available at that moment.
552
- # Wait for a few seconds, and then try the DescribeTable request again.
553
- # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method
554
- rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
555
- case status
556
- when :creating then
557
- if counter >= Dynamoid::Config.sync_retry_max_times
558
- Dynamoid.logger.warn "Waiting on table metadata for #{table_name} (check #{counter})"
559
- retry # start over at first line of begin, does not reset counter
560
- else
561
- Dynamoid.logger.error "Exhausted max retries (Dynamoid::Config.sync_retry_max_times) waiting on table metadata for #{table_name} (check #{counter})"
562
- raise e
563
- end
564
- else
565
- # When deleting a table, "not found" is the goal.
566
- Dynamoid.logger.info "Checked table status for #{table_name}: Not Found (check #{check.inspect})"
567
- end
568
- end
569
- end
570
-
571
- #Converts from symbol to the API string for the given data type
572
- # E.g. :number -> 'N'
573
- def api_type(type)
574
- case(type)
575
- when :string then STRING_TYPE
576
- when :number then NUM_TYPE
577
- when :binary then BINARY_TYPE
578
- else raise "Unknown type: #{type}"
579
- end
580
- end
581
-
582
- #
583
- # The key hash passed on get_item, put_item, delete_item, update_item, etc
584
- #
585
- def key_stanza(table, hash_key, range_key = nil)
586
- key = { table.hash_key.to_s => hash_key }
587
- key[table.range_key.to_s] = range_key if range_key
588
- key
589
- end
590
-
591
- #
592
- # @param [Hash] conditions Conditions to enforce on operation (e.g. { :if => { :count => 5 }, :unless_exists => ['id']})
593
- # @return an Expected stanza for the given conditions hash
594
- #
595
- def expected_stanza(conditions = nil)
596
- expected = Hash.new { |h,k| h[k] = {} }
597
- return expected unless conditions
598
-
599
- conditions.delete(:unless_exists).try(:each) do |col|
600
- expected[col.to_s][:exists] = false
601
- end
602
- conditions.delete(:if).try(:each) do |col,val|
603
- expected[col.to_s][:value] = val
604
- end
605
-
606
- expected
607
- end
608
-
609
- #
610
- # New, semi-arbitrary API to get data on the table
611
- #
612
- def describe_table(table_name, reload = false)
613
- (!reload && table_cache[table_name]) || begin
614
- table_cache[table_name] = Table.new(client.describe_table(table_name: table_name).data)
615
- end
616
- end
617
-
618
- #
619
- # Converts a hash returned by get_item, scan, etc. into a key-value hash
620
- #
621
- def result_item_to_hash(item)
622
- {}.tap do |r|
623
- item.each { |k,v| r[k.to_sym] = v }
624
- end
625
- end
626
-
627
- # Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
628
- # This resulting hash is of the form:
629
- #
630
- # {
631
- # index_name: String
632
- # keys: {
633
- # hash_key: aws_key_schema (hash)
634
- # range_key: aws_key_schema (hash)
635
- # }
636
- # projection: {
637
- # projection_type: (ALL, KEYS_ONLY, INCLUDE) String
638
- # non_key_attributes: (optional) Array
639
- # }
640
- # provisioned_throughput: {
641
- # read_capacity_units: Integer
642
- # write_capacity_units: Integer
643
- # }
644
- # }
645
- #
646
- # @param [Dynamoid::Indexes::Index] index the index.
647
- # @return [Hash] hash representing an AWS Index definition.
648
- def index_to_aws_hash(index)
649
- key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
650
-
651
- hash = {
652
- :index_name => index.name,
653
- :key_schema => key_schema,
654
- :projection => {
655
- :projection_type => index.projection_type.to_s.upcase
656
- }
657
- }
658
-
659
- # If the projection type is include, specify the non key attributes
660
- if index.projection_type == :include
661
- hash[:projection][:non_key_attributes] = index.projected_attributes
662
- end
663
-
664
- # Only global secondary indexes have a separate throughput.
665
- if index.type == :global_secondary
666
- hash[:provisioned_throughput] = {
667
- :read_capacity_units => index.read_capacity,
668
- :write_capacity_units => index.write_capacity
669
- }
670
- end
671
- hash
672
- end
673
-
674
- # Converts hash_key_schema and range_key_schema to aws_key_schema
675
- # @param [Hash] hash_key_schema eg: {:id => :string}
676
- # @param [Hash] range_key_schema eg: {:created_at => :number}
677
- # @return [Array]
678
- def aws_key_schema(hash_key_schema, range_key_schema)
679
- schema = [{
680
- attribute_name: hash_key_schema.keys.first.to_s,
681
- key_type: HASH_KEY
682
- }]
683
-
684
- if range_key_schema.present?
685
- schema << {
686
- attribute_name: range_key_schema.keys.first.to_s,
687
- key_type: RANGE_KEY
688
- }
689
- end
690
- schema
691
- end
692
-
693
- # Builds aws attributes definitions based off of primary hash/range and
694
- # secondary indexes
695
- #
696
- # @param key_data
697
- # @option key_data [Hash] hash_key_schema - eg: {:id => :string}
698
- # @option key_data [Hash] range_key_schema - eg: {:created_at => :number}
699
- # @param [Hash] secondary_indexes
700
- # @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
701
- # @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
702
- def build_all_attribute_definitions(key_schema, secondary_indexes = {})
703
- ls_indexes = secondary_indexes[:local_secondary_indexes]
704
- gs_indexes = secondary_indexes[:global_secondary_indexes]
705
-
706
- attribute_definitions = []
707
-
708
- attribute_definitions << build_attribute_definitions(
709
- key_schema[:hash_key_schema],
710
- key_schema[:range_key_schema]
711
- )
712
-
713
- if ls_indexes.present?
714
- ls_indexes.map do |index|
715
- attribute_definitions << build_attribute_definitions(
716
- index.hash_key_schema,
717
- index.range_key_schema
718
- )
719
- end
720
- end
721
-
722
- if gs_indexes.present?
723
- gs_indexes.map do |index|
724
- attribute_definitions << build_attribute_definitions(
725
- index.hash_key_schema,
726
- index.range_key_schema
727
- )
728
- end
729
- end
730
-
731
- attribute_definitions.flatten!
732
- # uniq these definitions because range keys might be common between
733
- # primary and secondary indexes
734
- attribute_definitions.uniq!
735
- attribute_definitions
736
- end
737
-
738
-
739
- # Builds an attribute definitions based on hash key and range key
740
- # @params [Hash] hash_key_schema - eg: {:id => :string}
741
- # @params [Hash] range_key_schema - eg: {:created_at => :datetime}
742
- # @return [Array]
743
- def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
744
- attrs = []
745
-
746
- attrs << attribute_definition_element(
747
- hash_key_schema.keys.first,
748
- hash_key_schema.values.first
749
- )
750
-
751
- if range_key_schema.present?
752
- attrs << attribute_definition_element(
753
- range_key_schema.keys.first,
754
- range_key_schema.values.first
755
- )
756
- end
757
-
758
- attrs
759
- end
760
-
761
- # Builds an aws attribute definition based on name and dynamoid type
762
- # @params [Symbol] name - eg: :id
763
- # @params [Symbol] dynamoid_type - eg: :string
764
- # @return [Hash]
765
- def attribute_definition_element(name, dynamoid_type)
766
- aws_type = api_type(dynamoid_type)
767
-
768
- {
769
- :attribute_name => name.to_s,
770
- :attribute_type => aws_type
771
- }
772
- end
773
-
774
- #
775
- # Represents a table. Exposes data from the "DescribeTable" API call, and also
776
- # provides methods for coercing values to the proper types based on the table's schema data
777
- #
778
- class Table
779
- attr_reader :schema
780
-
781
- #
782
- # @param [Hash] schema Data returns from a "DescribeTable" call
783
- #
784
- def initialize(schema)
785
- @schema = schema[:table]
786
- end
787
-
788
- def range_key
789
- @range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
790
- end
791
-
792
- def range_type
793
- range_type ||= schema[:attribute_definitions].find { |d|
794
- d[:attribute_name] == range_key
795
- }.try(:fetch,:attribute_type, nil)
796
- end
797
-
798
- def hash_key
799
- @hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
800
- end
801
-
802
- #
803
- # Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
804
- # nil otherwise
805
- #
806
- def col_type(col)
807
- col = col.to_s
808
- col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
809
- col_def && col_def[:attribute_type]
810
- end
811
-
812
- def item_count
813
- schema[:item_count]
814
- end
815
- end
816
-
817
- #
818
- # Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
819
- #
820
- class ItemUpdater
821
- attr_reader :table, :key, :range_key
822
-
823
- def initialize(table, key, range_key = nil)
824
- @table = table; @key = key, @range_key = range_key
825
- @additions = {}
826
- @deletions = {}
827
- @updates = {}
828
- end
829
-
830
- #
831
- # Adds the given values to the values already stored in the corresponding columns.
832
- # The column must contain a Set or a number.
833
- #
834
- # @param [Hash] vals keys of the hash are the columns to update, vals are the values to
835
- # add. values must be a Set, Array, or Numeric
836
- #
837
- def add(values)
838
- @additions.merge!(values)
839
- end
840
-
841
- #
842
- # Removes values from the sets of the given columns
843
- #
844
- # @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
845
- # to remove
846
- #
847
- def delete(values)
848
- @deletions.merge!(values)
849
- end
850
-
851
- #
852
- # Replaces the values of one or more attributes
853
- #
854
- def set(values)
855
- @updates.merge!(values)
856
- end
857
-
858
- #
859
- # Returns an AttributeUpdates hash suitable for passing to the V2 Client API
860
- #
861
- def to_h
862
- ret = {}
863
-
864
- @additions.each do |k,v|
865
- ret[k.to_s] = {
866
- action: ADD,
867
- value: v
868
- }
869
- end
870
- @deletions.each do |k,v|
871
- ret[k.to_s] = {
872
- action: DELETE,
873
- value: v
874
- }
875
- end
876
- @updates.each do |k,v|
877
- ret[k.to_s] = {
878
- action: PUT,
879
- value: v
880
- }
881
- end
882
-
883
- ret
884
- end
885
-
886
- ADD = "ADD".freeze
887
- DELETE = "DELETE".freeze
888
- PUT = "PUT".freeze
889
- end
890
- end
891
- end
892
- end