dynamoid 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +18 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +15 -0
- data/README.md +113 -63
- data/Vagrantfile +2 -2
- data/docker-compose.yml +1 -1
- data/gemfiles/rails_4_2.gemfile +1 -1
- data/gemfiles/rails_5_0.gemfile +1 -1
- data/gemfiles/rails_5_1.gemfile +1 -1
- data/gemfiles/rails_5_2.gemfile +1 -1
- data/lib/dynamoid/adapter.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
- data/lib/dynamoid/associations/has_many.rb +1 -0
- data/lib/dynamoid/associations/has_one.rb +1 -0
- data/lib/dynamoid/associations/single_association.rb +1 -0
- data/lib/dynamoid/criteria.rb +4 -4
- data/lib/dynamoid/criteria/chain.rb +86 -79
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
- data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
- data/lib/dynamoid/document.rb +18 -13
- data/lib/dynamoid/dumping.rb +52 -40
- data/lib/dynamoid/fields.rb +4 -3
- data/lib/dynamoid/finders.rb +3 -3
- data/lib/dynamoid/persistence.rb +5 -6
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
- data/lib/dynamoid/tasks.rb +1 -0
- data/lib/dynamoid/tasks/database.rake +2 -2
- data/lib/dynamoid/type_casting.rb +37 -19
- data/lib/dynamoid/undumping.rb +53 -42
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +17 -5
- data/lib/dynamoid/adapter_plugin/query.rb +0 -144
- data/lib/dynamoid/adapter_plugin/scan.rb +0 -107
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 193003bce5df92dae1a2ad3a4f94345bc610193b0da7692eb6f722231e1becae
|
4
|
+
data.tar.gz: 36fb0bdea8126c6fd9af32343ad5d0ebe87ca92d01caab17fb9fe87aa74d07a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c53982905e1e487db42f0497f682e4a62b33d8adf1b1b7079e98f5d1635e338396aa62bef467483406ab558d4ad66a2a285ac72c90d5f278530026fbcb96fa1
|
7
|
+
data.tar.gz: 2d8e83e5ea4ba495ac6ec37c7178b8f77db4d0b2e33210c698ef00f8f92f1762672df3ca64c70e053f173d040e2abafe01bdd0c850499f65523fb9a50c2f0a7b
|
data/.rubocop.yml
CHANGED
@@ -8,12 +8,28 @@ AllCops:
|
|
8
8
|
# It's a matter of taste
|
9
9
|
Layout/AlignParameters:
|
10
10
|
EnforcedStyle: with_fixed_indentation
|
11
|
+
Layout/AlignHash:
|
12
|
+
Enabled: false
|
11
13
|
Style/GuardClause:
|
12
14
|
Enabled: false
|
13
15
|
Style/FormatStringToken:
|
14
16
|
Enabled: false
|
15
17
|
Style/DoubleNegation:
|
16
18
|
Enabled: false
|
19
|
+
Style/IfUnlessModifier:
|
20
|
+
Enabled: false
|
21
|
+
Style/EachWithObject:
|
22
|
+
Enabled: false
|
23
|
+
Style/SafeNavigation:
|
24
|
+
Enabled: false
|
25
|
+
Style/BlockDelimiters:
|
26
|
+
Enabled: false
|
27
|
+
Layout/MultilineMethodCallIndentation:
|
28
|
+
EnforcedStyle: indented
|
29
|
+
Naming/VariableNumber:
|
30
|
+
Enabled: false
|
31
|
+
Style/MultilineBlockChain:
|
32
|
+
Enabled: false
|
17
33
|
|
18
34
|
# We aren't so brave to tackle all these issues right now
|
19
35
|
Metrics/LineLength:
|
@@ -50,4 +66,6 @@ Style/MissingRespondToMissing:
|
|
50
66
|
Enabled: false
|
51
67
|
Naming/PredicateName:
|
52
68
|
Enabled: false
|
69
|
+
Security/YAMLLoad:
|
70
|
+
Enabled: false
|
53
71
|
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -10,6 +10,21 @@
|
|
10
10
|
|
11
11
|
|
12
12
|
|
13
|
+
# 3.2.0
|
14
|
+
|
15
|
+
## Features
|
16
|
+
* Feature: [#341](https://github.com/Dynamoid/dynamoid/pull/341), [#342](https://github.com/Dynamoid/dynamoid/pull/342) Add `find_by_pages` method to provide access to DynamoDB query result pagination mechanism (@bmalinconico, @arjes)
|
17
|
+
* Feature: [#354](https://github.com/Dynamoid/dynamoid/pull/354) Add `map` field type
|
18
|
+
|
19
|
+
## Improvements
|
20
|
+
* Improvement: [#340](https://github.com/Dynamoid/dynamoid/pull/340) Improve selecting more optimal GSI for Query operation - choose GSI with sort key if it's used in criteria (@ryz310)
|
21
|
+
* Improvement: [#351](https://github.com/Dynamoid/dynamoid/pull/351) Add warnings about nonexistent fields in `where` conditions
|
22
|
+
* Improvement: [#352](https://github.com/Dynamoid/dynamoid/pull/352) Add warning about skipped conditions
|
23
|
+
* Improvement: [#356](https://github.com/Dynamoid/dynamoid/pull/356) Simplify requiring Rake tasks in non-Rails application
|
24
|
+
* Improvement: Readme.md. Minor improvements and fixes (@cabello)
|
25
|
+
|
26
|
+
|
27
|
+
|
13
28
|
# 3.1.0
|
14
29
|
|
15
30
|
## Improvements
|
data/README.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# Dynamoid
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
[![Build Status](https://travis-ci.org/Dynamoid/dynamoid.svg?branch=master)](https://travis-ci.org/Dynamoid/dynamoid)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/Dynamoid/dynamoid.svg)](https://codeclimate.com/github/Dynamoid/dynamoid)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/github/Dynamoid/dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/dynamoid?branch=master)
|
6
|
+
[![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid)
|
7
|
+
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/github/Dynamoid/dynamoid/frames)
|
8
|
+
[![Inline docs](http://inch-ci.org/github/Dynamoid/Dynamoid.svg?branch=master)](http://inch-ci.org/github/Dynamoid/Dynamoid)
|
9
|
+
![GitHub](https://img.shields.io/github/license/Dynamoid/dynamoid.svg)
|
6
10
|
|
7
11
|
Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
|
8
12
|
provides similar functionality to ActiveRecord and improves on
|
@@ -14,27 +18,12 @@ DynamoDB is not like other document-based databases you might know, and is very
|
|
14
18
|
|
15
19
|
But if you want a fast, scalable, simple, easy-to-use database (and a Gem that supports it) then look no further!
|
16
20
|
|
17
|
-
|
18
|
-
| Project | Dynamoid |
|
19
|
-
|------------------------ | ----------------- |
|
20
|
-
| gem name | dynamoid |
|
21
|
-
| license | MIT |
|
22
|
-
| download rank | [![Total Downloads](https://img.shields.io/gem/rt/Dynamoid.svg)](https://rubygems.org/gems/dynamoid) |
|
23
|
-
| version | [![Gem Version](https://badge.fury.io/rb/dynamoid.svg)](https://badge.fury.io/rb/dynamoid) |
|
24
|
-
| dependencies | [![Depfu](https://badges.depfu.com/badges/6661c063c8e77a5008344fc7283a50aa/status.svg)](https://depfu.com) |
|
25
|
-
| code quality | [![Code Climate](https://codeclimate.com/github/Dynamoid/dynamoid.svg)](https://codeclimate.com/github/Dynamoid/dynamoid) |
|
26
|
-
| continuous integration | [![Build Status](https://travis-ci.org/Dynamoid/dynamoid.svg?branch=master)](https://travis-ci.org/Dynamoid/dynamoid) |
|
27
|
-
| test coverage | [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/Dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
|
28
|
-
| triage helpers | [![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid) |
|
29
|
-
| homepage | [https://github.com/Dynamoid/dynamoid](https://github.com/Dynamoid/dynamoid) |
|
30
|
-
| documentation | [http://rdoc.info/github/Dynamoid/dynamoid/frames](http://rdoc.info/github/Dynamoid/dynamoid/frames) |
|
31
|
-
|
32
21
|
## Installation
|
33
22
|
|
34
23
|
Installing Dynamoid is pretty simple. First include the Gem in your Gemfile:
|
35
24
|
|
36
25
|
```ruby
|
37
|
-
gem 'dynamoid'
|
26
|
+
gem 'dynamoid'
|
38
27
|
```
|
39
28
|
## Prerequisities
|
40
29
|
|
@@ -98,14 +87,9 @@ Then you need to initialize Dynamoid config to get it going. Put code similar to
|
|
98
87
|
|
99
88
|
Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
|
100
89
|
|
101
|
-
Its compatibility is tested
|
102
|
-
|
103
|
-
|
104
|
-
|:---------------------:|:-----:|:-----:|:-----:|:-----:|
|
105
|
-
| 2.3.7 | ✓ | ✓ | ✓ | ✓ |
|
106
|
-
| 2.4.4 | ✓ | ✓ | ✓ | ✓ |
|
107
|
-
| 2.5.1 | ✓ | ✓ | ✓ | ✓ |
|
108
|
-
| jruby-9.1.17.0 | ✓ | ✓ | ✓ | ✓ |
|
90
|
+
Its compatibility is tested against following Ruby versions: 2.3.8,
|
91
|
+
2.4.5, 2.5.3 and 2.6.1, JRuby versions 9.1.17.0 and 9.2.6.0 and
|
92
|
+
against Rails versions: 4.2.x, 5.0.x, 5.1.x and 5.2.x.
|
109
93
|
|
110
94
|
## Setup
|
111
95
|
|
@@ -137,7 +121,8 @@ These fields will not change an existing table: so specifying a new read_capacit
|
|
137
121
|
You'll have to define all the fields on the model and the data type of each field. Every field on the object must be included here; if you miss any they'll be completely bypassed during DynamoDB's initialization and will not appear on the model objects.
|
138
122
|
|
139
123
|
By default, fields are assumed to be of type `:string`. Other built-in types are
|
140
|
-
`:integer`, `:number`, `:set`, `:array`, `:datetime`, `date`, `:boolean`, `:raw` and `:serialized`.
|
124
|
+
`:integer`, `:number`, `:set`, `:array`, `:map`, `:datetime`, `date`, `:boolean`, `:raw` and `:serialized`.
|
125
|
+
`array` and `map` match List and Map DynamoDB types respectively.
|
141
126
|
`raw` type means you can store Ruby Array, Hash, String and numbers.
|
142
127
|
If built-in types do not suit you, you can use a custom field type represented by an arbitrary class, provided that the class supports a compatible serialization interface.
|
143
128
|
The primary use case for using a custom field type is to represent your business logic with high-level types, while ensuring portability or backward-compatibility of the serialized representation.
|
@@ -181,7 +166,7 @@ class Document
|
|
181
166
|
end
|
182
167
|
```
|
183
168
|
|
184
|
-
WARNING
|
169
|
+
**WARNING:** Fields in numeric format are stored with nanoseconds as a fraction part and precision could be lost.
|
185
170
|
That's why `datetime` field in numeric format shouldn't be used as a range key.
|
186
171
|
|
187
172
|
You have two options if you need to use a `datetime` field as a range key:
|
@@ -547,6 +532,18 @@ u.addresses.where(city: 'Chicago').all
|
|
547
532
|
|
548
533
|
But keep in mind Dynamoid -- and document-based storage systems in general -- are not drop-in replacements for existing relational databases. The above query does not efficiently perform a conditional join, but instead finds all the user's addresses and naively filters them in Ruby. For large associations this is a performance hit compared to relational database engines.
|
549
534
|
|
535
|
+
**WARNING:** There is a limitation of conditions passed to `where`
|
536
|
+
method. Only one condition for some particular field could be specified.
|
537
|
+
The last one only will be applyed and others will be ignored. E.g. in
|
538
|
+
examples:
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
User.where('age.gt': 10, 'age.lt': 20)
|
542
|
+
User.where(name: 'Mike').where('name.begins_with': 'Ed')
|
543
|
+
```
|
544
|
+
|
545
|
+
the first one will be ignored and the last one will be used.
|
546
|
+
|
550
547
|
#### Limits
|
551
548
|
|
552
549
|
There are three types of limits that you can query with:
|
@@ -585,6 +582,42 @@ Address.record_limit(10_000).batch(100).each { … } # Batch specified as part o
|
|
585
582
|
The implication of batches is that the underlying requests are done in the batch sizes to make the request and responses
|
586
583
|
more manageable. Note that this batching is for `Query` and `Scans` and not `BatchGetItem` commands.
|
587
584
|
|
585
|
+
#### DynamoDB pagination
|
586
|
+
|
587
|
+
At times it can be useful to rely on DynamoDB [low-level pagination](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Pagination)
|
588
|
+
instead of fixed pages sizes. Each page results in a single Query or Scan call
|
589
|
+
to DyanmoDB, but returns an unknown number of records.
|
590
|
+
|
591
|
+
Access to the native DynamoDB pages can be obtained via the `find_by_pages`
|
592
|
+
method, which yields arrays of records.
|
593
|
+
|
594
|
+
```ruby
|
595
|
+
Address.find_by_pages do |addresses, metadata|
|
596
|
+
end
|
597
|
+
```
|
598
|
+
|
599
|
+
Each yielded pages returns page metadata as the second argument, which is a hash
|
600
|
+
including a key `:last_evaluated_key`. The value of this key can be used for
|
601
|
+
the `start` method to fetch the next page of records.
|
602
|
+
|
603
|
+
This way it can be used for instance to implement efficiently
|
604
|
+
pagination in web-application:
|
605
|
+
|
606
|
+
```ruby
|
607
|
+
class UserController < ApplicationController
|
608
|
+
def index
|
609
|
+
next_page = params[:next_page_token] ? JSON.parse(Base64.decode64(params[:next_page_token])) : nil
|
610
|
+
|
611
|
+
records, metadata = User.start(next_page).find_by_pages.first
|
612
|
+
|
613
|
+
render json: {
|
614
|
+
records: records,
|
615
|
+
next_page_token: Base64.encode64(metadata[:last_evaluated_key].to_json)
|
616
|
+
}
|
617
|
+
end
|
618
|
+
end
|
619
|
+
```
|
620
|
+
|
588
621
|
#### Sort Conditions and Filters
|
589
622
|
|
590
623
|
You are able to optimize query with condition for sort key. Following operators are available: `gt`, `lt`, `gte`, `lte`,
|
@@ -684,7 +717,7 @@ class User
|
|
684
717
|
field :name
|
685
718
|
field :age, :number
|
686
719
|
|
687
|
-
global_secondary_index hash_key: :age
|
720
|
+
global_secondary_index hash_key: :age # Must come after field definitions.
|
688
721
|
end
|
689
722
|
```
|
690
723
|
|
@@ -698,44 +731,17 @@ There are following options:
|
|
698
731
|
|
699
732
|
The only mandatory option is `name`.
|
700
733
|
|
701
|
-
|
702
|
-
|
703
|
-
There are two ways to query Global Secondary Indexes (GSI).
|
704
|
-
|
705
|
-
#### Explicit
|
706
|
-
|
707
|
-
The first way explicitly uses your GSI and utilizes the `find_all_by_secondary_index` method which will lookup a valid
|
708
|
-
GSI to use based on the inputs, you MUST provide the correct keys to match the GSI you want:
|
734
|
+
**WARNING:** In order to use global secondary index in `Document.where` implicitly you need to have all the attributes of the original table in the index and declare it with option `projected_attributes: :all`:
|
709
735
|
|
710
736
|
```ruby
|
711
|
-
|
712
|
-
|
713
|
-
dynamo_primary_key_column_name => dynamo_primary_key_value
|
714
|
-
}, # The signature of find_all_by_secondary_index is ugly, so must be an explicit hash here
|
715
|
-
:range => {
|
716
|
-
"#{range_column}.#{range_modifier}" => range_value
|
717
|
-
},
|
718
|
-
# false is the same as DESC in SQL (newest timestamp first)
|
719
|
-
# true is the same as ASC in SQL (oldest timestamp first)
|
720
|
-
scan_index_forward: false # or true
|
721
|
-
)
|
722
|
-
```
|
723
|
-
|
724
|
-
Where the range modifier is one of `Dynamoid::Finders::RANGE_MAP.keys`, where the `RANGE_MAP` is:
|
737
|
+
class User
|
738
|
+
# ...
|
725
739
|
|
726
|
-
|
727
|
-
|
728
|
-
'gt' => :range_greater_than,
|
729
|
-
'lt' => :range_less_than,
|
730
|
-
'gte' => :range_gte,
|
731
|
-
'lte' => :range_lte,
|
732
|
-
'begins_with' => :range_begins_with,
|
733
|
-
'between' => :range_between,
|
734
|
-
'eq' => :range_eq
|
735
|
-
}
|
740
|
+
global_secondary_index hash_key: :age, projected_attributes: :all
|
741
|
+
end
|
736
742
|
```
|
737
743
|
|
738
|
-
|
744
|
+
There is only one implicit way to query Global and Local Secondary Indexes (GSI/LSI).
|
739
745
|
|
740
746
|
#### Implicit
|
741
747
|
|
@@ -791,6 +797,10 @@ Listed below are all configuration options.
|
|
791
797
|
`'t'` and `'f'`. Default is true
|
792
798
|
* `backoff` - is a hash: key is a backoff strategy (symbol), value is parameters for the strategy. Is used in batch operations. Default id `nil`
|
793
799
|
* `backoff_strategies`: is a hash and contains all available strategies. Default is { constant: ..., exponential: ...}
|
800
|
+
* `http_continue_timeout`: The number of seconds to wait for a 100-continue HTTP response before sending the request body. Default option value is `nil`. If not specified effected value is `1`
|
801
|
+
* `http_idle_timeout`: The number of seconds an HTTP connection is allowed to sit idble before it is considered stale. Default option value is `nil`. If not specified effected value is `5`
|
802
|
+
* `http_open_timeout`: The number of seconds to wait when opening a HTTP session. Default option value is `nil`. If not specified effected value is `15`
|
803
|
+
* `http_read_timeout`:The number of seconds to wait for HTTP response data. Default option value is `nil`. If not specified effected value is `60`
|
794
804
|
|
795
805
|
|
796
806
|
## Concurrency
|
@@ -859,9 +869,23 @@ end
|
|
859
869
|
|
860
870
|
## Rake Tasks
|
861
871
|
|
872
|
+
There are a few Rake tasks available out of the box:
|
873
|
+
|
862
874
|
* `rake dynamoid:create_tables`
|
863
875
|
* `rake dynamoid:ping`
|
864
876
|
|
877
|
+
In order to use them in non-Rails application they should be required explicitly:
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
# Rakefile
|
881
|
+
|
882
|
+
Rake::Task.define_task(:environment)
|
883
|
+
require 'dynamoid/tasks'
|
884
|
+
```
|
885
|
+
|
886
|
+
The Rake tasks depend on `:environment` task so it should be declared
|
887
|
+
as well.
|
888
|
+
|
865
889
|
## Test Environment
|
866
890
|
|
867
891
|
In test environment you will most likely want to clean the database between test runs to keep tests completely isolated. This can be achieved like so
|
@@ -904,6 +928,32 @@ Dynamoid.configure do |config|
|
|
904
928
|
end
|
905
929
|
```
|
906
930
|
|
931
|
+
## Logging
|
932
|
+
|
933
|
+
There is a config option `logger`. Dynamoid writes requests and
|
934
|
+
responses to DynamoDB using this logger on the `debug` level. So in
|
935
|
+
order to troubleshoot and debug issues just set it:
|
936
|
+
|
937
|
+
```ruby
|
938
|
+
class User
|
939
|
+
include Dynamoid::Document
|
940
|
+
field name
|
941
|
+
end
|
942
|
+
|
943
|
+
Dynamoid.config.logger.level = :debug
|
944
|
+
Dynamoid.config.endpoint = 'localhost:8000'
|
945
|
+
|
946
|
+
User.create(name: 'Alex')
|
947
|
+
|
948
|
+
# => D, [2019-05-12T20:01:07.840051 #75059] DEBUG -- : put_item | Request "{\"TableName\":\"dynamoid_users\",\"Item\":{\"created_at\":{\"N\":\"1557680467.608749\"},\"updated_at\":{\"N\":\"1557680467.608809\"},\"id\":{\"S\":\"1227eea7-2c96-4b8a-90d9-77b38eb85cd0\"}},\"Expected\":{\"id\":{\"Exists\":false}}}" | Response "{}"
|
949
|
+
|
950
|
+
# => D, [2019-05-12T20:01:07.842397 #75059] DEBUG -- : (231.28 ms) PUT ITEM - ["dynamoid_users", {:created_at=>0.1557680467608749e10, :updated_at=>0.1557680467608809e10, :id=>"1227eea7-2c96-4b8a-90d9-77b38eb85cd0", :User=>nil}, {}]
|
951
|
+
```
|
952
|
+
|
953
|
+
The first line is a body of HTTP request and response. The second line -
|
954
|
+
Dynamoid internal logging of API call (`PUT ITEM` in our case) with
|
955
|
+
timing (231.28 ms).
|
956
|
+
|
907
957
|
## Credits
|
908
958
|
|
909
959
|
Dynamoid borrows code, structure, and even its name very liberally from the truly amazing [Mongoid](https://github.com/mongoid/mongoid). Without Mongoid to crib from none of this would have been possible, and I hope they don't mind me reusing their very awesome ideas to make DynamoDB just as accessible to the Ruby world as MongoDB.
|
data/Vagrantfile
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Vagrant.configure('2') do |config|
|
4
4
|
# Choose base box
|
5
|
-
config.vm.box = 'bento/ubuntu-
|
5
|
+
config.vm.box = 'bento/ubuntu-18.04'
|
6
6
|
|
7
7
|
config.vm.provider 'virtualbox' do |vb|
|
8
8
|
# Prevent clock skew when host goes to sleep while VM is running
|
@@ -20,7 +20,7 @@ Vagrant.configure('2') do |config|
|
|
20
20
|
# Pillars
|
21
21
|
salt.pillar(
|
22
22
|
'ruby' => {
|
23
|
-
'version' => '2.
|
23
|
+
'version' => '2.6.2'
|
24
24
|
}
|
25
25
|
)
|
26
26
|
|
data/docker-compose.yml
CHANGED
data/gemfiles/rails_4_2.gemfile
CHANGED
data/gemfiles/rails_5_0.gemfile
CHANGED
data/gemfiles/rails_5_1.gemfile
CHANGED
data/gemfiles/rails_5_2.gemfile
CHANGED
data/lib/dynamoid/adapter.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'query'
|
4
|
-
require_relative 'scan'
|
3
|
+
require_relative 'aws_sdk_v3/query'
|
4
|
+
require_relative 'aws_sdk_v3/scan'
|
5
|
+
require_relative 'aws_sdk_v3/create_table'
|
6
|
+
require_relative 'aws_sdk_v3/item_updater'
|
7
|
+
require_relative 'aws_sdk_v3/table'
|
8
|
+
require_relative 'aws_sdk_v3/until_past_table_status'
|
5
9
|
|
6
10
|
module Dynamoid
|
7
11
|
module AdapterPlugin
|
@@ -187,6 +191,7 @@ module Dynamoid
|
|
187
191
|
|
188
192
|
table_ids.each do |t, ids|
|
189
193
|
next if ids.blank?
|
194
|
+
|
190
195
|
ids = Array(ids).dup
|
191
196
|
tbl = describe_table(t)
|
192
197
|
hk = tbl.hash_key.to_s
|
@@ -293,58 +298,7 @@ module Dynamoid
|
|
293
298
|
# @since 1.0.0
|
294
299
|
def create_table(table_name, key = :id, options = {})
|
295
300
|
Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
|
296
|
-
|
297
|
-
write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
|
298
|
-
|
299
|
-
secondary_indexes = options.slice(
|
300
|
-
:local_secondary_indexes,
|
301
|
-
:global_secondary_indexes
|
302
|
-
)
|
303
|
-
ls_indexes = options[:local_secondary_indexes]
|
304
|
-
gs_indexes = options[:global_secondary_indexes]
|
305
|
-
|
306
|
-
key_schema = {
|
307
|
-
hash_key_schema: { key => (options[:hash_key_type] || :string) },
|
308
|
-
range_key_schema: options[:range_key]
|
309
|
-
}
|
310
|
-
attribute_definitions = build_all_attribute_definitions(
|
311
|
-
key_schema,
|
312
|
-
secondary_indexes
|
313
|
-
)
|
314
|
-
key_schema = aws_key_schema(
|
315
|
-
key_schema[:hash_key_schema],
|
316
|
-
key_schema[:range_key_schema]
|
317
|
-
)
|
318
|
-
|
319
|
-
client_opts = {
|
320
|
-
table_name: table_name,
|
321
|
-
provisioned_throughput: {
|
322
|
-
read_capacity_units: read_capacity,
|
323
|
-
write_capacity_units: write_capacity
|
324
|
-
},
|
325
|
-
key_schema: key_schema,
|
326
|
-
attribute_definitions: attribute_definitions
|
327
|
-
}
|
328
|
-
|
329
|
-
if ls_indexes.present?
|
330
|
-
client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
|
331
|
-
index_to_aws_hash(index)
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
if gs_indexes.present?
|
336
|
-
client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
|
337
|
-
index_to_aws_hash(index)
|
338
|
-
end
|
339
|
-
end
|
340
|
-
resp = client.create_table(client_opts)
|
341
|
-
options[:sync] = true if !options.key?(:sync) && ls_indexes.present? || gs_indexes.present?
|
342
|
-
until_past_table_status(table_name, :creating) if options[:sync] &&
|
343
|
-
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
344
|
-
status == TABLE_STATUSES[:creating]
|
345
|
-
# Response to original create_table, which, if options[:sync]
|
346
|
-
# may have an outdated table_description.table_status of "CREATING"
|
347
|
-
resp
|
301
|
+
CreateTable.new(client, table_name, key, options).call
|
348
302
|
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
349
303
|
Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
|
350
304
|
end
|
@@ -402,9 +356,9 @@ module Dynamoid
|
|
402
356
|
# @since 1.0.0
|
403
357
|
def delete_table(table_name, options = {})
|
404
358
|
resp = client.delete_table(table_name: table_name)
|
405
|
-
|
406
|
-
|
407
|
-
|
359
|
+
UntilPastTableStatus.new(table_name, :deleting).call if options[:sync] &&
|
360
|
+
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
361
|
+
status == TABLE_STATUSES[:deleting]
|
408
362
|
table_cache.delete(table_name)
|
409
363
|
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
410
364
|
Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
|
@@ -461,6 +415,7 @@ module Dynamoid
|
|
461
415
|
yield(iu = ItemUpdater.new(table, key, range_key))
|
462
416
|
|
463
417
|
raise "non-empty options: #{options}" unless options.empty?
|
418
|
+
|
464
419
|
begin
|
465
420
|
result = client.update_item(table_name: table_name,
|
466
421
|
key: key_stanza(table, key, range_key),
|
@@ -532,11 +487,14 @@ module Dynamoid
|
|
532
487
|
#
|
533
488
|
# @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
|
534
489
|
def query(table_name, options = {})
|
535
|
-
table = describe_table(table_name)
|
536
|
-
|
537
490
|
Enumerator.new do |yielder|
|
491
|
+
table = describe_table(table_name)
|
492
|
+
|
538
493
|
Query.new(client, table, options).call.each do |page|
|
539
|
-
|
494
|
+
yielder.yield(
|
495
|
+
page.items.map { |row| result_item_to_hash(row) },
|
496
|
+
last_evaluated_key: page.last_evaluated_key
|
497
|
+
)
|
540
498
|
end
|
541
499
|
end
|
542
500
|
end
|
@@ -562,11 +520,14 @@ module Dynamoid
|
|
562
520
|
#
|
563
521
|
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
564
522
|
def scan(table_name, conditions = {}, options = {})
|
565
|
-
table = describe_table(table_name)
|
566
|
-
|
567
523
|
Enumerator.new do |yielder|
|
524
|
+
table = describe_table(table_name)
|
525
|
+
|
568
526
|
Scan.new(client, table, conditions, options).call.each do |page|
|
569
|
-
|
527
|
+
yielder.yield(
|
528
|
+
page.items.map { |row| result_item_to_hash(row) },
|
529
|
+
last_evaluated_key: page.last_evaluated_key
|
530
|
+
)
|
570
531
|
end
|
571
532
|
end
|
572
533
|
end
|
@@ -591,7 +552,7 @@ module Dynamoid
|
|
591
552
|
hk = table.hash_key
|
592
553
|
rk = table.range_key
|
593
554
|
|
594
|
-
scan(table_name, {}, {}).each do |attributes|
|
555
|
+
scan(table_name, {}, {}).flat_map{ |i| i }.each do |attributes|
|
595
556
|
opts = {}
|
596
557
|
opts[:range_key] = attributes[rk.to_sym] if rk
|
597
558
|
delete_item(table_name, attributes[hk], opts)
|
@@ -604,59 +565,6 @@ module Dynamoid
|
|
604
565
|
|
605
566
|
protected
|
606
567
|
|
607
|
-
def check_table_status?(counter, resp, expect_status)
|
608
|
-
status = PARSE_TABLE_STATUS.call(resp)
|
609
|
-
again = counter < Dynamoid::Config.sync_retry_max_times &&
|
610
|
-
status == TABLE_STATUSES[expect_status]
|
611
|
-
{ again: again, status: status, counter: counter }
|
612
|
-
end
|
613
|
-
|
614
|
-
def until_past_table_status(table_name, status = :creating)
|
615
|
-
counter = 0
|
616
|
-
resp = nil
|
617
|
-
begin
|
618
|
-
check = { again: true }
|
619
|
-
while check[:again]
|
620
|
-
sleep Dynamoid::Config.sync_retry_wait_seconds
|
621
|
-
resp = client.describe_table(table_name: table_name)
|
622
|
-
check = check_table_status?(counter, resp, status)
|
623
|
-
Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
|
624
|
-
counter += 1
|
625
|
-
end
|
626
|
-
# If you issue a DescribeTable request immediately after a CreateTable
|
627
|
-
# request, DynamoDB might return a ResourceNotFoundException.
|
628
|
-
# This is because DescribeTable uses an eventually consistent query,
|
629
|
-
# and the metadata for your table might not be available at that moment.
|
630
|
-
# Wait for a few seconds, and then try the DescribeTable request again.
|
631
|
-
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method
|
632
|
-
rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
|
633
|
-
case status
|
634
|
-
when :creating then
|
635
|
-
if counter >= Dynamoid::Config.sync_retry_max_times
|
636
|
-
Dynamoid.logger.warn "Waiting on table metadata for #{table_name} (check #{counter})"
|
637
|
-
retry # start over at first line of begin, does not reset counter
|
638
|
-
else
|
639
|
-
Dynamoid.logger.error "Exhausted max retries (Dynamoid::Config.sync_retry_max_times) waiting on table metadata for #{table_name} (check #{counter})"
|
640
|
-
raise e
|
641
|
-
end
|
642
|
-
else
|
643
|
-
# When deleting a table, "not found" is the goal.
|
644
|
-
Dynamoid.logger.info "Checked table status for #{table_name}: Not Found (check #{check.inspect})"
|
645
|
-
end
|
646
|
-
end
|
647
|
-
end
|
648
|
-
|
649
|
-
# Converts from symbol to the API string for the given data type
|
650
|
-
# E.g. :number -> 'N'
|
651
|
-
def api_type(type)
|
652
|
-
case type
|
653
|
-
when :string then STRING_TYPE
|
654
|
-
when :number then NUM_TYPE
|
655
|
-
when :binary then BINARY_TYPE
|
656
|
-
else raise "Unknown type: #{type}"
|
657
|
-
end
|
658
|
-
end
|
659
|
-
|
660
568
|
#
|
661
569
|
# The key hash passed on get_item, put_item, delete_item, update_item, etc
|
662
570
|
#
|
@@ -706,152 +614,6 @@ module Dynamoid
|
|
706
614
|
end
|
707
615
|
end
|
708
616
|
|
709
|
-
# Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
|
710
|
-
# This resulting hash is of the form:
|
711
|
-
#
|
712
|
-
# {
|
713
|
-
# index_name: String
|
714
|
-
# keys: {
|
715
|
-
# hash_key: aws_key_schema (hash)
|
716
|
-
# range_key: aws_key_schema (hash)
|
717
|
-
# }
|
718
|
-
# projection: {
|
719
|
-
# projection_type: (ALL, KEYS_ONLY, INCLUDE) String
|
720
|
-
# non_key_attributes: (optional) Array
|
721
|
-
# }
|
722
|
-
# provisioned_throughput: {
|
723
|
-
# read_capacity_units: Integer
|
724
|
-
# write_capacity_units: Integer
|
725
|
-
# }
|
726
|
-
# }
|
727
|
-
#
|
728
|
-
# @param [Dynamoid::Indexes::Index] index the index.
|
729
|
-
# @return [Hash] hash representing an AWS Index definition.
|
730
|
-
def index_to_aws_hash(index)
|
731
|
-
key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
|
732
|
-
|
733
|
-
hash = {
|
734
|
-
index_name: index.name,
|
735
|
-
key_schema: key_schema,
|
736
|
-
projection: {
|
737
|
-
projection_type: index.projection_type.to_s.upcase
|
738
|
-
}
|
739
|
-
}
|
740
|
-
|
741
|
-
# If the projection type is include, specify the non key attributes
|
742
|
-
if index.projection_type == :include
|
743
|
-
hash[:projection][:non_key_attributes] = index.projected_attributes
|
744
|
-
end
|
745
|
-
|
746
|
-
# Only global secondary indexes have a separate throughput.
|
747
|
-
if index.type == :global_secondary
|
748
|
-
hash[:provisioned_throughput] = {
|
749
|
-
read_capacity_units: index.read_capacity,
|
750
|
-
write_capacity_units: index.write_capacity
|
751
|
-
}
|
752
|
-
end
|
753
|
-
hash
|
754
|
-
end
|
755
|
-
|
756
|
-
# Converts hash_key_schema and range_key_schema to aws_key_schema
|
757
|
-
# @param [Hash] hash_key_schema eg: {:id => :string}
|
758
|
-
# @param [Hash] range_key_schema eg: {:created_at => :number}
|
759
|
-
# @return [Array]
|
760
|
-
def aws_key_schema(hash_key_schema, range_key_schema)
|
761
|
-
schema = [{
|
762
|
-
attribute_name: hash_key_schema.keys.first.to_s,
|
763
|
-
key_type: HASH_KEY
|
764
|
-
}]
|
765
|
-
|
766
|
-
if range_key_schema.present?
|
767
|
-
schema << {
|
768
|
-
attribute_name: range_key_schema.keys.first.to_s,
|
769
|
-
key_type: RANGE_KEY
|
770
|
-
}
|
771
|
-
end
|
772
|
-
schema
|
773
|
-
end
|
774
|
-
|
775
|
-
# Builds aws attributes definitions based off of primary hash/range and
|
776
|
-
# secondary indexes
|
777
|
-
#
|
778
|
-
# @param key_data
|
779
|
-
# @option key_data [Hash] hash_key_schema - eg: {:id => :string}
|
780
|
-
# @option key_data [Hash] range_key_schema - eg: {:created_at => :number}
|
781
|
-
# @param [Hash] secondary_indexes
|
782
|
-
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
|
783
|
-
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
|
784
|
-
def build_all_attribute_definitions(key_schema, secondary_indexes = {})
|
785
|
-
ls_indexes = secondary_indexes[:local_secondary_indexes]
|
786
|
-
gs_indexes = secondary_indexes[:global_secondary_indexes]
|
787
|
-
|
788
|
-
attribute_definitions = []
|
789
|
-
|
790
|
-
attribute_definitions << build_attribute_definitions(
|
791
|
-
key_schema[:hash_key_schema],
|
792
|
-
key_schema[:range_key_schema]
|
793
|
-
)
|
794
|
-
|
795
|
-
if ls_indexes.present?
|
796
|
-
ls_indexes.map do |index|
|
797
|
-
attribute_definitions << build_attribute_definitions(
|
798
|
-
index.hash_key_schema,
|
799
|
-
index.range_key_schema
|
800
|
-
)
|
801
|
-
end
|
802
|
-
end
|
803
|
-
|
804
|
-
if gs_indexes.present?
|
805
|
-
gs_indexes.map do |index|
|
806
|
-
attribute_definitions << build_attribute_definitions(
|
807
|
-
index.hash_key_schema,
|
808
|
-
index.range_key_schema
|
809
|
-
)
|
810
|
-
end
|
811
|
-
end
|
812
|
-
|
813
|
-
attribute_definitions.flatten!
|
814
|
-
# uniq these definitions because range keys might be common between
|
815
|
-
# primary and secondary indexes
|
816
|
-
attribute_definitions.uniq!
|
817
|
-
attribute_definitions
|
818
|
-
end
|
819
|
-
|
820
|
-
# Builds an attribute definitions based on hash key and range key
|
821
|
-
# @params [Hash] hash_key_schema - eg: {:id => :string}
|
822
|
-
# @params [Hash] range_key_schema - eg: {:created_at => :datetime}
|
823
|
-
# @return [Array]
|
824
|
-
def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
|
825
|
-
attrs = []
|
826
|
-
|
827
|
-
attrs << attribute_definition_element(
|
828
|
-
hash_key_schema.keys.first,
|
829
|
-
hash_key_schema.values.first
|
830
|
-
)
|
831
|
-
|
832
|
-
if range_key_schema.present?
|
833
|
-
attrs << attribute_definition_element(
|
834
|
-
range_key_schema.keys.first,
|
835
|
-
range_key_schema.values.first
|
836
|
-
)
|
837
|
-
end
|
838
|
-
|
839
|
-
attrs
|
840
|
-
end
|
841
|
-
|
842
|
-
# Builds an aws attribute definition based on name and dynamoid type
|
843
|
-
# @params [Symbol] name - eg: :id
|
844
|
-
# @params [Symbol] dynamoid_type - eg: :string
|
845
|
-
# @return [Hash]
|
846
|
-
def attribute_definition_element(name, dynamoid_type)
|
847
|
-
aws_type = api_type(dynamoid_type)
|
848
|
-
|
849
|
-
{
|
850
|
-
attribute_name: name.to_s,
|
851
|
-
attribute_type: aws_type
|
852
|
-
}
|
853
|
-
end
|
854
|
-
|
855
617
|
# Build an array of values for Condition
|
856
618
|
# Is used in ScanFilter and QueryFilter
|
857
619
|
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
|
@@ -871,139 +633,8 @@ module Dynamoid
|
|
871
633
|
end
|
872
634
|
end
|
873
635
|
|
874
|
-
#
|
875
|
-
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
876
|
-
# provides methods for coercing values to the proper types based on the table's schema data
|
877
|
-
#
|
878
|
-
class Table
|
879
|
-
attr_reader :schema
|
880
|
-
|
881
|
-
#
|
882
|
-
# @param [Hash] schema Data returns from a "DescribeTable" call
|
883
|
-
#
|
884
|
-
def initialize(schema)
|
885
|
-
@schema = schema[:table]
|
886
|
-
end
|
887
|
-
|
888
|
-
def range_key
|
889
|
-
@range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
|
890
|
-
end
|
891
|
-
|
892
|
-
def range_type
|
893
|
-
range_type ||= schema[:attribute_definitions].find do |d|
|
894
|
-
d[:attribute_name] == range_key
|
895
|
-
end.try(:fetch, :attribute_type, nil)
|
896
|
-
end
|
897
|
-
|
898
|
-
def hash_key
|
899
|
-
@hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
|
900
|
-
end
|
901
|
-
|
902
|
-
#
|
903
|
-
# Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
|
904
|
-
# nil otherwise
|
905
|
-
#
|
906
|
-
def col_type(col)
|
907
|
-
col = col.to_s
|
908
|
-
col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
|
909
|
-
col_def && col_def[:attribute_type]
|
910
|
-
end
|
911
|
-
|
912
|
-
def item_count
|
913
|
-
schema[:item_count]
|
914
|
-
end
|
915
|
-
|
916
|
-
def name
|
917
|
-
schema[:table_name]
|
918
|
-
end
|
919
|
-
end
|
920
|
-
|
921
|
-
#
|
922
|
-
# Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
|
923
|
-
#
|
924
|
-
class ItemUpdater
|
925
|
-
attr_reader :table, :key, :range_key
|
926
|
-
|
927
|
-
def initialize(table, key, range_key = nil)
|
928
|
-
@table = table
|
929
|
-
@key = key
|
930
|
-
@range_key = range_key
|
931
|
-
@additions = {}
|
932
|
-
@deletions = {}
|
933
|
-
@updates = {}
|
934
|
-
end
|
935
|
-
|
936
|
-
#
|
937
|
-
# Adds the given values to the values already stored in the corresponding columns.
|
938
|
-
# The column must contain a Set or a number.
|
939
|
-
#
|
940
|
-
# @param [Hash] vals keys of the hash are the columns to update, vals are the values to
|
941
|
-
# add. values must be a Set, Array, or Numeric
|
942
|
-
#
|
943
|
-
def add(values)
|
944
|
-
@additions.merge!(sanitize_attributes(values))
|
945
|
-
end
|
946
|
-
|
947
|
-
#
|
948
|
-
# Removes values from the sets of the given columns
|
949
|
-
#
|
950
|
-
# @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
|
951
|
-
# to remove
|
952
|
-
#
|
953
|
-
def delete(values)
|
954
|
-
@deletions.merge!(sanitize_attributes(values))
|
955
|
-
end
|
956
|
-
|
957
|
-
#
|
958
|
-
# Replaces the values of one or more attributes
|
959
|
-
#
|
960
|
-
def set(values)
|
961
|
-
@updates.merge!(sanitize_attributes(values))
|
962
|
-
end
|
963
|
-
|
964
|
-
#
|
965
|
-
# Returns an AttributeUpdates hash suitable for passing to the V2 Client API
|
966
|
-
#
|
967
|
-
def to_h
|
968
|
-
ret = {}
|
969
|
-
|
970
|
-
@additions.each do |k, v|
|
971
|
-
ret[k.to_s] = {
|
972
|
-
action: ADD,
|
973
|
-
value: v
|
974
|
-
}
|
975
|
-
end
|
976
|
-
@deletions.each do |k, v|
|
977
|
-
ret[k.to_s] = {
|
978
|
-
action: DELETE,
|
979
|
-
value: v
|
980
|
-
}
|
981
|
-
end
|
982
|
-
@updates.each do |k, v|
|
983
|
-
ret[k.to_s] = {
|
984
|
-
action: PUT,
|
985
|
-
value: v
|
986
|
-
}
|
987
|
-
end
|
988
|
-
|
989
|
-
ret
|
990
|
-
end
|
991
|
-
|
992
|
-
private
|
993
|
-
|
994
|
-
def sanitize_attributes(attributes)
|
995
|
-
attributes.transform_values do |v|
|
996
|
-
v.is_a?(Hash) ? v.stringify_keys : v
|
997
|
-
end
|
998
|
-
end
|
999
|
-
|
1000
|
-
ADD = 'ADD'
|
1001
|
-
DELETE = 'DELETE'
|
1002
|
-
PUT = 'PUT'
|
1003
|
-
end
|
1004
|
-
|
1005
636
|
def sanitize_item(attributes)
|
1006
|
-
attributes.reject do |
|
637
|
+
attributes.reject do |_, v|
|
1007
638
|
v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
1008
639
|
end.transform_values do |v|
|
1009
640
|
v.is_a?(Hash) ? v.stringify_keys : v
|