dynamoid 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26990dd042447eeba355601d0c7eb3657bcc482e80d00c1da5602d2f82765504
4
- data.tar.gz: 76e1778155fb8a7a1d3135d4886d9e7caf1a07b72263fc38d9162ae946d5dab1
3
+ metadata.gz: 2146a1a188ecf2b8dd2f280172119879d6612d9eadda6cbcbf6defb3319f7022
4
+ data.tar.gz: df59300783324be7952fd25f8d49880707938762d3960ce1d422c18b49e26ac6
5
5
  SHA512:
6
- metadata.gz: 2aadf93639577566ed5dd9dadff596316f3d22384f5d7dabd0e06aa49992d3a58e824a8c150002c3a39d6e42f7b2b17c4f383bda2c145f4d05e1d56bf21a5e54
7
- data.tar.gz: 14801f95c8cd1a241d2b1522b05a1e7af307822cef50e4515502f641027ade16aaf750a099801a2f2e4e379b15a4cd96e78f84754278db37d2e6fcdaab57b242
6
+ metadata.gz: d2ebd06729d024b98338e5c11953976935f5252ad76f6dec9265929fd447ed488295d9f06c8ae5440c7c6a494fb1cdb44ffeba002c18ea52bd4ad100df2d4c66
7
+ data.tar.gz: eddf03eb98125fbc7fcfa842e8276ba5dcbd558abacc3adeb2d451e3bb71bcb92f69eb84a01231304b4690b087a826ebf7c4ee2426a16d92a7a126ceddf1e132
data/CHANGELOG.md CHANGED
@@ -1,11 +1,28 @@
1
1
  # HEAD
2
2
 
3
+ ---
4
+
5
+
6
+
7
+ # 3.7.0 / 2021-02-02
8
+
9
+
3
10
  ## Features
4
11
 
12
+ * [#476](https://github.com/Dynamoid/dynamoid/pull/476) Added `#with_index` method to force an index in query (@bmalinconico)
13
+ * [#481](https://github.com/Dynamoid/dynamoid/pull/481) Added `alias` option to the `field` method to declare a field alias and use more conventional name to set and get value
14
+
5
15
  ## Improvements
6
16
 
17
+ * [#482](https://github.com/Dynamoid/dynamoid/pull/482) Support Ruby 3.0 and Rails 6.1
18
+ * [#461](https://github.com/Dynamoid/dynamoid/pull/461) Allow to delete item attribute with `#update` method (@jkirsteins)
19
+ * [#463](https://github.com/Dynamoid/dynamoid/pull/463) Raise `UnknownAttribute` exception when specified not declared attribute name (@AlexGascon)
20
+
7
21
  ## Fixes
8
22
 
23
+ * [#480](https://github.com/Dynamoid/dynamoid/pull/480) Repair `.consistent`/`.delete_all`/`.destroy_all` calls directly on a model class
24
+ * [#484](https://github.com/Dynamoid/dynamoid/pull/484) Fix broken foreign keys after model deleting (@kkan)
25
+ * Fixes in Readme.md: [#470](https://github.com/Dynamoid/dynamoid/pull/470) (@rromanchuk), [#473](https://github.com/Dynamoid/dynamoid/pull/473) (@Rulikkk)
9
26
 
10
27
  ---
11
28
 
data/README.md CHANGED
@@ -126,8 +126,8 @@ end
126
126
  Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
127
127
 
128
128
  Its compatibility is tested against following Ruby versions: 2.3, 2.4,
129
- 2.5 and 2.6, JRuby 9.2.8.0 and against Rails versions: 4.2, 5.0, 5.1,
130
- 5.2 and 6.0.
129
+ 2.5, 2.6, 2.7 and 3.0, JRuby 9.2.x and against Rails versions: 4.2, 5.0, 5.1,
130
+ 5.2, 6.0 and 6.1.
131
131
 
132
132
  ## Setup
133
133
 
@@ -365,6 +365,26 @@ field :actions_taken, :integer, default: 0
365
365
  field :joined_at, :datetime, default: -> { Time.now }
366
366
  ```
367
367
 
368
+ #### Aliases
369
+
370
+ It might be helpful to define an alias for already existing field when
371
+ naming convention used for a table differs from conventions common in
372
+ Ruby:
373
+
374
+ ```ruby
375
+ field firstName, :string, alias: :first_name
376
+ ```
377
+
378
+ This way there will be generated
379
+ setters/getters/`<name>?`/`<name>_before_type_cast` methods for both
380
+ original field name (`firstName`) and an alias (`first_name`).
381
+
382
+ ```ruby
383
+ user = User.new(first_name: 'Michael')
384
+ user.first_name # => 'Michael'
385
+ user.firstName # => 'Michael'
386
+ ```
387
+
368
388
  #### Custom Types
369
389
 
370
390
  To use a custom type for a field, suppose you have a `Money` type.
@@ -576,7 +596,7 @@ c.my_new_type
576
596
 
577
597
  ### Type casting
578
598
 
579
- Dynamid supports type casting and tries to do it in the most convenient
599
+ Dynamoid supports type casting and tries to do it in the most convenient
580
600
  way. Values for all fields (except custom type) are coerced to declared
581
601
  field types.
582
602
 
@@ -614,7 +634,7 @@ well.
614
634
 
615
635
  ### Dirty API
616
636
 
617
- Dynamoid supports Dirty API which equivalents to [Rails 5.2
637
+ Dynamoid supports Dirty API which is equivalent to [Rails 5.2
618
638
  `ActiveModel::Dirty`](https://api.rubyonrails.org/v5.2/classes/ActiveModel/Dirty.html).
619
639
  There is only one limitation - change in place of field isn't detected
620
640
  automatically.
@@ -632,7 +652,7 @@ u.email = 'josh@joshsymonds.com'
632
652
  u.save
633
653
  ```
634
654
 
635
- Save forces persistence to the datastore: a unique ID is also assigned,
655
+ Save forces persistence to the data store: a unique ID is also assigned,
636
656
  but it is a string and not an auto-incrementing number.
637
657
 
638
658
  ```ruby
@@ -1267,7 +1287,7 @@ class User
1267
1287
  end
1268
1288
 
1269
1289
  Dynamoid.config.logger.level = :debug
1270
- Dynamoid.config.endpoint = 'localhost:8000'
1290
+ Dynamoid.config.endpoint = 'http://localhost:8000'
1271
1291
 
1272
1292
  User.create(name: 'Alex')
1273
1293
 
@@ -1342,12 +1362,12 @@ environment.
1342
1362
 
1343
1363
  If you want to run all the specs that travis runs, use `bundle exec
1344
1364
  wwtd`, but first you will need to setup all the rubies, for each of `%w(
1345
- 2.0.0-p648 2.1.10 2.2.6 2.3.3 2.4.1 jruby-9.1.8.0 )`. When you run
1365
+ 2.3.8 2.4.6 2.5.5 2.6.3 2.7.0 3.0.0 9.2.14.0)`. When you run
1346
1366
  `bundle exec wwtd` it will take care of starting and stopping the local
1347
1367
  dynamodb instance.
1348
1368
 
1349
1369
  ```shell
1350
- rvm use 2.0.0-p648
1370
+ rvm use 3.0.0
1351
1371
  gem install rubygems-update
1352
1372
  gem install bundler
1353
1373
  bundle install
@@ -158,8 +158,14 @@ module Dynamoid
158
158
  #
159
159
  # @since 0.2.0
160
160
  def method_missing(method, *args, &block)
161
- return benchmark(method, *args) { adapter.send(method, *args, &block) } if adapter.respond_to?(method)
161
+ # Don't use keywork arguments delegating (with **kw). It works in
162
+ # different way in different Ruby versions: <= 2.6, 2.7, 3.0 and in some
163
+ # future 3.x versions. Providing that there are no downstream methods
164
+ # with keyword arguments in adapter.
165
+ #
166
+ # https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
162
167
 
168
+ return benchmark(method, *args) { adapter.send(method, *args, &block) } if adapter.respond_to?(method)
163
169
  super
164
170
  end
165
171
 
@@ -12,6 +12,15 @@ module Dynamoid
12
12
  # @private
13
13
  module AdapterPlugin
14
14
  # The AwsSdkV3 adapter provides support for the aws-sdk version 2 for ruby.
15
+
16
+ # Note: Don't use keyword arguments in public methods as far as method
17
+ # calls on adapter are delegated to the plugin.
18
+ #
19
+ # There are breaking changes in Ruby related to delegating keyword
20
+ # arguments so we have decided just to avoid them when use delegation.
21
+ #
22
+ # https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
23
+
15
24
  class AwsSdkV3
16
25
  EQ = 'EQ'
17
26
  RANGE_MAP = {
@@ -281,7 +290,7 @@ module Dynamoid
281
290
  false
282
291
  end
283
292
 
284
- def update_time_to_live(table_name:, attribute:)
293
+ def update_time_to_live(table_name, attribute)
285
294
  request = {
286
295
  table_name: table_name,
287
296
  time_to_live_specification: {
@@ -59,9 +59,9 @@ module Dynamoid
59
59
  end
60
60
  @deletions.each do |k, v|
61
61
  ret[k.to_s] = {
62
- action: DELETE,
63
- value: v
62
+ action: DELETE
64
63
  }
64
+ ret[k.to_s][:value] = v unless v.nil?
65
65
  end
66
66
  @updates.each do |k, v|
67
67
  ret[k.to_s] = {
@@ -58,6 +58,12 @@ module Dynamoid
58
58
  :set
59
59
  end
60
60
 
61
+ def disassociate_source
62
+ Array(target).each do |target_entry|
63
+ target_entry.send(target_association).disassociate(source.hash_key) if target_entry && target_association
64
+ end
65
+ end
66
+
61
67
  private
62
68
 
63
69
  # The target class name, either inferred through the association's name or specified in options.
@@ -25,7 +25,9 @@ module Dynamoid
25
25
  # @private
26
26
  # @since 0.2.0
27
27
  def find_target
28
- Array(target_class.find(source_ids.to_a))
28
+ return [] if source_ids.empty?
29
+
30
+ Array(target_class.find(source_ids.to_a, raise_error: false))
29
31
  end
30
32
 
31
33
  # @private
@@ -28,7 +28,7 @@ module Dynamoid
28
28
  # unsaved changes will be saved. Doesn't delete an associated model from
29
29
  # DynamoDB.
30
30
  def delete
31
- target.send(target_association).disassociate(source.hash_key) if target && target_association
31
+ disassociate_source
32
32
  disassociate
33
33
  target
34
34
  end
@@ -90,7 +90,7 @@ module Dynamoid
90
90
 
91
91
  # @private
92
92
  def associate(hash_key)
93
- target.send(target_association).disassociate(source.hash_key) if target && target_association
93
+ disassociate_source
94
94
  source.update_attribute(source_attribute, Set[hash_key])
95
95
  end
96
96
 
@@ -109,7 +109,7 @@ module Dynamoid
109
109
  def find_target
110
110
  return if source_ids.empty?
111
111
 
112
- target_class.find(source_ids.first)
112
+ target_class.find(source_ids.first, raise_error: false)
113
113
  end
114
114
 
115
115
  def target=(object)
@@ -9,12 +9,19 @@ module Dynamoid
9
9
 
10
10
  # @private
11
11
  module ClassMethods
12
- %i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages project pluck].each do |meth|
12
+ %i[where consistent all first last delete_all destroy_all each record_limit scan_limit batch start scan_index_forward find_by_pages project pluck].each do |meth|
13
13
  # Return a criteria chain in response to a method that will begin or end a chain. For more information,
14
14
  # see Dynamoid::Criteria::Chain.
15
15
  #
16
16
  # @since 0.2.0
17
17
  define_method(meth) do |*args, &blk|
18
+ # Don't use keywork arguments delegating (with **kw). It works in
19
+ # different way in different Ruby versions: <= 2.6, 2.7, 3.0 and in some
20
+ # future 3.x versions. Providing that there are no downstream methods
21
+ # with keyword arguments in Chain.
22
+ #
23
+ # https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
24
+
18
25
  chain = Dynamoid::Criteria::Chain.new(self)
19
26
  if args
20
27
  chain.send(meth, *args, &blk)
@@ -110,7 +110,7 @@ module Dynamoid
110
110
  query.update(args.symbolize_keys)
111
111
 
112
112
  # we should re-initialize keys detector every time we change query
113
- @key_fields_detector = KeyFieldsDetector.new(@query, @source)
113
+ @key_fields_detector = KeyFieldsDetector.new(@query, @source, forced_index_name: @forced_index_name)
114
114
 
115
115
  self
116
116
  end
@@ -358,6 +358,36 @@ module Dynamoid
358
358
  self
359
359
  end
360
360
 
361
+ # Force the index name to use for queries.
362
+ #
363
+ # By default allows the library to select the most appropriate index.
364
+ # Sometimes you have more than one index which will fulfill your query's
365
+ # needs. When this case occurs you may want to force an order. This occurs
366
+ # when you are searching by hash key, but not specifying a range key.
367
+ #
368
+ # class Comment
369
+ # include Dynamoid::Document
370
+ #
371
+ # table key: :post_id
372
+ # range_key :author_id
373
+ #
374
+ # field :post_date, :datetime
375
+ #
376
+ # global_secondary_index name: :time_sorted_comments, hash_key: :post_id, range_key: post_date, projected_attributes: :all
377
+ # end
378
+ #
379
+ #
380
+ # Comment.where(post_id: id).with_index(:time_sorted_comments).scan_index_forward(false)
381
+ #
382
+ # @return [Dynamoid::Criteria::Chain]
383
+ def with_index(index_name)
384
+ raise Dynamoid::Errors::InvalidIndex, "Unknown index #{index_name}" unless @source.find_index_by_name(index_name)
385
+
386
+ @forced_index_name = index_name
387
+ @key_fields_detector = KeyFieldsDetector.new(@query, @source, forced_index_name: index_name)
388
+ self
389
+ end
390
+
361
391
  # Allows to use the results of a search as an enumerable over the results
362
392
  # found.
363
393
  #
@@ -24,10 +24,11 @@ module Dynamoid
24
24
  end
25
25
  end
26
26
 
27
- def initialize(query, source)
27
+ def initialize(query, source, forced_index_name: nil)
28
28
  @query = query
29
29
  @source = source
30
30
  @query = Query.new(query)
31
+ @forced_index_name = forced_index_name
31
32
  @result = find_keys_in_query
32
33
  end
33
34
 
@@ -54,6 +55,8 @@ module Dynamoid
54
55
  private
55
56
 
56
57
  def find_keys_in_query
58
+ return match_forced_index if @forced_index_name
59
+
57
60
  match_table_and_sort_key ||
58
61
  match_local_secondary_index ||
59
62
  match_global_secondary_index_and_sort_key ||
@@ -133,6 +136,16 @@ module Dynamoid
133
136
  }
134
137
  end
135
138
  end
139
+
140
+ def match_forced_index
141
+ idx = @source.find_index_by_name(@forced_index_name)
142
+
143
+ {
144
+ hash_key: idx.hash_key,
145
+ range_key: idx.range_key,
146
+ index_name: idx.name,
147
+ }
148
+ end
136
149
  end
137
150
  end
138
151
  end
@@ -75,5 +75,7 @@ module Dynamoid
75
75
  class InvalidQuery < Error; end
76
76
 
77
77
  class UnsupportedKeyType < Error; end
78
+
79
+ class UnknownAttribute < Error; end
78
80
  end
79
81
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dynamoid/fields/declare'
4
+
3
5
  module Dynamoid
4
6
  # All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
5
7
  # specified with field, then they will be ignored.
@@ -43,15 +45,15 @@ module Dynamoid
43
45
  # end
44
46
  #
45
47
  # Its type determines how it is coerced when read in and out of the
46
- # datastore. You can specify +string+, +integer+, +number+, +set+, +array+,
48
+ # data store. You can specify +string+, +integer+, +number+, +set+, +array+,
47
49
  # +map+, +datetime+, +date+, +serialized+, +raw+, +boolean+ and +binary+
48
50
  # or specify a class that defines a serialization strategy.
49
51
  #
50
52
  # By default field type is +string+.
51
53
  #
52
54
  # Set can store elements of the same type only (it's a limitation of
53
- # DynamoDB itself). If a set should store elements only some particular
54
- # type +of+ option should be specified:
55
+ # DynamoDB itself). If a set should store elements only of some particular
56
+ # type then +of+ option should be specified:
55
57
  #
56
58
  # field :hobbies, :set, of: :string
57
59
  #
@@ -126,41 +128,31 @@ module Dynamoid
126
128
  # user.age # => 21 - integer
127
129
  # user.age_before_type_cast # => '21' - string
128
130
  #
131
+ # There is also an option +alias+ which allows to use another name for a
132
+ # field:
133
+ #
134
+ # class User
135
+ # include Dynamoid::Document
136
+ #
137
+ # field :firstName, :string, alias: :first_name
138
+ # end
139
+ #
140
+ # user = User.new(firstName: 'Michael')
141
+ # user.firstName # Michael
142
+ # user.first_name # Michael
143
+ #
129
144
  # @param name [Symbol] name of the field
130
145
  # @param type [Symbol] type of the field (optional)
131
146
  # @param options [Hash] any additional options for the field type (optional)
132
147
  #
133
148
  # @since 0.2.0
134
149
  def field(name, type = :string, options = {})
135
- named = name.to_s
136
150
  if type == :float
137
151
  Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
138
152
  type = :number
139
153
  end
140
- self.attributes = attributes.merge(name => { type: type }.merge(options))
141
-
142
- # should be called before `define_attribute_methods` method because it defines a getter itself
143
- warn_about_method_overriding(name, name)
144
- warn_about_method_overriding("#{named}=", name)
145
- warn_about_method_overriding("#{named}?", name)
146
- warn_about_method_overriding("#{named}_before_type_cast?", name)
147
154
 
148
- define_attribute_method(name) # Dirty API
149
-
150
- generated_methods.module_eval do
151
- define_method(named) { read_attribute(named) }
152
- define_method("#{named}?") do
153
- value = read_attribute(named)
154
- case value
155
- when true then true
156
- when false, nil then false
157
- else
158
- !value.nil?
159
- end
160
- end
161
- define_method("#{named}=") { |value| write_attribute(named, value) }
162
- define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
163
- end
155
+ Dynamoid::Fields::Declare.new(self, name, type, options).call
164
156
  end
165
157
 
166
158
  # Declare a table range key.
@@ -273,8 +265,7 @@ module Dynamoid
273
265
  options[:timestamps] || (options[:timestamps].nil? && Dynamoid::Config.timestamps)
274
266
  end
275
267
 
276
- private
277
-
268
+ # @private
278
269
  def generated_methods
279
270
  @generated_methods ||= begin
280
271
  Module.new.tap do |mod|
@@ -282,12 +273,6 @@ module Dynamoid
282
273
  end
283
274
  end
284
275
  end
285
-
286
- def warn_about_method_overriding(method_name, field_name)
287
- if instance_methods.include?(method_name.to_sym)
288
- Dynamoid.logger.warn("Method #{method_name} generated for the field #{field_name} overrides already existing method")
289
- end
290
- end
291
276
  end
292
277
 
293
278
  # You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
@@ -309,6 +294,10 @@ module Dynamoid
309
294
  def write_attribute(name, value)
310
295
  name = name.to_sym
311
296
 
297
+ unless attribute_is_present_on_model?(name)
298
+ raise Dynamoid::Errors::UnknownAttribute.new("Attribute #{name} is not part of the model")
299
+ end
300
+
312
301
  if association = @associations[name]
313
302
  association.reset
314
303
  end
@@ -405,5 +394,10 @@ module Dynamoid
405
394
  send("#{type}=", self.class.name)
406
395
  end
407
396
  end
397
+
398
+ def attribute_is_present_on_model?(attribute_name)
399
+ setter = "#{attribute_name}=".to_sym
400
+ respond_to?(setter)
401
+ end
408
402
  end
409
403
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Fields
5
+ # @private
6
+ class Declare
7
+ def initialize(source, name, type, options)
8
+ @source = source
9
+ @name = name.to_sym
10
+ @type = type
11
+ @options = options
12
+ end
13
+
14
+ def call
15
+ # Register new field metadata
16
+ @source.attributes = @source.attributes.merge(
17
+ @name => { type: @type }.merge(@options)
18
+ )
19
+
20
+ # Should be called before `define_attribute_methods` method because it
21
+ # defines an attribute getter itself
22
+ warn_about_method_overriding
23
+
24
+ # Dirty API
25
+ @source.define_attribute_method(@name)
26
+
27
+ # Generate getters and setters as well as other helper methods
28
+ generate_instance_methods
29
+
30
+ # If alias name specified - generate the same instance methods
31
+ if @options[:alias]
32
+ generate_instance_methods_for_alias
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def warn_about_method_overriding
39
+ warn_if_method_exists(@name)
40
+ warn_if_method_exists("#{@name}=")
41
+ warn_if_method_exists("#{@name}?")
42
+ warn_if_method_exists("#{@name}_before_type_cast?")
43
+ end
44
+
45
+ def generate_instance_methods
46
+ # only local variable is visible in `module_eval` block
47
+ name = @name
48
+
49
+ @source.generated_methods.module_eval do
50
+ define_method(name) { read_attribute(name) }
51
+ define_method("#{name}?") do
52
+ value = read_attribute(name)
53
+ case value
54
+ when true then true
55
+ when false, nil then false
56
+ else
57
+ !value.nil?
58
+ end
59
+ end
60
+ define_method("#{name}=") { |value| write_attribute(name, value) }
61
+ define_method("#{name}_before_type_cast") { read_attribute_before_type_cast(name) }
62
+ end
63
+ end
64
+
65
+ def generate_instance_methods_for_alias
66
+ # only local variable is visible in `module_eval` block
67
+ name = @name
68
+
69
+ alias_name = @options[:alias].to_sym
70
+
71
+ @source.generated_methods.module_eval do
72
+ alias_method alias_name, name
73
+ alias_method "#{alias_name}=", "#{name}="
74
+ alias_method "#{alias_name}?", "#{name}?"
75
+ alias_method "#{alias_name}_before_type_cast", "#{name}_before_type_cast"
76
+ end
77
+ end
78
+
79
+ def warn_if_method_exists(method)
80
+ if @source.instance_methods.include?(method.to_sym)
81
+ Dynamoid.logger.warn("Method #{method} generated for the field #{@name} overrides already existing method")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -154,6 +154,16 @@ module Dynamoid
154
154
  index
155
155
  end
156
156
 
157
+ # Returns an index by its name
158
+ #
159
+ # @param name [string, symbol] the name of the index to lookup
160
+ # @return [Dynamoid::Indexes::Index, nil] index object or nil if it isn't found
161
+ def find_index_by_name(name)
162
+ string_name = name.to_s
163
+ indexes.each_value.detect{ |i| i.name.to_s == string_name }
164
+ end
165
+
166
+
157
167
  # Returns true iff the provided hash[,range] key combo is a local
158
168
  # secondary index.
159
169
  #
@@ -10,7 +10,7 @@ module Dynamoid
10
10
  end
11
11
  end
12
12
 
13
- # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
13
+ # Reload an object from the database -- if you suspect the object has changed in the data store and you need those
14
14
  # changes to be reflected immediately, you would call this method. This is a consistent read.
15
15
  #
16
16
  # @return [Dynamoid::Document] the document this method was called on
@@ -23,7 +23,7 @@ module Dynamoid
23
23
  options[:range_key] = range_value
24
24
  end
25
25
 
26
- self.attributes = self.class.find(hash_key, options).attributes
26
+ self.attributes = self.class.find(hash_key, **options).attributes
27
27
  @associations.values.each(&:reset)
28
28
  self
29
29
  end
@@ -8,11 +8,12 @@ require 'dynamoid/persistence/import'
8
8
  require 'dynamoid/persistence/update_fields'
9
9
  require 'dynamoid/persistence/upsert'
10
10
  require 'dynamoid/persistence/save'
11
+ require 'dynamoid/persistence/update_validations'
11
12
 
12
13
  # encoding: utf-8
13
14
  module Dynamoid
14
- # # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
15
- # # values to be of the same type as when they were passed in, based on the fields in the class.
15
+ # Persistence is responsible for dumping objects to and marshalling objects from the data store. It tries to reserialize
16
+ # values to be of the same type as when they were passed in, based on the fields in the class.
16
17
  module Persistence
17
18
  extend ActiveSupport::Concern
18
19
 
@@ -111,7 +112,7 @@ module Dynamoid
111
112
 
112
113
  if created_successfuly && self.options[:expires]
113
114
  attribute = self.options[:expires][:field]
114
- Dynamoid.adapter.update_time_to_live(table_name: table_name, attribute: attribute)
115
+ Dynamoid.adapter.update_time_to_live(table_name, attribute)
115
116
  end
116
117
  end
117
118
 
@@ -276,6 +277,9 @@ module Dynamoid
276
277
  # +update_fields+ uses the +UpdateItem+ operation so it saves changes and
277
278
  # loads an updated document back with one HTTP request.
278
279
  #
280
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
281
+ # attributes is not on the model
282
+ #
279
283
  # @param hash_key_value [Scalar value] hash key
280
284
  # @param range_key_value [Scalar value] range key (optional)
281
285
  # @param attrs [Hash]
@@ -324,6 +328,9 @@ module Dynamoid
324
328
  # +upsert+ uses the +UpdateItem+ operation so it saves changes and loads
325
329
  # an updated document back with one HTTP request.
326
330
  #
331
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
332
+ # attributes is not on the model
333
+ #
327
334
  # @param hash_key_value [Scalar value] hash key
328
335
  # @param range_key_value [Scalar value] range key (optional)
329
336
  # @param attrs [Hash]
@@ -491,6 +498,9 @@ module Dynamoid
491
498
  #
492
499
  # user.update_attributes(age: 27, last_name: 'Tylor')
493
500
  #
501
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
502
+ # attributes is not on the model
503
+ #
494
504
  # @param attributes [Hash] a hash of attributes to update
495
505
  # @return [true|false] Whether updating successful or not
496
506
  # @since 0.2.0
@@ -507,6 +517,9 @@ module Dynamoid
507
517
  # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
508
518
  # fails.
509
519
  #
520
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
521
+ # attributes is not on the model
522
+ #
510
523
  # @param attributes [Hash] a hash of attributes to update
511
524
  def update_attributes!(attributes)
512
525
  attributes.each { |attribute, value| write_attribute(attribute, value) }
@@ -519,6 +532,9 @@ module Dynamoid
519
532
  #
520
533
  # user.update_attribute(:last_name, 'Tylor')
521
534
  #
535
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
536
+ # attributes is not on the model
537
+ #
522
538
  # @param attribute [Symbol] attribute name to update
523
539
  # @param value [Object] the value to assign it
524
540
  # @return [Dynamoid::Document] self
@@ -786,6 +802,10 @@ module Dynamoid
786
802
  @destroyed = true
787
803
 
788
804
  Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
805
+
806
+ self.class.associations.each do |name, options|
807
+ send(name).disassociate_source
808
+ end
789
809
  rescue Dynamoid::Errors::ConditionalCheckFailedException
790
810
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
791
811
  end
@@ -4,8 +4,8 @@ module Dynamoid
4
4
  module Persistence
5
5
  # @private
6
6
  class UpdateFields
7
- def self.call(*args)
8
- new(*args).call
7
+ def self.call(*args, **options)
8
+ new(*args, **options).call
9
9
  end
10
10
 
11
11
  def initialize(model_class, partition_key:, sort_key:, attributes:, conditions:)
@@ -17,6 +17,8 @@ module Dynamoid
17
17
  end
18
18
 
19
19
  def call
20
+ UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
+
20
22
  if Dynamoid::Config.timestamps
21
23
  @attributes[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
22
24
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Persistence
5
+ # @private
6
+ module UpdateValidations
7
+ def self.validate_attributes_exist(model_class, attributes)
8
+ model_attributes = model_class.attributes.keys
9
+
10
+ attributes.each do |attr_name, _|
11
+ unless model_attributes.include?(attr_name)
12
+ raise Dynamoid::Errors::UnknownAttribute.new("Attribute #{attr_name} does not exist in #{model_class}")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,8 +4,8 @@ module Dynamoid
4
4
  module Persistence
5
5
  # @private
6
6
  class Upsert
7
- def self.call(*args)
8
- new(*args).call
7
+ def self.call(*args, **options)
8
+ new(*args, **options).call
9
9
  end
10
10
 
11
11
  def initialize(model_class, partition_key:, sort_key:, attributes:, conditions:)
@@ -17,6 +17,8 @@ module Dynamoid
17
17
  end
18
18
 
19
19
  def call
20
+ UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
+
20
22
  if Dynamoid::Config.timestamps
21
23
  @attributes[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
22
24
  end
@@ -50,7 +50,10 @@ module Dynamoid
50
50
  class PresenceValidator < ActiveModel::EachValidator
51
51
  # Validate the record for the record and value.
52
52
  def validate_each(record, attr_name, value)
53
- record.errors.add(attr_name, :blank, options) if not_present?(value)
53
+ # Use keyword argument `options` because it was a Hash in Rails < 6.1
54
+ # and became a keyword argument in 6.1. This way it works in both
55
+ # cases.
56
+ record.errors.add(attr_name, :blank, **options) if not_present?(value)
54
57
  end
55
58
 
56
59
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
- VERSION = '3.6.0'
4
+ VERSION = '3.7.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.0
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Symonds
@@ -18,10 +18,10 @@ authors:
18
18
  - Brian Glusman
19
19
  - Peter Boling
20
20
  - Andrew Konchin
21
- autorequire:
21
+ autorequire:
22
22
  bindir: bin
23
23
  cert_chain: []
24
- date: 2020-07-13 00:00:00.000000000 Z
24
+ date: 2021-02-02 00:00:00.000000000 Z
25
25
  dependencies:
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activemodel
@@ -244,6 +244,7 @@ files:
244
244
  - lib/dynamoid/dynamodb_time_zone.rb
245
245
  - lib/dynamoid/errors.rb
246
246
  - lib/dynamoid/fields.rb
247
+ - lib/dynamoid/fields/declare.rb
247
248
  - lib/dynamoid/finders.rb
248
249
  - lib/dynamoid/identity_map.rb
249
250
  - lib/dynamoid/indexes.rb
@@ -254,6 +255,7 @@ files:
254
255
  - lib/dynamoid/persistence/import.rb
255
256
  - lib/dynamoid/persistence/save.rb
256
257
  - lib/dynamoid/persistence/update_fields.rb
258
+ - lib/dynamoid/persistence/update_validations.rb
257
259
  - lib/dynamoid/persistence/upsert.rb
258
260
  - lib/dynamoid/primary_key_type_mapping.rb
259
261
  - lib/dynamoid/railtie.rb
@@ -269,10 +271,10 @@ licenses:
269
271
  - MIT
270
272
  metadata:
271
273
  bug_tracker_uri: https://github.com/Dynamoid/dynamoid/issues
272
- changelog_uri: https://github.com/Dynamoid/dynamoid/tree/v3.6.0/CHANGELOG.md
273
- source_code_uri: https://github.com/Dynamoid/dynamoid/tree/v3.6.0
274
- documentation_uri: https://rubydoc.info/gems/dynamoid/3.6.0
275
- post_install_message:
274
+ changelog_uri: https://github.com/Dynamoid/dynamoid/tree/v3.7.0/CHANGELOG.md
275
+ source_code_uri: https://github.com/Dynamoid/dynamoid/tree/v3.7.0
276
+ documentation_uri: https://rubydoc.info/gems/dynamoid/3.7.0
277
+ post_install_message:
276
278
  rdoc_options: []
277
279
  require_paths:
278
280
  - lib
@@ -287,8 +289,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
287
289
  - !ruby/object:Gem::Version
288
290
  version: '0'
289
291
  requirements: []
290
- rubygems_version: 3.1.2
291
- signing_key:
292
+ rubygems_version: 3.2.0
293
+ signing_key:
292
294
  specification_version: 4
293
295
  summary: Dynamoid is an ORM for Amazon's DynamoDB
294
296
  test_files: []