dynomite 1.2.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -188
  7. data/Rakefile +13 -1
  8. data/dynomite.gemspec +9 -2
  9. data/exe/dynomite +14 -0
  10. data/lib/dynomite/associations/association.rb +126 -0
  11. data/lib/dynomite/associations/belongs_to.rb +35 -0
  12. data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
  13. data/lib/dynomite/associations/has_many.rb +19 -0
  14. data/lib/dynomite/associations/has_one.rb +19 -0
  15. data/lib/dynomite/associations/many_association.rb +257 -0
  16. data/lib/dynomite/associations/single_association.rb +157 -0
  17. data/lib/dynomite/associations.rb +248 -0
  18. data/lib/dynomite/autoloader.rb +25 -0
  19. data/lib/dynomite/cli.rb +48 -0
  20. data/lib/dynomite/client.rb +118 -0
  21. data/lib/dynomite/command.rb +89 -0
  22. data/lib/dynomite/completer/script.rb +6 -0
  23. data/lib/dynomite/completer/script.sh +10 -0
  24. data/lib/dynomite/completer.rb +159 -0
  25. data/lib/dynomite/config.rb +39 -0
  26. data/lib/dynomite/core.rb +18 -19
  27. data/lib/dynomite/engine.rb +45 -0
  28. data/lib/dynomite/erb.rb +5 -3
  29. data/lib/dynomite/error.rb +12 -0
  30. data/lib/dynomite/help/completion.md +20 -0
  31. data/lib/dynomite/help/completion_script.md +3 -0
  32. data/lib/dynomite/help/migrate.md +3 -0
  33. data/lib/dynomite/help.rb +9 -0
  34. data/lib/dynomite/install.rb +4 -0
  35. data/lib/dynomite/item/abstract.rb +15 -0
  36. data/lib/dynomite/item/components.rb +33 -0
  37. data/lib/dynomite/item/dsl.rb +101 -0
  38. data/lib/dynomite/item/id.rb +41 -0
  39. data/lib/dynomite/item/indexes/finder.rb +58 -0
  40. data/lib/dynomite/item/indexes/index.rb +21 -0
  41. data/lib/dynomite/item/indexes/primary_index.rb +18 -0
  42. data/lib/dynomite/item/indexes.rb +25 -0
  43. data/lib/dynomite/item/locking.rb +53 -0
  44. data/lib/dynomite/item/magic_fields.rb +66 -0
  45. data/lib/dynomite/item/primary_key.rb +85 -0
  46. data/lib/dynomite/item/query/delegates.rb +28 -0
  47. data/lib/dynomite/item/query/params/base.rb +42 -0
  48. data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
  49. data/lib/dynomite/item/query/params/filter.rb +41 -0
  50. data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
  51. data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
  52. data/lib/dynomite/item/query/params/function/base.rb +33 -0
  53. data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
  54. data/lib/dynomite/item/query/params/function/contains.rb +7 -0
  55. data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
  56. data/lib/dynomite/item/query/params/helpers.rb +94 -0
  57. data/lib/dynomite/item/query/params/key_condition.rb +34 -0
  58. data/lib/dynomite/item/query/params.rb +115 -0
  59. data/lib/dynomite/item/query/partiql/executer.rb +72 -0
  60. data/lib/dynomite/item/query/partiql.rb +67 -0
  61. data/lib/dynomite/item/query/relation/chain.rb +125 -0
  62. data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
  63. data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
  64. data/lib/dynomite/item/query/relation/delete.rb +38 -0
  65. data/lib/dynomite/item/query/relation/ids.rb +21 -0
  66. data/lib/dynomite/item/query/relation/math.rb +19 -0
  67. data/lib/dynomite/item/query/relation/where_field.rb +32 -0
  68. data/lib/dynomite/item/query/relation/where_group.rb +78 -0
  69. data/lib/dynomite/item/query/relation.rb +127 -0
  70. data/lib/dynomite/item/query.rb +7 -0
  71. data/lib/dynomite/item/read/find.rb +196 -0
  72. data/lib/dynomite/item/read/find_with_event.rb +42 -0
  73. data/lib/dynomite/item/read.rb +90 -0
  74. data/lib/dynomite/item/sti.rb +43 -0
  75. data/lib/dynomite/item/table_namespace.rb +43 -0
  76. data/lib/dynomite/item/typecaster.rb +106 -0
  77. data/lib/dynomite/item/waiter_methods.rb +18 -0
  78. data/lib/dynomite/item/write/base.rb +15 -0
  79. data/lib/dynomite/item/write/delete_item.rb +14 -0
  80. data/lib/dynomite/item/write/put_item.rb +99 -0
  81. data/lib/dynomite/item/write/update_item.rb +73 -0
  82. data/lib/dynomite/item/write.rb +204 -0
  83. data/lib/dynomite/item.rb +113 -299
  84. data/lib/dynomite/migration/dsl/accessor.rb +19 -0
  85. data/lib/dynomite/migration/dsl/index/base.rb +42 -0
  86. data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
  87. data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
  88. data/lib/dynomite/migration/dsl/index.rb +72 -0
  89. data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
  90. data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
  91. data/lib/dynomite/migration/dsl.rb +89 -142
  92. data/lib/dynomite/migration/file_info.rb +28 -0
  93. data/lib/dynomite/migration/generator.rb +30 -16
  94. data/lib/dynomite/migration/helpers.rb +7 -0
  95. data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
  96. data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
  97. data/lib/dynomite/migration/runner.rb +178 -0
  98. data/lib/dynomite/migration/templates/create_table.rb +7 -23
  99. data/lib/dynomite/migration/templates/delete_table.rb +7 -0
  100. data/lib/dynomite/migration/templates/update_table.rb +3 -18
  101. data/lib/dynomite/migration.rb +53 -10
  102. data/lib/dynomite/query.rb +48 -0
  103. data/lib/dynomite/reserved_words.rb +13 -3
  104. data/lib/dynomite/seed.rb +12 -0
  105. data/lib/dynomite/types.rb +22 -0
  106. data/lib/dynomite/version.rb +1 -1
  107. data/lib/dynomite/waiter.rb +40 -0
  108. data/lib/dynomite.rb +11 -17
  109. data/lib/generators/application_item/application_item_generator.rb +30 -0
  110. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  111. data/lib/jets/commands/dynamodb_command.rb +29 -0
  112. data/lib/jets/commands/help/generate.md +33 -0
  113. data/lib/jets/commands/help/migrate.md +3 -0
  114. metadata +202 -17
  115. data/docs/migrations/long-example.rb +0 -127
  116. data/docs/migrations/short-example.rb +0 -40
  117. data/lib/dynomite/db_config.rb +0 -107
  118. data/lib/dynomite/errors.rb +0 -15
  119. data/lib/dynomite/log.rb +0 -15
  120. data/lib/dynomite/migration/common.rb +0 -86
  121. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  122. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  123. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  124. data/lib/dynomite/migration/executor.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f80bf11d2121217d52d8ceac89defcd4e738b3ff220847f7421b1337fbc104f
4
- data.tar.gz: cd1a4fb8c66678458c8e08b3222064ea3153129f31d1a81ff7ee6d25f1ac3928
3
+ metadata.gz: '0609ff0c2a5e95266162de30b16a2c10a9be1255f1ce4ed23672fea369dc67b0'
4
+ data.tar.gz: a7e2eb874c3be932c85e04f027d7c5da778bf6cc9671e1d173a169e62109ad83
5
5
  SHA512:
6
- metadata.gz: 890d2960cd53cec65a93b81b31fb61da535f123f4718d5e870eb2983813811352e2bbf93043007389ee85224590e2484ec0f2b0c5f377b384c5cf886923a87fd
7
- data.tar.gz: 1e2ab49bfc684afe96927971d2a57544b9d4898bb1271b920b88969e267b79f5271c4bf71ed07f45b4b160c5f27ef7ff943250c62e42eb1ce1c1c81d720e78ab
6
+ metadata.gz: 872e734331f516ae93d2c9a89452258ed448058597bfff3854e82d68b6e80bfe8a18a61a65167c3435c20776d8a58f01288787c441a2738cb126ee22db4620ca
7
+ data.tar.gz: 2f572680129343676ed4a92a9aa58d979f494d2da1fb26592681959ef14a4bd670c0f5cf9ac8705e611ce1d09384ccd78f56b16fbbd6285344c25f74557f9620
data/.gitignore CHANGED
@@ -1,10 +1,25 @@
1
+ .bundle
2
+ .config
3
+ *.gem
4
+ *.rbc
5
+ /_yardoc/
1
6
  /.bundle/
2
7
  /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
8
  /coverage/
6
9
  /doc/
10
+ /Gemfile.lock
7
11
  /pkg/
8
12
  /spec/reports/
9
13
  /tmp/
14
+ coverage
15
+ doc/
16
+ Gemfile.lock
17
+ InstalledFiles
18
+ lib/bundler/man
19
+ pkg
20
+ rdoc
21
+ spec/reports
22
+ test/tmp
23
+ test/version_tmp
24
+ tmp
10
25
  vendor/bundle
data/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [2.0.0] - 2023-12-03
7
+ - [#35](https://github.com/tongueroo/dynomite/pull/35) ActiveModel compatible
8
+ - Breaking change interface to be ActiveModel compatible
9
+ - ActiveModel: validations, callbacks, etc
10
+ - Use zeitwerk for autoloading
11
+ - Typecast support for DateTime-like objects. Store date as iso8601 string.
12
+ - Remove config/dynamodb.yml in favor of Dynomite.configure for use with initializers
13
+ - namespace separator default is _ instead of -
14
+ - Dynomite.logger introduced
15
+ - arel like where query builder interface
16
+ - finder methods: all, first, last, find_by, find, count
17
+ - index finder: automatically use query over scan with where when possible
18
+ - organize query to read and write ruby files
19
+ - Migrations: improved migrate command. No need to specify files.
20
+ - namespaced schema_migrations table tracks ran migrations.
21
+ - Favor ondemand provisioning vs explicit provisioned_throughput
22
+ - Standalone dynamodb cli to generate migrations and run them
23
+
24
+ ## [1.2.7] - 2022-06-12
25
+ - [#23](https://github.com/tongueroo/dynomite/pull/23) #where method refactor to allow Model.index_name('index').where(...)
26
+ - [#24](https://github.com/tongueroo/dynomite/pull/24) Add get_endpoint_ip to db_config.rb
27
+ - [#26](https://github.com/tongueroo/dynomite/pull/26) change pay_per_use to pay_per_request
28
+ - [#27](https://github.com/tongueroo/dynomite/pull/27) Fixed message that tells how to install dynamodb-local
29
+
6
30
  ## [1.2.6]
7
31
  - Implement the `PAY_PER_USE` billing mode for table creations and updates. See [DynamoDB On Demand](https://aws.amazon.com/blogs/aws/amazon-dynamodb-on-demand-no-capacity-planning-and-pay-per-request-pricing/).
8
32
 
data/Gemfile CHANGED
@@ -2,9 +2,5 @@ source "https://rubygems.org"
2
2
 
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
- # Specify your gem's dependencies in dynomite.gemspec
5
+ # Specify your gem dependencies in dynomite.gemspec
6
6
  gemspec
7
-
8
- group :development, :test do
9
- gem "activemodel"
10
- end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) Tung Nguyen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,195 +1,13 @@
1
1
  # Dynomite
2
2
 
3
- [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com)
4
-
5
- NOTE: Am looking for maintainers to help with this gem. Send me an email! Also [dynamoid](https://github.com/Dynamoid/dynamoid) seems like a good option that should be considered delegating to or straight using. Learning on delegation so we can have better default behavior for a DynamoDB model layer for Jets.
6
-
7
- A simple wrapper library to make DynamoDB usage a little more friendly. The modeling is ActiveRecord-ish but not exactly because DynamoDB is a different type of database. Examples below explain it best:
8
-
9
- ## Jets Docs
10
-
11
- * [Database DynamoDB](https://rubyonjets.com/docs/database/dynamodb/)
12
-
13
- ## Examples
14
-
15
- First define a class:
16
-
17
- ```ruby
18
- class Post < Dynomite::Item
19
- # partition_key "id" # optional, defaults to id
20
-
21
- column :id, :title, :desc
22
- end
23
- ```
24
-
25
- ### Create
26
-
27
- ```ruby
28
- post = Post.new
29
- post = post.replace(title: "test title")
30
- post.attrs # {"id" => "generated-id", title" => "test title"}
31
- ```
32
-
33
- `post.attrs[:id]` now contain a generated unique partition_key id. Usually the partition_key is 'id'. You can set your own unique id also by specifying id.
34
-
35
- ```ruby
36
- post = Post.new(id: "myid", title: "my title")
37
- post.replace
38
- post.attrs # {"id" => "myid", title" => "my title"}
39
- ```
40
-
41
- Note that the replace method replaces the entire item, so you need to merge the attributes if you want to keep the other attributes. Know this is weird, but this is how DynamoDB works.
42
-
43
- ### Find
44
-
45
- ```ruby
46
- post = Post.find("myid")
47
- post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
48
- post.replace
49
- post.attrs # {"id" => "myid", title" => "my title", desc: "my desc"}
50
- ```
51
-
52
- The convenience `attrs` method performs a deep_merge:
53
-
54
- ```ruby
55
- post = Post.find("myid")
56
- post.attrs("desc": "my desc 2") # <= does a deep_merge
57
- post.replace
58
- post.attrs # {"id" => "myid", title" => "my title", desc: "my desc 2"}
59
- ```
60
-
61
- Note, a race condition edge case can exist when several concurrent replace
62
- calls are happening. This is why the interface is called replace to
63
- emphasize that possibility.
64
-
65
- ### Delete
66
-
67
- ```ruby
68
- resp = Post.delete("myid") # dynamodb client resp
69
- # or
70
- post = Post.find("myid")
71
- resp = post.delete # dynamodb client resp
72
- ```
73
-
74
- ### Scan
75
-
76
- ```ruby
77
- options = {}
78
- posts = Post.scan(options)
79
- posts # Array of Post items. [Post.new, Post.new, ...]
80
- ```
81
-
82
- ### Query
83
-
84
- ```ruby
85
- posts = Post.query(
86
- index_name: 'category-index',
87
- expression_attribute_names: { "#category_name" => "category" },
88
- expression_attribute_values: { ":category_value" => "Entertainment" },
89
- key_condition_expression: "#category_name = :category_value",
90
- )
91
- posts # Array of Post items. [Post.new, Post.new, ...]
92
- ```
93
-
94
- ### Where
95
-
96
- The where could be prettied up. Appreciate any pull requests.
97
-
98
- ```ruby
99
- Post.where({category: "Drama"}, {index_name: "category-index"})
100
- ```
101
-
102
- Examples are also in [item_spec.rb](spec/lib/dynomite/item_spec.rb).
3
+ [![Gem Version](https://badge.fury.io/rb/dynomite.svg)](http://badge.fury.io/rb/dynomite)
103
4
 
104
- ## Column Lists
105
-
106
- You can define your column list using the `column` method inside your item class. This gives you
107
- a possibility to access your column fields using getters and setters. Also, any predefined column
108
- can be passed to `ActiveModel::Validations` (see Validations).
109
-
110
- ```ruby
111
- class Post < Dynomite::Item
112
- column :id, :name
113
- end
114
-
115
- post = Post.new
116
- post.name = "My First Post"
117
- post.replace
118
-
119
- puts post.id # 1962DE7D852298C5CDC809C0FEF50D8262CEDF09
120
- puts post.name # "My First Post"
121
- ```
122
-
123
- Note that any column not defined using the `column` method can still be accessed using the `attrs`
124
- method.
125
-
126
- ## Validations
127
-
128
- You can add validations known from ActiveRecord to your Dynomite items.
129
- Just add `include ActiveModel::Validations` at the top of your item class.
130
-
131
- ```ruby
132
- class Post < Dynomite::Item
133
- include ActiveModel::Validations
134
-
135
- column :id, :name # needed
136
-
137
- validates :id, presence: true
138
- validates :name, presence: true
139
- end
140
- ```
141
-
142
- **Be sure to define all validated columns using `column` method**.
143
-
144
- Validations are executed by default as soon as you call the `replace` method, returning `false` on
145
- failure. It also can be ran manually using the `valid?` method just like with ActiveRecord models.
146
-
147
-
148
- ## Migration Support
149
-
150
- Dynomite supports ActiveRecord-like migrations. Here's a short example:
151
-
152
- ```ruby
153
- class CreateCommentsMigration < Dynomite::Migration
154
- def up
155
- create_table :comments do |t|
156
- t.partition_key "post_id:string" # required
157
- t.sort_key "created_at:string" # optional
158
- t.provisioned_throughput(5) # sets both read and write, defaults to 5 when not set
159
- end
160
- end
161
- end
162
- ```
163
-
164
- More examples are in the [docs/migrations](docs/migrations) folder.
165
-
166
- ## Installation
167
-
168
- Add this line to your application's Gemfile:
169
-
170
- ```ruby
171
- gem 'dynomite'
172
- ```
173
-
174
- And then execute:
175
-
176
- $ bundle
177
-
178
- Or install it yourself as:
179
-
180
- $ gem install dynomite
181
-
182
- ## Development
183
-
184
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
185
-
186
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
5
+ [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com)
187
6
 
188
- ## Contributing
7
+ [![BoltOps Learn Badge](https://img.boltops.com/boltops-learn/boltops-learn.png)](https://learn.boltops.com)
189
8
 
190
- Bug reports and pull requests are welcome on GitHub at https://github.com/tongueroo/dynomite.
9
+ A DynamoDB ORM that is ActiveModel compatible.
191
10
 
192
- ### TODO
11
+ ## Dynomite Docs
193
12
 
194
- * improve Post.where. Something like `Post.index_name("user_id").where(category_name: "Entertainment")` would be nice.
195
- * implement `post.update` with `db.update_item` in a Ruby-ish way
13
+ * [Database Dynomite](https://rubyonjets.com/docs/database/dynamodb/)
data/Rakefile CHANGED
@@ -1,2 +1,14 @@
1
1
  require "bundler/gem_tasks"
2
- task :default => :spec
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new
7
+
8
+ require_relative "lib/dynomite"
9
+ require "cli_markdown"
10
+ desc "Generates cli reference docs as markdown"
11
+ task :docs do
12
+ mkdir_p "docs/_includes"
13
+ CliMarkdown::Creator.create_all(cli_class: Dynomite::CLI, cli_name: "dynomite")
14
+ end
data/dynomite.gemspec CHANGED
@@ -9,9 +9,9 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Tung Nguyen"]
10
10
  spec.email = ["tongueroo@gmail.com"]
11
11
 
12
- spec.summary = %q{ActiveRecord-ish Dynamodb Model}
13
- spec.description = %q{ActiveRecord-ish Dynamodb Model}
12
+ spec.summary = "ActiveRecord-ish DynamoDB ORM"
14
13
  spec.homepage = "https://github.com/tongueroo/dynomite"
14
+ spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
17
  f.match(%r{^(test|spec|features)/})
@@ -20,11 +20,18 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_dependency "activemodel"
23
24
  spec.add_dependency "activesupport"
24
25
  spec.add_dependency "aws-sdk-dynamodb"
26
+ spec.add_dependency "memoist"
25
27
  spec.add_dependency "rainbow"
28
+ spec.add_dependency "thor"
29
+ spec.add_dependency "zeitwerk"
26
30
 
27
31
  spec.add_development_dependency "bundler"
32
+ spec.add_development_dependency "byebug"
33
+ spec.add_development_dependency "cli_markdown"
34
+ spec.add_development_dependency "nokogiri"
28
35
  spec.add_development_dependency "rake"
29
36
  spec.add_development_dependency "rspec"
30
37
  end
data/exe/dynomite ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Trap ^C
4
+ Signal.trap("INT") {
5
+ puts "\nCtrl-C detected. Exiting..."
6
+ sleep 0.1
7
+ exit
8
+ }
9
+
10
+ $:.unshift(File.expand_path("../../lib", __FILE__))
11
+ require "dynomite"
12
+ require "dynomite/cli"
13
+
14
+ Dynomite::CLI.start(ARGV)
@@ -0,0 +1,126 @@
1
+ module Dynomite
2
+ # The base association module which all associations include. Every association has two very important components: the source and
3
+ # the target. The source is the object which is calling the association information. It always has the target_ids inside of an attribute on itself.
4
+ # The target is the object which is referencing by this association.
5
+ module Associations
6
+ module Association
7
+ attr_accessor :name, :options, :source, :loaded
8
+
9
+ # Create a new association.
10
+ #
11
+ # @param [Class] source the source record of the association; that is, the record that you already have
12
+ # @param [Symbol] name the name of the association
13
+ # @param [Hash] options optional parameters for the association
14
+ # @option options [Class] :class the target class of the association; that is, the class to which the association objects belong
15
+ # @option options [Symbol] :class_name the name of the target class of the association; only this or Class is necessary
16
+ # @option options [Symbol] :inverse_of the name of the association on the target class
17
+ # @option options [Symbol] :foreign_key the name of the field for belongs_to association
18
+ #
19
+ # @return [Dynomite::Association] the actual association instance itself
20
+ def initialize(source, name, options)
21
+ @source = source
22
+ @name = name
23
+ @options = options
24
+ @loaded = false
25
+ end
26
+
27
+ def coerce_to_id(object)
28
+ object.respond_to?(:partition_key) ? object.partition_key : object
29
+ end
30
+
31
+ def coerce_to_item(object)
32
+ object.is_a?(String) ? target_class.find(object) : object
33
+ end
34
+
35
+ def loaded?
36
+ @loaded
37
+ end
38
+
39
+ def find_target; end
40
+
41
+ def target
42
+ unless loaded?
43
+ @target = find_target
44
+ @loaded = true
45
+ end
46
+
47
+ @target
48
+ end
49
+
50
+ def reader_target
51
+ self
52
+ end
53
+
54
+ def reset
55
+ @target = nil
56
+ @loaded = false
57
+ end
58
+
59
+ def declaration_field_name
60
+ "#{name}_ids"
61
+ end
62
+
63
+ def declaration_field_type
64
+ :set
65
+ end
66
+
67
+ private
68
+
69
+ # The target class name, either inferred through the association's name or specified in options.
70
+ def target_class_name
71
+ options[:class_name] || name.to_s.classify
72
+ end
73
+
74
+ # The target class, either inferred through the association's name or specified in options.
75
+ def target_class
76
+ options[:class] || target_class_name.constantize
77
+ end
78
+
79
+ # The target attribute: that is, the attribute on each object of the association that should reference the source.
80
+ def target_attribute
81
+ # In simple case it's equivalent to
82
+ # "#{target_association}_ids".to_sym if target_association
83
+ if target_association
84
+ target_options = target_class.associations[target_association]
85
+ assoc = Dynomite::Associations.const_get(target_options[:type].to_s.camelcase).new(nil, target_association, target_options)
86
+ assoc.send(:source_attribute)
87
+ end
88
+ end
89
+
90
+ # The ids in the target association.
91
+ def target_ids
92
+ target.send(target_attribute) || Set.new
93
+ end
94
+
95
+ # The ids in the target association.
96
+ def source_class
97
+ source.class
98
+ end
99
+
100
+ # The source's association attribute: the name of the association with _ids afterwards, like "users_ids".
101
+ def source_attribute
102
+ declaration_field_name.to_sym
103
+ end
104
+
105
+ # The ids in the source association.
106
+ def source_ids
107
+ # handle case when we store scalar value instead of collection (when foreign_key option is specified)
108
+ Array(source.send(source_attribute)).compact.to_set || Set.new
109
+ end
110
+
111
+ # Create a new instance of the target class without trying to add it to the association. This creates a base, that caller can update before setting or adding it.
112
+ #
113
+ # @param attributes [Hash] attribute values for the new object
114
+ #
115
+ # @return [Dynomite::Item] the newly-created object
116
+ def build(attributes = {})
117
+ target_class.build(attributes)
118
+ end
119
+
120
+ def association_method_name(name)
121
+ name = name.to_s.end_with?("_association") ? name : "#{name}_association"
122
+ name.starts_with?("_") ? name : "_#{name}"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,35 @@
1
+ module Dynomite
2
+ # The belongs_to association. For belongs_to, we reference only a single target instead of multiple records; that target is the
3
+ # item to which the association item is associated.
4
+ module Associations
5
+ class BelongsTo
6
+ include SingleAssociation
7
+
8
+ def declaration_field_type
9
+ if options[:foreign_key]
10
+ target_class.attributes[target_class.partition_key][:type]
11
+ else
12
+ :set
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ # Find the target association, either has_many or has_one. Uses either options[:inverse_of] or the source class name and default parsing to
19
+ # return the most likely name for the target association.
20
+ def target_association
21
+ has_many_key_name = options[:inverse_of] || source.class.to_s.underscore.pluralize.to_sym
22
+ has_one_key_name = options[:inverse_of] || source.class.to_s.underscore.to_sym
23
+ unless target_class.associations[has_many_key_name].nil?
24
+ method_name = association_method_name(has_many_key_name)
25
+ return method_name if target_class.associations[has_many_key_name][:type] == :has_many
26
+ end
27
+
28
+ unless target_class.associations[has_one_key_name].nil?
29
+ method_name = association_method_name(has_one_key_name)
30
+ return method_name if target_class.associations[has_one_key_name][:type] == :has_one
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ module Dynomite
2
+ module Associations
3
+ class HasAndBelongsToMany
4
+ include ManyAssociation
5
+
6
+ private
7
+
8
+ # Find the target association, always another :has_and_belongs_to_many association. Uses either options[:inverse_of] or the source class name
9
+ # and default parsing to return the most likely name for the target association.
10
+ def target_association
11
+ key_name = options[:inverse_of] || source.class.to_s.pluralize.underscore.to_sym
12
+ guess = target_class.associations[key_name]
13
+ return nil if guess.nil? || guess[:type] != :has_and_belongs_to_many
14
+
15
+ association_method_name(key_name)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Dynomite
2
+ module Associations
3
+ class HasMany
4
+ include ManyAssociation
5
+
6
+ private
7
+
8
+ # Find the target association, always a :belongs_to association. Uses either options[:inverse_of] or the source class name
9
+ # and default parsing to return the most likely name for the target association.
10
+ def target_association
11
+ key_name = options[:inverse_of] || source.class.to_s.singularize.underscore.to_sym
12
+ guess = target_class.associations[key_name]
13
+ return nil if guess.nil? || guess[:type] != :belongs_to
14
+
15
+ association_method_name(key_name)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Dynomite
2
+ module Associations
3
+ class HasOne
4
+ include SingleAssociation
5
+
6
+ private
7
+
8
+ # Find the target association, always a :belongs_to association. Uses either options[:inverse_of] or the source class name
9
+ # and default parsing to return the most likely name for the target association.
10
+ def target_association
11
+ key_name = options[:inverse_of] || source.class.to_s.singularize.underscore.to_sym
12
+ guess = target_class.associations[key_name]
13
+ return nil if guess.nil? || guess[:type] != :belongs_to
14
+
15
+ association_method_name(key_name)
16
+ end
17
+ end
18
+ end
19
+ end