dynamoid 1.3.4 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +3 -0
- data/.travis.yml +37 -7
- data/Appraisals +11 -0
- data/CHANGELOG.md +115 -2
- data/Gemfile +2 -0
- data/LICENSE.txt +18 -16
- data/README.md +253 -34
- data/Rakefile +0 -24
- data/Vagrantfile +1 -1
- data/docker-compose.yml +7 -0
- data/dynamoid.gemspec +4 -4
- data/gemfiles/rails_4_0.gemfile +3 -3
- data/gemfiles/rails_4_1.gemfile +3 -3
- data/gemfiles/rails_4_2.gemfile +3 -3
- data/gemfiles/rails_5_0.gemfile +2 -1
- data/gemfiles/rails_5_1.gemfile +8 -0
- data/gemfiles/rails_5_2.gemfile +8 -0
- data/lib/dynamoid.rb +31 -31
- data/lib/dynamoid/adapter.rb +14 -10
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +188 -100
- data/lib/dynamoid/associations.rb +21 -12
- data/lib/dynamoid/associations/association.rb +19 -3
- data/lib/dynamoid/associations/belongs_to.rb +26 -16
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -16
- data/lib/dynamoid/associations/has_many.rb +2 -17
- data/lib/dynamoid/associations/has_one.rb +0 -14
- data/lib/dynamoid/associations/many_association.rb +19 -6
- data/lib/dynamoid/associations/single_association.rb +25 -7
- data/lib/dynamoid/config.rb +37 -18
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +11 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +25 -0
- data/lib/dynamoid/config/options.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +48 -32
- data/lib/dynamoid/dirty.rb +23 -4
- data/lib/dynamoid/document.rb +88 -5
- data/lib/dynamoid/errors.rb +4 -1
- data/lib/dynamoid/fields.rb +6 -6
- data/lib/dynamoid/finders.rb +42 -12
- data/lib/dynamoid/identity_map.rb +0 -1
- data/lib/dynamoid/indexes.rb +41 -54
- data/lib/dynamoid/persistence.rb +151 -40
- data/lib/dynamoid/railtie.rb +1 -1
- data/lib/dynamoid/validations.rb +4 -3
- data/lib/dynamoid/version.rb +1 -1
- metadata +18 -29
- data/gemfiles/rails_4_0.gemfile.lock +0 -150
- data/gemfiles/rails_4_1.gemfile.lock +0 -154
- data/gemfiles/rails_4_2.gemfile.lock +0 -175
- data/gemfiles/rails_5_0.gemfile.lock +0 -180
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 542fc1c696af080fcd65a7428ec553584d3fe48d
|
4
|
+
data.tar.gz: 93cca063db742ea205238443dcf98ab8e03ba627
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 438f005ef464d1f90f8e6ba57b3c1b2969e183828e2ad35dcb9fd5db00dea9bc394684ad1787c4c61c6ad23f8a879a933b8a18be3254fc730bc74c1d39b574e9
|
7
|
+
data.tar.gz: 905e8db4276d80e280fd95fb56902da845a0aa0eb496c4251d67deff3200436a83fada31f23af341fe490e4576956c9db7e5f9137d58ac53f68e7505addae7d1
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
sudo: required
|
2
|
+
|
1
3
|
language: ruby
|
2
4
|
rvm:
|
3
5
|
- ruby-2.0.0-p648
|
@@ -6,26 +8,54 @@ rvm:
|
|
6
8
|
- ruby-2.3.4
|
7
9
|
- ruby-2.4.1
|
8
10
|
- jruby-9.1.9.0
|
11
|
+
gemfile:
|
9
12
|
gemfile:
|
10
13
|
- gemfiles/rails_4_0.gemfile
|
11
14
|
- gemfiles/rails_4_1.gemfile
|
12
15
|
- gemfiles/rails_4_2.gemfile
|
13
16
|
- gemfiles/rails_5_0.gemfile
|
17
|
+
- gemfiles/rails_5_1.gemfile
|
18
|
+
- gemfiles/rails_5_2.gemfile
|
14
19
|
matrix:
|
15
20
|
exclude:
|
16
21
|
- rvm: ruby-2.0.0-p648
|
17
22
|
gemfile: gemfiles/rails_5_0.gemfile
|
23
|
+
- rvm: ruby-2.0.0-p648
|
24
|
+
gemfile: gemfiles/rails_5_1.gemfile
|
25
|
+
- rvm: ruby-2.0.0-p648
|
26
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
18
27
|
- rvm: ruby-2.1.10
|
19
28
|
gemfile: gemfiles/rails_5_0.gemfile
|
29
|
+
- rvm: ruby-2.1.10
|
30
|
+
gemfile: gemfiles/rails_5_1.gemfile
|
31
|
+
- rvm: ruby-2.1.10
|
32
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
20
33
|
- rvm: ruby-2.4.1
|
21
34
|
gemfile: gemfiles/rails_4_0.gemfile
|
22
35
|
- rvm: ruby-2.4.1
|
23
36
|
gemfile: gemfiles/rails_4_1.gemfile
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
-
|
29
|
-
|
37
|
+
|
38
|
+
### BUILD LIFECYCLE STEPS ###
|
39
|
+
|
40
|
+
before_install:
|
41
|
+
# Debugging: Print out the current docker-compose version.
|
42
|
+
- docker-compose --version
|
43
|
+
|
44
|
+
# If one of your containers does not build for
|
45
|
+
# whatever reason it's best to report that now before your tests start
|
46
|
+
# otherwise it can be really tricky to debug why tests are failing sometimes.
|
47
|
+
- docker ps
|
48
|
+
|
49
|
+
after_install:
|
50
|
+
- gem install bundler -v 1.15.4
|
30
51
|
- bundle install
|
31
|
-
|
52
|
+
|
53
|
+
before_script:
|
54
|
+
# Start Docker Compose as a daemon
|
55
|
+
- docker-compose up -d
|
56
|
+
|
57
|
+
script:
|
58
|
+
- bundle exec rake spec
|
59
|
+
|
60
|
+
after_script:
|
61
|
+
- docker-compose down
|
data/Appraisals
CHANGED
@@ -1,15 +1,26 @@
|
|
1
1
|
appraise "rails-4-0" do
|
2
2
|
gem "rails", "~> 4.0.0"
|
3
|
+
gem "nokogiri", "~> 1.6.8" # can be removed once we drop support for Ruby 2.0.0
|
3
4
|
end
|
4
5
|
|
5
6
|
appraise "rails-4-1" do
|
6
7
|
gem "rails", "~> 4.1.0"
|
8
|
+
gem "nokogiri", "~> 1.6.8" # can be removed once we drop support for Ruby 2.0.0
|
7
9
|
end
|
8
10
|
|
9
11
|
appraise "rails-4-2" do
|
10
12
|
gem "rails", "~> 4.2.0"
|
13
|
+
gem "nokogiri", "~> 1.6.8" # can be removed once we drop support for Ruby 2.0.0
|
11
14
|
end
|
12
15
|
|
13
16
|
appraise "rails-5-0" do
|
14
17
|
gem "rails", "~> 5.0.0"
|
15
18
|
end
|
19
|
+
|
20
|
+
appraise "rails-5-1" do
|
21
|
+
gem "rails", "~> 5.1.0"
|
22
|
+
end
|
23
|
+
|
24
|
+
appraise "rails-5-2" do
|
25
|
+
gem "rails", "~> 5.2.0"
|
26
|
+
end
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,121 @@
|
|
1
1
|
# HEAD
|
2
2
|
|
3
|
+
## Breaking
|
4
|
+
|
5
|
+
* N/A
|
6
|
+
|
7
|
+
## Improvements
|
8
|
+
|
9
|
+
* N/A
|
10
|
+
|
11
|
+
## Fixes
|
12
|
+
|
13
|
+
* N/A
|
14
|
+
|
15
|
+
# 2.2.0
|
16
|
+
|
17
|
+
## Breaking
|
18
|
+
|
19
|
+
* N/A
|
20
|
+
|
21
|
+
## Improvements
|
22
|
+
|
23
|
+
* Feature: [#256](https://github.com/Dynamoid/Dynamoid/pull/256) Support Rails 5.2 (@andrykonchin)
|
24
|
+
|
25
|
+
## Fixes
|
26
|
+
|
27
|
+
* Bug: [#255](https://github.com/Dynamoid/Dynamoid/pull/255) Fix Vagrant RVM configuration and upgrade to Ruby 2.4.1 (@richardhsu)
|
28
|
+
|
29
|
+
# 2.1.0
|
30
|
+
|
31
|
+
## Breaking
|
32
|
+
|
33
|
+
* N/A
|
34
|
+
|
35
|
+
## Improvements
|
36
|
+
|
37
|
+
* Feature: [#221](https://github.com/Dynamoid/Dynamoid/pull/221) Add field declaration option `of` to specify the type of `set` elements (@pratik60)
|
38
|
+
* Feature: [#223](https://github.com/Dynamoid/Dynamoid/pull/223) Add field declaration option `store_as_string` to store `datetime` as ISO-8601 formatted strings (@william101)
|
39
|
+
* Feature: [#228](https://github.com/Dynamoid/Dynamoid/pull/228) Add field declaration option `store_as_string` to store `date` as ISO-8601 formatted strings (@andrykonchin)
|
40
|
+
* Feature: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Support hash argument for `start` chain method (@mnussbaumer)
|
41
|
+
* Feature: [#236](https://github.com/Dynamoid/Dynamoid/pull/236) Change log level from `info` to `debug` for benchmark logging (@kicktheken)
|
42
|
+
* Feature: [#239](https://github.com/Dynamoid/Dynamoid/pull/239) Add methods for low-level updating: `.update`, `.update_fields` and `.upsert` (@andrykonchin)
|
43
|
+
* Feature: [#243](https://github.com/Dynamoid/Dynamoid/pull/243) Support `ne` condition operator (@andrykonchin)
|
44
|
+
* Feature: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Added support of backoff in batch operations (@andrykonchin)
|
45
|
+
* added global config options `backoff` and `backoff_strategies` to configure backoff
|
46
|
+
* added `constant` and `exponential` built-in backoff strategies
|
47
|
+
* `.find_all` and `.import` support new backoff options
|
48
|
+
|
49
|
+
## Fixes
|
50
|
+
|
51
|
+
* Bug: [#216](https://github.com/Dynamoid/Dynamoid/pull/216) Fix global index detection in queries with conditions other than equal (@andrykonchin)
|
52
|
+
* Bug: [#224](https://github.com/Dynamoid/Dynamoid/pull/224) Fix how `contains` operator works with `set` and `array` field types (@andrykonchin)
|
53
|
+
* Bug: [#225](https://github.com/Dynamoid/Dynamoid/pull/225) Fix equal conditions for `array` fields (@andrykonchin)
|
54
|
+
* Bug: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Repair support `start` chain method on Scan operation (@mnussbaumer)
|
55
|
+
* Bug: [#238](https://github.com/Dynamoid/Dynamoid/pull/238) Fix default value of `models_dir` config option (@baloran)
|
56
|
+
* Bug: [#244](https://github.com/Dynamoid/Dynamoid/pull/244) Allow to pass empty strings and sets to `.import` (@andrykonchin)
|
57
|
+
* Bug: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Batch operations (`batch_write_item` and `batch_read_item`) handle unprocessed items themselves (@andrykonchin)
|
58
|
+
* Bug: [#250](https://github.com/Dynamoid/Dynamoid/pull/250) Update outdated warning message about inefficient query and missing indices (@andrykonchin)
|
59
|
+
* Bug: [252](https://github.com/Dynamoid/Dynamoid/pull/252) Don't loose nanoseconds when store DateTime as float number
|
60
|
+
|
61
|
+
# 2.0.0
|
62
|
+
|
63
|
+
## Breaking
|
64
|
+
|
65
|
+
Breaking changes in this release generally bring Dynamoid behavior closer to the Rails-way.
|
66
|
+
|
67
|
+
* Change: [#186](https://github.com/Dynamoid/Dynamoid/pull/186) Consistent behavior for `Model.where({}).all` (@andrykonchin)
|
68
|
+
* <= 1.3.x behaviour -
|
69
|
+
* load lazily if user specified batch size
|
70
|
+
* load all collection into memory otherwise
|
71
|
+
* New behaviour -
|
72
|
+
* always return lazy evaluated collection
|
73
|
+
* It means Model.where({}).all returns Enumerator instead of Array.
|
74
|
+
* If you need Array interface you have to convert collection to Array manually with to_a method call
|
75
|
+
* Change: [#195](https://github.com/Dynamoid/Dynamoid/pull/195) Failed `#find` returns error (@andrykonchin)
|
76
|
+
* <= 1.3.x behaviour - find returns nil or smaller array.
|
77
|
+
* New behaviour - it raises RecordNotFound if one or more records can not be found for the requested ids
|
78
|
+
* Change: [#196](https://github.com/Dynamoid/Dynamoid/pull/196) Return value of `#save` (@andrykonchin)
|
79
|
+
* <= 1.3.x behaviour - save returns self if model is saved successfully
|
80
|
+
* New behaviour - it returns true
|
81
|
+
|
82
|
+
## Improvements
|
83
|
+
|
84
|
+
* Feature: [#185](https://github.com/Dynamoid/Dynamoid/pull/185) `where`, finders and friends take into account STI (single table inheritance) now (@andrykonchin)
|
85
|
+
* query will return items of the model class and all subclasses
|
86
|
+
* Feature: [#190](https://github.com/Dynamoid/Dynamoid/pull/190) Allow passing options to range when defining attributes of the document (@richardhsu)
|
87
|
+
* Allows for serialized fields and passing the serializer option.
|
88
|
+
* Feature: [#198](https://github.com/Dynamoid/Dynamoid/pull/198) Enhanced `#create` and `#create!` to allow multiple document creation like `#import` (@andrykonchin)
|
89
|
+
* `User.create([{name: 'Josh'}, {name: 'Nick'}])`
|
90
|
+
* Feature: [#199](https://github.com/Dynamoid/Dynamoid/pull/199) Added `Document.import` method (@andrykonchin)
|
91
|
+
* Feature: [#205](https://github.com/Dynamoid/Dynamoid/pull/205) Use batch deletion via `batch_write_item` for `delete_all` (@andrykonchin)
|
92
|
+
* Rename: [#205](https://github.com/Dynamoid/Dynamoid/pull/205) `Chain#destroy_all` as `Chain#delete_all`, to better match Rails conventions when no callbacks are run (@andrykonchin)
|
93
|
+
* kept the old name as an alias, for backwards compatibility
|
94
|
+
* Feature: [#207](https://github.com/Dynamoid/Dynamoid/pull/207) Added slicing by 25 requests in #batch_write_item (@andrykonchin)
|
95
|
+
* Feature: [#211](https://github.com/Dynamoid/Dynamoid/pull/211) Improved Vagrant setup for testing (@richardhsu)
|
96
|
+
* Feature: [#212](https://github.com/Dynamoid/Dynamoid/pull/212) Add foreign_key option (@andrykonchin)
|
97
|
+
* Feature: [#213](https://github.com/Dynamoid/Dynamoid/pull/213) Support Boolean raw type (@andrykonchin)
|
98
|
+
* Improved Documentation (@pboling, @andrykonchin)
|
99
|
+
|
100
|
+
## Fixes
|
101
|
+
|
102
|
+
* Bug: [#191](https://github.com/Dynamoid/Dynamoid/pull/191), [#192](https://github.com/Dynamoid/Dynamoid/pull/192) Support lambdas as fix for value types were not able to be used as default values (@andrykonchin)(@richardhsu)
|
103
|
+
* Bug: [#202](https://github.com/Dynamoid/Dynamoid/pull/202) Fix several issues with associations (@andrykonchin)
|
104
|
+
* setting `nil` value raises an exception
|
105
|
+
* document doesn't keep assigned model and loads it from the storage
|
106
|
+
* delete call doesn't update cached ids of associated models
|
107
|
+
* fix clearing old `has_many` association while add model to new `has_many` association
|
108
|
+
* Bug: [#204](https://github.com/Dynamoid/Dynamoid/pull/204) Fixed issue where `Document.where(:"id.in" => [])` would do `Query` operation instead of `Scan` (@andrykonchin)
|
109
|
+
* Fixed `Chain#key_present?`
|
110
|
+
* Bug: [#205](https://github.com/Dynamoid/Dynamoid/pull/205) Fixed `delete_all` (@andrykonchin)
|
111
|
+
* Fixed exception when makes scan and sort key is declared in model
|
112
|
+
* Fixed exception when makes scan and any condition is specified in where clause (like Document.where().delete_all)
|
113
|
+
* Fixed exception when makes query and sort key isn't declared in model
|
114
|
+
* Bug: [#207](https://github.com/Dynamoid/Dynamoid/pull/207) Fixed `#delete` method for case `adapter.delete(table_name, [1, 2, 3], range_key: 1)` (@andrykonchin)
|
115
|
+
|
3
116
|
# 1.3.4
|
4
117
|
|
5
|
-
|
118
|
+
## Improvements
|
6
119
|
|
7
120
|
* Added `Chain#last` method (@andrykonchin)
|
8
121
|
* Added `date` field type (@andrykonchin)
|
@@ -19,7 +132,7 @@ Improving
|
|
19
132
|
* Support querying Global/Local Secondary Indices in `where` clause (@richardhsu)
|
20
133
|
* Only query on GSI if projects all attributes in `where` clause (@richardhsu)
|
21
134
|
|
22
|
-
Fixes
|
135
|
+
## Fixes
|
23
136
|
|
24
137
|
* Fix incorrect applying of default field value (#36 and #117, @andrykonchin)
|
25
138
|
* Fix sync table creation/deletion (#160, @mirokuxy)
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,20 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
1
3
|
Copyright (c) 2012 Josh Symonds
|
4
|
+
Copyright (c) 2013 - 2018 Dynamoid, https://github.com/Dynamoid
|
2
5
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
the following conditions:
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
10
12
|
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
14
|
+
copies or substantial portions of the Software.
|
13
15
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
OF
|
20
|
-
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Dynamoid
|
2
2
|
|
3
|
+
You are viewing the README for version 2 of Dynamoid. See the [CHANGELOG](https://github.com/Dynamoid/Dynamoid/blob/master/CHANGELOG.md#200) for details on breaking changes since 1.3.x.
|
4
|
+
|
5
|
+
For version 1.3.x use the [1-3-stable branch](https://github.com/Dynamoid/Dynamoid/blob/1-3-stable/README.md).
|
6
|
+
|
3
7
|
Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
|
4
8
|
provides similar functionality to ActiveRecord and improves on
|
5
9
|
Amazon's existing
|
@@ -10,21 +14,31 @@ DynamoDB is not like other document-based databases you might know, and is very
|
|
10
14
|
|
11
15
|
But if you want a fast, scalable, simple, easy-to-use database (and a Gem that supports it) then look no further!
|
12
16
|
|
13
|
-
## Call For Maintainers
|
14
17
|
|
15
|
-
|
16
|
-
|
18
|
+
| Project | Dynamoid |
|
19
|
+
|------------------------ | ----------------- |
|
20
|
+
| gem name | dynamoid |
|
21
|
+
| license | MIT |
|
22
|
+
| download rank | [](https://rubygems.org/gems/dynamoid) |
|
23
|
+
| version | [](https://rubygems.org/gems/dynamoid) |
|
24
|
+
| dependencies | [](https://gemnasium.com/github.com/Dynamoid/Dynamoid) [](https://depfu.com)|
|
25
|
+
| code quality | [](https://codeclimate.com/github/Dynamoid/Dynamoid) |
|
26
|
+
| continuous integration | [](https://travis-ci.org/Dynamoid/Dynamoid) |
|
27
|
+
| test coverage | [](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
|
28
|
+
| triage helpers | [](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) |
|
17
31
|
|
18
32
|
## Installation
|
19
33
|
|
20
34
|
Installing Dynamoid is pretty simple. First include the Gem in your Gemfile:
|
21
35
|
|
22
36
|
```ruby
|
23
|
-
gem 'dynamoid', '~>
|
37
|
+
gem 'dynamoid', '~> 2'
|
24
38
|
```
|
25
39
|
## Prerequisities
|
26
40
|
|
27
|
-
Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (
|
41
|
+
Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (>= 4).
|
28
42
|
Hence the configuration as needed for aws to work will be dealt with by aws setup.
|
29
43
|
|
30
44
|
Here are the steps to setup aws-sdk.
|
@@ -35,7 +49,18 @@ gem 'aws-sdk', '~>2'
|
|
35
49
|
|
36
50
|
(or) include the aws-sdk in your Gemfile.
|
37
51
|
|
38
|
-
|
52
|
+
### AWS SDK Version Compatibility
|
53
|
+
|
54
|
+
Make sure you are using the version for the right AWS SDK.
|
55
|
+
|
56
|
+
| Dynamoid version | AWS SDK Version |
|
57
|
+
| ---------------- | --------------- |
|
58
|
+
| 0.x | 1.x |
|
59
|
+
| 1.x | 2.x |
|
60
|
+
| 2.x | 2.x |
|
61
|
+
| 3.x (unreleased) | 3.x |
|
62
|
+
|
63
|
+
### AWS Configuration
|
39
64
|
|
40
65
|
Configure AWS access:
|
41
66
|
[Reference](https://github.com/aws/aws-sdk-ruby)
|
@@ -56,6 +81,7 @@ Create config/initializers/aws.rb as follows:
|
|
56
81
|
Alternatively, if you don't want Aws connection settings to be overwritten for you entire project, you can specify connection settings for Dynamoid only, by setting those in the `Dynamoid.configure` clause:
|
57
82
|
|
58
83
|
```ruby
|
84
|
+
require 'dynamoid'
|
59
85
|
Dynamoid.configure do |config|
|
60
86
|
config.access_key = 'REPLACE_WITH_ACCESS_KEY_ID'
|
61
87
|
config.secret_key = 'REPLACE_WITH_SECRET_ACCESS_KEY'
|
@@ -69,6 +95,7 @@ For a full list of the DDB regions, you can go
|
|
69
95
|
Then you need to initialize Dynamoid config to get it going. Put code similar to this somewhere (a Rails initializer would be a great place for this if you're using Rails):
|
70
96
|
|
71
97
|
```ruby
|
98
|
+
require 'dynamoid'
|
72
99
|
Dynamoid.configure do |config|
|
73
100
|
config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have. Set to nil to avoid namespacing.
|
74
101
|
config.endpoint = 'http://localhost:3000' # [Optional]. If provided, it communicates with the DB listening at the endpoint. This is useful for testing with [Amazon Local DB] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html).
|
@@ -76,7 +103,7 @@ Then you need to initialize Dynamoid config to get it going. Put code similar to
|
|
76
103
|
|
77
104
|
```
|
78
105
|
|
79
|
-
### Compatibility Matrix
|
106
|
+
### Ruby & Rails Compatibility Matrix
|
80
107
|
|
81
108
|
| Ruby / Active Record | 4.0.x | 4.1.x | 4.2.x | 5.0.x |
|
82
109
|
|:---------------------:|:-----:|:-----:|:-----:|:-----:|
|
@@ -124,6 +151,61 @@ By default, fields are assumed to be of type ```:string```. Other built-in types
|
|
124
151
|
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.
|
125
152
|
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.
|
126
153
|
|
154
|
+
#### Note on boolean type
|
155
|
+
|
156
|
+
The boolean fields are stored as `"t", "f"` strings by default. DynamoDB
|
157
|
+
supports boolean type natively. So if you want to use native boolean
|
158
|
+
type or already have table with native boolean attribute you can easily
|
159
|
+
achieve this with `store_as_native_boolean` option:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class Document
|
163
|
+
include DynamoId::Document
|
164
|
+
|
165
|
+
field :active, :boolean, store_as_native_boolean: true
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
#### Note on date type
|
170
|
+
|
171
|
+
By default date fields are persisted as days count since 1 January 1970 like UNIX time. If you prefer dates to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true`
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
class Document
|
175
|
+
include DynamoId::Document
|
176
|
+
|
177
|
+
field :sent_at, :datetime, store_as_string: true
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
#### Note on datetime type
|
182
|
+
|
183
|
+
By default datetime fields are persisted as UNIX timestamps with milisecond precission in DynamoDB. If you prefer datetimes to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true`
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class Document
|
187
|
+
include DynamoId::Document
|
188
|
+
|
189
|
+
field :sent_at, :datetime, store_as_string: true
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
### Note on set type
|
194
|
+
|
195
|
+
There is `of` option to declare the type of set elements. You can use
|
196
|
+
`:integer` value only
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
class Document
|
200
|
+
include DynamoId::Document
|
201
|
+
|
202
|
+
field :tags, :set, of: :integer
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
|
207
|
+
#### Magic Columns
|
208
|
+
|
127
209
|
You get magic columns of id (string), created_at (datetime), and updated_at (datetime) for free.
|
128
210
|
|
129
211
|
```ruby
|
@@ -140,6 +222,8 @@ class User
|
|
140
222
|
end
|
141
223
|
```
|
142
224
|
|
225
|
+
#### Default Values
|
226
|
+
|
143
227
|
You can optionally set a default value on a field using either a plain value or a lambda:
|
144
228
|
|
145
229
|
```ruby
|
@@ -147,6 +231,8 @@ You can optionally set a default value on a field using either a plain value or
|
|
147
231
|
field :joined_at, :datetime, {default: ->(){Time.now}}
|
148
232
|
```
|
149
233
|
|
234
|
+
#### Custom Types
|
235
|
+
|
150
236
|
To use a custom type for a field, suppose you have a `Money` type.
|
151
237
|
|
152
238
|
```ruby
|
@@ -159,7 +245,7 @@ To use a custom type for a field, suppose you have a `Money` type.
|
|
159
245
|
|
160
246
|
def self.dynamoid_load(serialized_str)
|
161
247
|
# parse serialized representation and return a Money instance
|
162
|
-
Money.new(
|
248
|
+
Money.new(1.23)
|
163
249
|
end
|
164
250
|
end
|
165
251
|
|
@@ -181,7 +267,7 @@ add a level of indirection for serializing.) Example:
|
|
181
267
|
|
182
268
|
class MoneyAdapter
|
183
269
|
def self.dynamoid_load(money_serialized_str)
|
184
|
-
Money.new(
|
270
|
+
Money.new(1.23)
|
185
271
|
end
|
186
272
|
|
187
273
|
def self.dynamoid_dump(money_obj)
|
@@ -201,8 +287,8 @@ This is especially important if you want to use your custom field as a numeric r
|
|
201
287
|
number-oriented queries. By default custom fields are persisted as a string attribute, but
|
202
288
|
your custom class can override this with a `.dynamoid_field_type` class method, which would
|
203
289
|
return either `:string` or `:number`.
|
204
|
-
(DynamoDB supports some other attribute types, but Dynamoid does not yet.)
|
205
290
|
|
291
|
+
DynamoDB may support some other attribute types that are not yet supported by Dynamoid.
|
206
292
|
|
207
293
|
### Associations
|
208
294
|
|
@@ -214,12 +300,13 @@ The only supported associations (so far) are ```has_many```, ```has_one```, ```h
|
|
214
300
|
class User
|
215
301
|
include Dynamoid::Document
|
216
302
|
|
217
|
-
...
|
303
|
+
# ...
|
218
304
|
|
219
305
|
has_many :addresses
|
220
306
|
has_many :students, :class => User
|
221
307
|
belongs_to :teacher, :class_name => :user
|
222
308
|
belongs_to :group
|
309
|
+
belongs_to :group, :foreign_key => :group_id
|
223
310
|
has_one :role
|
224
311
|
has_and_belongs_to_many :friends, :inverse_of => :friending_users
|
225
312
|
|
@@ -228,7 +315,7 @@ end
|
|
228
315
|
class Address
|
229
316
|
include Dynamoid::Document
|
230
317
|
|
231
|
-
...
|
318
|
+
# ...
|
232
319
|
|
233
320
|
belongs_to :user # Automatically links up with the user model
|
234
321
|
|
@@ -245,7 +332,7 @@ Dynamoid bakes in ActiveModel validations, just like ActiveRecord does.
|
|
245
332
|
class User
|
246
333
|
include Dynamoid::Document
|
247
334
|
|
248
|
-
...
|
335
|
+
# ...
|
249
336
|
|
250
337
|
validates_presence_of :name
|
251
338
|
validates_format_of :email, :with => /@/
|
@@ -268,7 +355,7 @@ Dynamoid also employs ActiveModel callbacks. Right now, callbacks are defined on
|
|
268
355
|
class User
|
269
356
|
include Dynamoid::Document
|
270
357
|
|
271
|
-
...
|
358
|
+
# ...
|
272
359
|
|
273
360
|
before_save :set_default_password
|
274
361
|
after_create :notify_friends
|
@@ -325,6 +412,19 @@ address.city = 'Chicago'
|
|
325
412
|
address.save
|
326
413
|
```
|
327
414
|
|
415
|
+
To create multiple documents at once:
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
User.create([{name: 'Josh'}, {name: 'Nick'}])
|
419
|
+
```
|
420
|
+
|
421
|
+
There is an efficient and low-level way to create multiple documents
|
422
|
+
(without validation and callbacks running):
|
423
|
+
|
424
|
+
```ruby
|
425
|
+
users = User.import([{name: 'Josh'}, {name: 'Nick'}])
|
426
|
+
```
|
427
|
+
|
328
428
|
### Querying
|
329
429
|
|
330
430
|
Querying can be done in one of three ways:
|
@@ -354,11 +454,13 @@ There are three types of limits that you can query with:
|
|
354
454
|
Using these in various combinations results in the underlying requests to be made in the smallest size possible and
|
355
455
|
the query returns once `record_limit` or `scan_limit` is satisfied. It will attempt to batch whenever possible.
|
356
456
|
|
357
|
-
You can thus limit the number of evaluated records, or select a record from which to start
|
457
|
+
You can thus limit the number of evaluated records, or select a record from which to start in order to support pagination.
|
358
458
|
|
359
459
|
```ruby
|
360
460
|
Address.record_limit(5).start(address) # Only 5 addresses starting at `address`
|
361
461
|
```
|
462
|
+
Where `address` is an instance of the model or a hash `{the_model_hash_key: 'value', the_model_range_key: 'value'}`:
|
463
|
+
Keep in mind that if you are passing a hash to `.start()` you need to explicitly define all required keys in it including range keys, depending on table or secondary indexes signatures, otherwise you'll get an `Aws::DynamoDB::Errors::ValidationException` either for `Exclusive Start Key must have same size as table's key schema` or `The provided starting key is invalid`
|
362
464
|
|
363
465
|
If you are potentially running over a large data set and this is especially true when using certain filters, you may
|
364
466
|
want to consider limiting the number of scanned records (the number of records DynamoDB infrastructure looks through
|
@@ -386,21 +488,21 @@ You are able to optimize query with condition for sort key. Following operators
|
|
386
488
|
|
387
489
|
```ruby
|
388
490
|
Address.where(latitude: 10212)
|
389
|
-
Address.where('latitude.gt'
|
390
|
-
Address.where('latitude.lt'
|
391
|
-
Address.where('latitude.gte'
|
392
|
-
Address.where('latitude.lte'
|
393
|
-
Address.where('city.begins_with'
|
394
|
-
Address.where('latitude.between'
|
491
|
+
Address.where('latitude.gt' => 10212)
|
492
|
+
Address.where('latitude.lt' => 10212)
|
493
|
+
Address.where('latitude.gte' => 10212)
|
494
|
+
Address.where('latitude.lte' => 10212)
|
495
|
+
Address.where('city.begins_with' => 'Lon')
|
496
|
+
Address.where('latitude.between' => [10212, 20000])
|
395
497
|
```
|
396
498
|
|
397
499
|
You are able to filter results on the DynamoDB side and specify conditions for non-key fields.
|
398
500
|
Following operators are available: `in`, `contains`, `not_contains`:
|
399
501
|
|
400
502
|
```ruby
|
401
|
-
Address.where('city.in'
|
402
|
-
Address.where('city.contains'
|
403
|
-
Address.where('city.not_contains'
|
503
|
+
Address.where('city.in' => ['London', 'Edenburg', 'Birmingham'])
|
504
|
+
Address.where('city.contains' => [on])
|
505
|
+
Address.where('city.not_contains' => [ing])
|
404
506
|
```
|
405
507
|
|
406
508
|
### Consistent Reads
|
@@ -423,8 +525,77 @@ User.where("created_at.lt" => DateTime.now - 1.day).all
|
|
423
525
|
|
424
526
|
It also supports .gte and .lte. Turning those into symbols and allowing a Rails SQL-style string syntax is in the works. You can only have one range argument per query, because of DynamoDB's inherent limitations, so use it sensibly!
|
425
527
|
|
528
|
+
|
529
|
+
### Updating
|
530
|
+
|
531
|
+
In order to update document you can use high level methods
|
532
|
+
`#update_attributes`, `#update_attribute` and `.update`.
|
533
|
+
They run validation and collbacks.
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
Address.find(id).update_attributes(city: 'Chicago')
|
537
|
+
Address.find(id).update_attribute(city, 'Chicago')
|
538
|
+
Address.update(id, city: 'Chicago')
|
539
|
+
Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
|
540
|
+
```
|
541
|
+
|
542
|
+
There are also some low level methods `#update`, `.update_fields` and
|
543
|
+
`.upsert`. They don't run validation and callbacks (except `#update` - it
|
544
|
+
runs `update` callbacks). All of them support conditional updates.
|
545
|
+
`#upsert` will create new document if document with specified `id`
|
546
|
+
doesn't exist.
|
547
|
+
|
548
|
+
```ruby
|
549
|
+
Adderess.find(id).update do |i|
|
550
|
+
i.set city: 'Chicago'
|
551
|
+
i.add latitude: 100
|
552
|
+
i.delete set_of_numbers: 10
|
553
|
+
end
|
554
|
+
Adderess.find(id).update(if: { deliverable: true }) do |i|
|
555
|
+
i.set city: 'Chicago'
|
556
|
+
end
|
557
|
+
Address.update_fields(id, city: 'Chicago')
|
558
|
+
Address.update_fields(id, { city: 'Chicago' }, if: { deliverable: true })
|
559
|
+
Address.upsert(id, city: 'Chicago')
|
560
|
+
Address.upsert(id, { city: 'Chicago' }, if: { deliverable: true })
|
561
|
+
```
|
562
|
+
|
563
|
+
### Deleting
|
564
|
+
|
565
|
+
In order to delete some items `delete_all` method should be used.
|
566
|
+
Any callback wont be called. Items delete in efficient way in batch.
|
567
|
+
|
568
|
+
```ruby
|
569
|
+
Address.where(city: "London").delete_all
|
570
|
+
```
|
571
|
+
|
426
572
|
### Global Secondary Indexes
|
427
573
|
|
574
|
+
You can define index with `global_secondary_index`:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
class User
|
578
|
+
include Dynamoid::Document
|
579
|
+
|
580
|
+
field :name
|
581
|
+
field :age, :number
|
582
|
+
|
583
|
+
global_secondary_index hash_key: :age
|
584
|
+
end
|
585
|
+
```
|
586
|
+
|
587
|
+
There are following options:
|
588
|
+
* `hash_key` - is used as hash key of an index,
|
589
|
+
* `range_key` - is used as range key of an index,
|
590
|
+
* `projected_attributes` - list of fields to store in an index or has a predefiled value `:keys_only`, `:all`; `:keys_only` is a default,
|
591
|
+
* `name` - an index will be created with this name when a table is created; by default name is generated and contains table name and keys names,
|
592
|
+
* `read_capacity` - is used when table creates and used as an index capacity; by default equals `Dynamoid::Config.read_capacity`,
|
593
|
+
* `write_capacity` - is used when table creates and used as an index capacity; by default equals `Dynamoid::Config.write_capacity`
|
594
|
+
|
595
|
+
The only mandatory option is `name`.
|
596
|
+
|
597
|
+
To use index in `Document.where` implicitly you need to project all the fields with option `projected_attributes: :all`.
|
598
|
+
|
428
599
|
There are two ways to query Global Secondary Indexes (GSI).
|
429
600
|
|
430
601
|
#### Explicit
|
@@ -485,7 +656,7 @@ the table since a query against GSI then a query on base table is still likely f
|
|
485
656
|
|
486
657
|
## Configuration
|
487
658
|
|
488
|
-
|
659
|
+
Listed below are all configuration options.
|
489
660
|
|
490
661
|
* `adapter` - usefull only for the gem developers to switch to a new adapter. Default and the only available value is `aws_sdk_v2`
|
491
662
|
* `namespace` - prefix for table names, default is `dynamoid_#{application_name}_#{environment}` for Rails application and `dynamoid` otherwise
|
@@ -504,9 +675,13 @@ There are listed all the configuration options:
|
|
504
675
|
* `sync_retry_max_times` - when Dynamoid creates or deletes table synchronously it checks for completion specified times. Default is 60 (times). It's a bit over 2 minutes by default
|
505
676
|
* `sync_retry_wait_seconds` - time to wait between retries. Default is 2 (seconds)
|
506
677
|
* `convert_big_decimal` - if `true` then Dynamoid converts numbers stored in `Hash` in `raw` field to float. Default is `false`
|
507
|
-
* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is
|
678
|
+
* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is `./app/models`.
|
508
679
|
* `application_timezone` - Dynamoid converts all `datetime` fields to specified time zone when loads data from the storage.
|
509
680
|
Acceptable values - `utc`, `local` (to use system time zone) and time zone name e.g. `Eastern Time (US & Canada)`. Default is `local`
|
681
|
+
* `store_datetime_as_string` - if `true` then Dynamoid stores :datetime fields in ISO 8601 string format. Default is `false`
|
682
|
+
* `store_date_as_string` - if `true` then Dynamoid stores :date fields in ISO 8601 string format. Default is `false`
|
683
|
+
* `backoff` - is a hash: key is a backoff strategy (symbol), value is parameters for the strategy. Is used in batch operations. Default id `nil`
|
684
|
+
* `backoff_strategies`: is a hash and contains all available strategies. Default is { constant: ..., exponential: ...}
|
510
685
|
|
511
686
|
|
512
687
|
## Concurrency
|
@@ -515,11 +690,11 @@ Dynamoid supports basic, ActiveRecord-like optimistic locking on save operations
|
|
515
690
|
|
516
691
|
```ruby
|
517
692
|
class MyTable
|
518
|
-
...
|
693
|
+
# ...
|
519
694
|
|
520
695
|
field :lock_version, :integer
|
521
696
|
|
522
|
-
...
|
697
|
+
# ...
|
523
698
|
end
|
524
699
|
```
|
525
700
|
|
@@ -527,6 +702,52 @@ In this example, all saves to `MyTable` will raise an `Dynamoid::Errors::StaleOb
|
|
527
702
|
|
528
703
|
Calls to `update` and `update!` also increment the `lock_version`, however they do not check the existing value. This guarantees that a update operation will raise an exception in a concurrent save operation, however a save operation will never cause an update to fail. Thus, `update` is useful & safe only for doing atomic operations (e.g. increment a value, add/remove from a set, etc), but should not be used in a read-modify-write pattern.
|
529
704
|
|
705
|
+
|
706
|
+
### Backoff strategies
|
707
|
+
|
708
|
+
|
709
|
+
You can use several methods that run efficiently in batch mode like `.find_all` and `.import`.
|
710
|
+
|
711
|
+
The backoff strategy will be used when, for any reason, some items could not be processed as part of a batch mode command.
|
712
|
+
Operations will be re-run to process these items.
|
713
|
+
|
714
|
+
Exponential backoff is the recommended way to handle throughput limits exceeding and throttling on the table.
|
715
|
+
|
716
|
+
There are two built-in strategies - constant delay and truncated binary exponential backoff.
|
717
|
+
By default no backoff is used but you can specify one of the built-in ones:
|
718
|
+
|
719
|
+
```ruby
|
720
|
+
Dynamoid.configure do |config|
|
721
|
+
config.backoff = { constant: 2.second }
|
722
|
+
end
|
723
|
+
|
724
|
+
Dynamoid.configure do |config|
|
725
|
+
config.backoff = { exponential: { base_backoff: 0.2.seconds, ceiling: 10 } }
|
726
|
+
end
|
727
|
+
|
728
|
+
```
|
729
|
+
|
730
|
+
You can just specify strategy without any arguments to use default presets:
|
731
|
+
|
732
|
+
```ruby
|
733
|
+
Dynamoid.configure do |config|
|
734
|
+
config.backoff = :constant
|
735
|
+
end
|
736
|
+
```
|
737
|
+
|
738
|
+
You can use your own strategy in following way:
|
739
|
+
|
740
|
+
```ruby
|
741
|
+
Dynamoid.configure do |config|
|
742
|
+
config.backoff_strategies[:custom] = lambda do |n|
|
743
|
+
-> { sleep rand(n) }
|
744
|
+
end
|
745
|
+
|
746
|
+
config.backoff = { custom: 10 }
|
747
|
+
end
|
748
|
+
```
|
749
|
+
|
750
|
+
|
530
751
|
## Rake Tasks
|
531
752
|
|
532
753
|
* `rake dynamoid:create_tables`
|
@@ -593,14 +814,15 @@ Also, without contributors the project wouldn't be nearly as awesome. So many th
|
|
593
814
|
* [Pascal Corpet](https://github.com/pcorpet)
|
594
815
|
* [Brian Glusman](https://github.com/bglusman) *
|
595
816
|
* [Peter Boling](https://github.com/pboling) *
|
817
|
+
* [Andrew Konchin](https://github.com/andrykonchin) *
|
596
818
|
|
597
819
|
\* Current Maintainers
|
598
820
|
|
599
821
|
## Running the tests
|
600
822
|
|
601
|
-
Running the tests is fairly simple. You should have an instance of DynamoDB running locally. Follow
|
823
|
+
Running the tests is fairly simple. You should have an instance of DynamoDB running locally. Follow these steps to setup your test environment.
|
602
824
|
|
603
|
-
* First download and unpack the latest version of DynamoDB.
|
825
|
+
* First download and unpack the latest version of DynamoDB. We have a script that will do this for you if you use homebrew on a Mac.
|
604
826
|
|
605
827
|
```shell
|
606
828
|
bin/setup
|
@@ -624,7 +846,7 @@ Running the tests is fairly simple. You should have an instance of DynamoDB runn
|
|
624
846
|
bin/stop_dynamodblocal
|
625
847
|
```
|
626
848
|
|
627
|
-
If you want to run all the specs that travis runs, use `bundle exec wwtd`, but first you will need to setup all the rubies, for each of `%w( 2.0.0-p648 2.1.10 2.2.6 2.3.3 2.4.1 jruby-9.1.8.0 )`.
|
849
|
+
If you want to run all the specs that travis runs, use `bundle exec wwtd`, but first you will need to setup all the rubies, for each of `%w( 2.0.0-p648 2.1.10 2.2.6 2.3.3 2.4.1 jruby-9.1.8.0 )`. When you run `bundle exec wwtd` it will take care of starting and stopping the local dynamodb instance.
|
628
850
|
|
629
851
|
```shell
|
630
852
|
rvm use 2.0.0-p648
|
@@ -633,9 +855,6 @@ gem install bundler
|
|
633
855
|
bundle install
|
634
856
|
```
|
635
857
|
|
636
|
-
[](https://travis-ci.org/Dynamoid/Dynamoid)
|
637
|
-
[](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master)
|
638
|
-
|
639
858
|
## Copyright
|
640
859
|
|
641
860
|
Copyright (c) 2012 Josh Symonds.
|