dynamoid-edge 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,324 @@
1
+ require 'bigdecimal'
2
+ require 'securerandom'
3
+ require 'yaml'
4
+
5
+ # encoding: utf-8
6
+ module Dynamoid
7
+
8
+ # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
9
+ # values to be of the same type as when they were passed in, based on the fields in the class.
10
+ module Persistence
11
+ extend ActiveSupport::Concern
12
+
13
+ attr_accessor :new_record
14
+ alias :new_record? :new_record
15
+
16
+ module ClassMethods
17
+
18
+ def table_name
19
+ @table_name ||= "#{Dynamoid::Config.namespace}_#{options[:name] || base_class.name.split('::').last.downcase.pluralize}"
20
+ end
21
+
22
+ # Creates a table.
23
+ #
24
+ # @param [Hash] options options to pass for table creation
25
+ # @option options [Symbol] :id the id field for the table
26
+ # @option options [Symbol] :table_name the actual name for the table
27
+ # @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
28
+ # @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
29
+ # @option options [Hash] {range_key => :type} a hash of the name of the range key and a symbol of its type
30
+ # @option options [Symbol] :hash_key_type the dynamo type of the hash key (:string or :number)
31
+ # @since 0.4.0
32
+ def create_table(options = {})
33
+ if self.range_key
34
+ range_key_hash = { range_key => dynamo_type(attributes[range_key][:type]) }
35
+ else
36
+ range_key_hash = nil
37
+ end
38
+ options = {
39
+ :id => self.hash_key,
40
+ :table_name => self.table_name,
41
+ :write_capacity => self.write_capacity,
42
+ :read_capacity => self.read_capacity,
43
+ :range_key => range_key_hash,
44
+ :hash_key_type => dynamo_type(attributes[self.hash_key][:type]),
45
+ :local_secondary_indexes => self.local_secondary_indexes.values,
46
+ :global_secondary_indexes => self.global_secondary_indexes.values
47
+ }.merge(options)
48
+
49
+ Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
50
+ end
51
+
52
+ def from_database(attrs = {})
53
+ clazz = attrs[:type] ? obj = attrs[:type].constantize : self
54
+ clazz.new(attrs).tap { |r| r.new_record = false }
55
+ end
56
+
57
+ # Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
58
+ #
59
+ # @since 0.2.0
60
+ def undump(incoming = nil)
61
+ incoming = (incoming || {}).symbolize_keys
62
+ Hash.new.tap do |hash|
63
+ self.attributes.each do |attribute, options|
64
+ hash[attribute] = undump_field(incoming[attribute], options)
65
+ end
66
+ incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute }
67
+ end
68
+ end
69
+
70
+ # Undump a string value for a given type.
71
+ #
72
+ # @since 0.2.0
73
+ def undump_field(value, options)
74
+ if (field_class = options[:type]).is_a?(Class)
75
+ raise 'Dynamoid class-type fields do not support default values' if options[:default]
76
+
77
+ if field_class.respond_to?(:dynamoid_load)
78
+ field_class.dynamoid_load(value)
79
+ end
80
+ elsif options[:type] == :serialized
81
+ if value.is_a?(String)
82
+ options[:serializer] ? options[:serializer].load(value) : YAML.load(value)
83
+ else
84
+ value
85
+ end
86
+ else
87
+ if value.nil? && (default_value = options[:default])
88
+ value = default_value.respond_to?(:call) ? default_value.call : default_value
89
+ end
90
+
91
+ if !value.nil?
92
+ case options[:type]
93
+ when :string
94
+ value.to_s
95
+ when :integer
96
+ Integer(value)
97
+ when :number
98
+ BigDecimal.new(value.to_s)
99
+ when :array
100
+ value.to_a
101
+ when :set
102
+ Set.new(value)
103
+ when :datetime
104
+ if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
105
+ value
106
+ else
107
+ Time.at(value).to_datetime
108
+ end
109
+ when :boolean
110
+ # persisted as 't', but because undump is called during initialize it can come in as true
111
+ if value == 't' || value == true
112
+ true
113
+ elsif value == 'f' || value == false
114
+ false
115
+ else
116
+ raise ArgumentError, "Boolean column neither true nor false"
117
+ end
118
+ else
119
+ raise ArgumentError, "Unknown type #{options[:type]}"
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def dynamo_type(type)
126
+ if type.is_a?(Class)
127
+ type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string
128
+ else
129
+ case type
130
+ when :integer, :number, :datetime
131
+ :number
132
+ when :string, :serialized
133
+ :string
134
+ else
135
+ raise 'unknown type'
136
+ end
137
+ end
138
+ end
139
+
140
+ end
141
+
142
+ # Set updated_at and any passed in field to current DateTime. Useful for things like last_login_at, etc.
143
+ #
144
+ def touch(name = nil)
145
+ now = DateTime.now
146
+ self.updated_at = now
147
+ attributes[name] = now if name
148
+ save
149
+ end
150
+
151
+ # Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
152
+ #
153
+ # @since 0.2.0
154
+ def persisted?
155
+ !new_record?
156
+ end
157
+
158
+ # Run the callbacks and then persist this object in the datastore.
159
+ #
160
+ # @since 0.2.0
161
+ def save(options = {})
162
+ self.class.create_table
163
+
164
+ if new_record?
165
+ conditions = { :unless_exists => [self.class.hash_key]}
166
+ conditions[:unless_exists] << range_key if(range_key)
167
+
168
+ run_callbacks(:create) { persist(conditions) }
169
+ else
170
+ persist
171
+ end
172
+
173
+ self
174
+ end
175
+
176
+ #
177
+ # update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
178
+ # never cause an update! to fail, but an update! may cause a concurrent save to fail.
179
+ #
180
+ #
181
+ def update!(conditions = {}, &block)
182
+ run_callbacks(:update) do
183
+ options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
184
+
185
+ begin
186
+ new_attrs = Dynamoid.adapter.update_item(self.class.table_name, self.hash_key, options.merge(:conditions => conditions)) do |t|
187
+ if(self.class.attributes[:lock_version])
188
+ t.add(lock_version: 1)
189
+ end
190
+
191
+ yield t
192
+ end
193
+ load(new_attrs)
194
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
195
+ raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
196
+ end
197
+ end
198
+ end
199
+
200
+ def update(conditions = {}, &block)
201
+ update!(conditions, &block)
202
+ true
203
+ rescue Dynamoid::Errors::StaleObjectError
204
+ false
205
+ end
206
+
207
+ # Delete this object, but only after running callbacks for it.
208
+ #
209
+ # @since 0.2.0
210
+ def destroy
211
+ run_callbacks(:destroy) do
212
+ self.delete
213
+ end
214
+ self
215
+ end
216
+
217
+ # Delete this object from the datastore.
218
+ #
219
+ # @since 0.2.0
220
+ def delete
221
+ options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
222
+
223
+ # Add an optimistic locking check if the lock_version column exists
224
+ if(self.class.attributes[:lock_version])
225
+ conditions = {:if => {}}
226
+ conditions[:if][:lock_version] =
227
+ if changes[:lock_version].nil?
228
+ self.lock_version
229
+ else
230
+ changes[:lock_version][0]
231
+ end
232
+ options[:conditions] = conditions
233
+ end
234
+ Dynamoid.adapter.delete(self.class.table_name, self.hash_key, options)
235
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
236
+ raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
237
+ end
238
+
239
+ # Dump this object's attributes into hash form, fit to be persisted into the datastore.
240
+ #
241
+ # @since 0.2.0
242
+ def dump
243
+ Hash.new.tap do |hash|
244
+ self.class.attributes.each do |attribute, options|
245
+ hash[attribute] = dump_field(self.read_attribute(attribute), options)
246
+ end
247
+ end
248
+ end
249
+
250
+ private
251
+
252
+ # Determine how to dump this field. Given a value, it'll determine how to turn it into a value that can be
253
+ # persisted into the datastore.
254
+ #
255
+ # @since 0.2.0
256
+ def dump_field(value, options)
257
+ if (field_class = options[:type]).is_a?(Class)
258
+ if value.respond_to?(:dynamoid_dump)
259
+ value.dynamoid_dump
260
+ elsif field_class.respond_to?(:dynamoid_dump)
261
+ field_class.dynamoid_dump(value)
262
+ else
263
+ raise ArgumentError, "Neither #{field_class} nor #{value} support serialization for Dynamoid."
264
+ end
265
+ else
266
+ case options[:type]
267
+ when :string
268
+ !value.nil? ? value.to_s : nil
269
+ when :integer
270
+ !value.nil? ? Integer(value) : nil
271
+ when :number
272
+ !value.nil? ? value : nil
273
+ when :set
274
+ !value.nil? ? Set.new(value) : nil
275
+ when :array
276
+ !value.nil? ? value : nil
277
+ when :datetime
278
+ !value.nil? ? value.to_time.to_f : nil
279
+ when :serialized
280
+ options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
281
+ when :boolean
282
+ !value.nil? ? value.to_s[0] : nil
283
+ else
284
+ raise ArgumentError, "Unknown type #{options[:type]}"
285
+ end
286
+ end
287
+ end
288
+
289
+ # Persist the object into the datastore. Assign it an id first if it doesn't have one.
290
+ #
291
+ # @since 0.2.0
292
+ def persist(conditions = nil)
293
+ run_callbacks(:save) do
294
+ self.hash_key = SecureRandom.uuid if self.hash_key.nil? || self.hash_key.blank?
295
+
296
+ # Add an exists check to prevent overwriting existing records with new ones
297
+ if(new_record?)
298
+ conditions ||= {}
299
+ (conditions[:unless_exists] ||= []) << self.class.hash_key
300
+ end
301
+
302
+ # Add an optimistic locking check if the lock_version column exists
303
+ if(self.class.attributes[:lock_version])
304
+ conditions ||= {}
305
+ self.lock_version = (lock_version || 0) + 1
306
+ #Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
307
+ (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if(changes[:lock_version][0])
308
+ end
309
+
310
+ begin
311
+ Dynamoid.adapter.write(self.class.table_name, self.dump, conditions)
312
+ @new_record = false
313
+ true
314
+ rescue Dynamoid::Errors::ConditionalCheckFailedException => e
315
+ if new_record?
316
+ raise Dynamoid::Errors::RecordNotUnique.new(e, self)
317
+ else
318
+ raise Dynamoid::Errors::StaleObjectError.new(self, 'persist')
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+ module Dynamoid
3
+
4
+ # Provide ActiveModel validations to Dynamoid documents.
5
+ module Validations
6
+ extend ActiveSupport::Concern
7
+
8
+ include ActiveModel::Validations
9
+ include ActiveModel::Validations::Callbacks
10
+
11
+ # Override save to provide validation support.
12
+ #
13
+ # @since 0.2.0
14
+ def save(options = {})
15
+ options.reverse_merge!(:validate => true)
16
+ return false if options[:validate] and (not valid?)
17
+ super
18
+ end
19
+
20
+ # Is this object valid?
21
+ #
22
+ # @since 0.2.0
23
+ def valid?(context = nil)
24
+ context ||= (new_record? ? :create : :update)
25
+ super(context)
26
+ end
27
+
28
+ # Raise an error unless this object is valid.
29
+ #
30
+ # @since 0.2.0
31
+ def save!
32
+ raise Dynamoid::Errors::DocumentNotValid.new(self) unless valid?
33
+ save(:validate => false)
34
+ end
35
+ end
36
+ end
data/lib/dynamoid.rb ADDED
@@ -0,0 +1,50 @@
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
+
12
+ require 'dynamoid/errors'
13
+ require 'dynamoid/fields'
14
+ require 'dynamoid/indexes'
15
+ require 'dynamoid/associations'
16
+ require 'dynamoid/persistence'
17
+ require 'dynamoid/dirty'
18
+ require 'dynamoid/validations'
19
+ require 'dynamoid/criteria'
20
+ require 'dynamoid/finders'
21
+ require 'dynamoid/identity_map'
22
+ require 'dynamoid/config'
23
+ require 'dynamoid/components'
24
+ require 'dynamoid/document'
25
+ require 'dynamoid/adapter'
26
+
27
+ require 'dynamoid/middleware/identity_map'
28
+
29
+ module Dynamoid
30
+ extend self
31
+
32
+ MAX_ITEM_SIZE = 65_536
33
+
34
+ def configure
35
+ block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
36
+ end
37
+ alias :config :configure
38
+
39
+ def logger
40
+ Dynamoid::Config.logger
41
+ end
42
+
43
+ def included_models
44
+ @included_models ||= []
45
+ end
46
+
47
+ def adapter
48
+ @adapter ||= Adapter.new
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamoid-edge
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Symonds
8
+ - Logan Bowers
9
+ - Craig Heneveld
10
+ - Anatha Kumaran
11
+ - Jason Dew
12
+ - Luis Arias
13
+ - Stefan Neculai
14
+ - Philip White
15
+ - Peeyush Kumar
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+ date: 2016-03-10 00:00:00.000000000 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activemodel
23
+ requirement: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '4'
28
+ type: :runtime
29
+ prerelease: false
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '4'
35
+ - !ruby/object:Gem::Dependency
36
+ name: aws-sdk-resources
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2'
49
+ - !ruby/object:Gem::Dependency
50
+ name: concurrent-ruby
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '1.0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: rake
65
+ requirement: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ - !ruby/object:Gem::Dependency
78
+ name: rspec
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '3'
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '3'
91
+ - !ruby/object:Gem::Dependency
92
+ name: bundler
93
+ requirement: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ - !ruby/object:Gem::Dependency
106
+ name: yard
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ type: :development
113
+ prerelease: false
114
+ version_requirements: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ - !ruby/object:Gem::Dependency
120
+ name: github-markup
121
+ requirement: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ type: :development
127
+ prerelease: false
128
+ version_requirements: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ - !ruby/object:Gem::Dependency
134
+ name: pry
135
+ requirement: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ type: :development
141
+ prerelease: false
142
+ version_requirements: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ - !ruby/object:Gem::Dependency
148
+ name: coveralls
149
+ requirement: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ description: Dynamoid is an ORM for Amazon's DynamoDB that supports offline development,
162
+ associations, querying, and everything else you'd expect from an ActiveRecord-style
163
+ replacement.
164
+ email:
165
+ executables: []
166
+ extensions: []
167
+ extra_rdoc_files:
168
+ - LICENSE.txt
169
+ - README.markdown
170
+ files:
171
+ - CHANGELOG.md
172
+ - Gemfile
173
+ - LICENSE.txt
174
+ - README.markdown
175
+ - Rakefile
176
+ - dynamoid-edge.gemspec
177
+ - lib/dynamoid.rb
178
+ - lib/dynamoid/adapter.rb
179
+ - lib/dynamoid/adapter_plugin/aws_sdk_v2.rb
180
+ - lib/dynamoid/associations.rb
181
+ - lib/dynamoid/associations/association.rb
182
+ - lib/dynamoid/associations/belongs_to.rb
183
+ - lib/dynamoid/associations/has_and_belongs_to_many.rb
184
+ - lib/dynamoid/associations/has_many.rb
185
+ - lib/dynamoid/associations/has_one.rb
186
+ - lib/dynamoid/associations/many_association.rb
187
+ - lib/dynamoid/associations/single_association.rb
188
+ - lib/dynamoid/components.rb
189
+ - lib/dynamoid/config.rb
190
+ - lib/dynamoid/config/options.rb
191
+ - lib/dynamoid/criteria.rb
192
+ - lib/dynamoid/criteria/chain.rb
193
+ - lib/dynamoid/dirty.rb
194
+ - lib/dynamoid/document.rb
195
+ - lib/dynamoid/errors.rb
196
+ - lib/dynamoid/fields.rb
197
+ - lib/dynamoid/finders.rb
198
+ - lib/dynamoid/identity_map.rb
199
+ - lib/dynamoid/middleware/identity_map.rb
200
+ - lib/dynamoid/persistence.rb
201
+ - lib/dynamoid/validations.rb
202
+ homepage: http://github.com/Dynamoid/Dynamoid
203
+ licenses:
204
+ - MIT
205
+ metadata: {}
206
+ post_install_message:
207
+ rdoc_options: []
208
+ require_paths:
209
+ - lib
210
+ required_ruby_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ required_rubygems_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ">="
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ requirements: []
221
+ rubyforge_project:
222
+ rubygems_version: 2.4.5.1
223
+ signing_key:
224
+ specification_version: 4
225
+ summary: Dynamoid is an ORM for Amazon's DynamoDB
226
+ test_files: []