dynomite 1.2.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -190
  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 -286
  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/reserved_words.rb +13 -3
  103. data/lib/dynomite/seed.rb +12 -0
  104. data/lib/dynomite/types.rb +22 -0
  105. data/lib/dynomite/version.rb +1 -1
  106. data/lib/dynomite/waiter.rb +40 -0
  107. data/lib/dynomite.rb +11 -17
  108. data/lib/generators/application_item/application_item_generator.rb +30 -0
  109. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  110. data/lib/jets/commands/dynamodb_command.rb +29 -0
  111. data/lib/jets/commands/help/generate.md +33 -0
  112. data/lib/jets/commands/help/migrate.md +3 -0
  113. metadata +201 -17
  114. data/docs/migrations/long-example.rb +0 -127
  115. data/docs/migrations/short-example.rb +0 -40
  116. data/lib/dynomite/db_config.rb +0 -121
  117. data/lib/dynomite/errors.rb +0 -15
  118. data/lib/dynomite/log.rb +0 -15
  119. data/lib/dynomite/migration/common.rb +0 -86
  120. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  121. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  122. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  123. data/lib/dynomite/migration/executor.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a0639976cc739134d6670945d099f3584b39f88f058e056678591a42c09be84
4
- data.tar.gz: 6ad8c28d9147a175cf940ec756d808b66a948c27be74239ab95dc3b589dd9d74
3
+ metadata.gz: '0609ff0c2a5e95266162de30b16a2c10a9be1255f1ce4ed23672fea369dc67b0'
4
+ data.tar.gz: a7e2eb874c3be932c85e04f027d7c5da778bf6cc9671e1d173a169e62109ad83
5
5
  SHA512:
6
- metadata.gz: 3f7f4e2dcb3163c4cc60065c3d0f74b835bd4763f6295ae00d8f92576f2c57afcc969a5b4bf0c727d9b8a45ab17e52ae573319a819cf3f7c0974cf29a91b19ac
7
- data.tar.gz: c37b011a90f943e6e04fbdfa248dee7b8bcf1dae762ae5175c976e02325e462118715d8340f5d42022e62a40ab1b77aa80af532544e255eed2c2fec89ecd6aa8
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,24 @@
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
+
6
24
  ## [1.2.7] - 2022-06-12
7
25
  - [#23](https://github.com/tongueroo/dynomite/pull/23) #where method refactor to allow Model.index_name('index').where(...)
8
26
  - [#24](https://github.com/tongueroo/dynomite/pull/24) Add get_endpoint_ip to db_config.rb
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,197 +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!
6
-
7
- IMPORTANT: The next major version of Dynomite will be ActiveModel compatible. A POC is in the [edge](https://github.com/tongueroo/dynomite/tree/edge) branch. It's still very rough and experimental. Would not recommend using it yet, but wanted to note it.
8
-
9
- 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:
10
-
11
- ## Jets Docs
12
-
13
- * [Database DynamoDB](https://rubyonjets.com/docs/database/dynamodb/)
14
-
15
- ## Examples
16
-
17
- First define a class:
18
-
19
- ```ruby
20
- class Post < Dynomite::Item
21
- # partition_key "id" # optional, defaults to id
22
-
23
- column :id, :title, :desc
24
- end
25
- ```
26
-
27
- ### Create
28
-
29
- ```ruby
30
- post = Post.new
31
- post = post.replace(title: "test title")
32
- post.attrs # {"id" => "generated-id", title" => "test title"}
33
- ```
34
-
35
- `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.
36
-
37
- ```ruby
38
- post = Post.new(id: "myid", title: "my title")
39
- post.replace
40
- post.attrs # {"id" => "myid", title" => "my title"}
41
- ```
42
-
43
- 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.
44
-
45
- ### Find
46
-
47
- ```ruby
48
- post = Post.find("myid")
49
- post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
50
- post.replace
51
- post.attrs # {"id" => "myid", title" => "my title", desc: "my desc"}
52
- ```
53
-
54
- The convenience `attrs` method performs a deep_merge:
55
-
56
- ```ruby
57
- post = Post.find("myid")
58
- post.attrs("desc": "my desc 2") # <= does a deep_merge
59
- post.replace
60
- post.attrs # {"id" => "myid", title" => "my title", desc: "my desc 2"}
61
- ```
62
-
63
- Note, a race condition edge case can exist when several concurrent replace
64
- calls are happening. This is why the interface is called replace to
65
- emphasize that possibility.
66
-
67
- ### Delete
68
-
69
- ```ruby
70
- resp = Post.delete("myid") # dynamodb client resp
71
- # or
72
- post = Post.find("myid")
73
- resp = post.delete # dynamodb client resp
74
- ```
75
-
76
- ### Scan
77
-
78
- ```ruby
79
- options = {}
80
- posts = Post.scan(options)
81
- posts # Array of Post items. [Post.new, Post.new, ...]
82
- ```
83
-
84
- ### Query
85
-
86
- ```ruby
87
- posts = Post.query(
88
- index_name: 'category-index',
89
- expression_attribute_names: { "#category_name" => "category" },
90
- expression_attribute_values: { ":category_value" => "Entertainment" },
91
- key_condition_expression: "#category_name = :category_value",
92
- )
93
- posts # Array of Post items. [Post.new, Post.new, ...]
94
- ```
95
-
96
- ### Where
97
-
98
- The where could be prettied up. Appreciate any pull requests.
99
-
100
- ```ruby
101
- Post.where({category: "Drama"}, {index_name: "category-index"})
102
- ```
3
+ [![Gem Version](https://badge.fury.io/rb/dynomite.svg)](http://badge.fury.io/rb/dynomite)
103
4
 
104
- Examples are also in [item_spec.rb](spec/lib/dynomite/item_spec.rb).
105
-
106
- ## Column Lists
107
-
108
- You can define your column list using the `column` method inside your item class. This gives you
109
- a possibility to access your column fields using getters and setters. Also, any predefined column
110
- can be passed to `ActiveModel::Validations` (see Validations).
111
-
112
- ```ruby
113
- class Post < Dynomite::Item
114
- column :id, :name
115
- end
116
-
117
- post = Post.new
118
- post.name = "My First Post"
119
- post.replace
120
-
121
- puts post.id # 1962DE7D852298C5CDC809C0FEF50D8262CEDF09
122
- puts post.name # "My First Post"
123
- ```
124
-
125
- Note that any column not defined using the `column` method can still be accessed using the `attrs`
126
- method.
127
-
128
- ## Validations
129
-
130
- You can add validations known from ActiveRecord to your Dynomite items.
131
- Just add `include ActiveModel::Validations` at the top of your item class.
132
-
133
- ```ruby
134
- class Post < Dynomite::Item
135
- include ActiveModel::Validations
136
-
137
- column :id, :name # needed
138
-
139
- validates :id, presence: true
140
- validates :name, presence: true
141
- end
142
- ```
143
-
144
- **Be sure to define all validated columns using `column` method**.
145
-
146
- Validations are executed by default as soon as you call the `replace` method, returning `false` on
147
- failure. It also can be ran manually using the `valid?` method just like with ActiveRecord models.
148
-
149
-
150
- ## Migration Support
151
-
152
- Dynomite supports ActiveRecord-like migrations. Here's a short example:
153
-
154
- ```ruby
155
- class CreateCommentsMigration < Dynomite::Migration
156
- def up
157
- create_table :comments do |t|
158
- t.partition_key "post_id:string" # required
159
- t.sort_key "created_at:string" # optional
160
- t.provisioned_throughput(5) # sets both read and write, defaults to 5 when not set
161
- end
162
- end
163
- end
164
- ```
165
-
166
- More examples are in the [docs/migrations](docs/migrations) folder.
167
-
168
- ## Installation
169
-
170
- Add this line to your application's Gemfile:
171
-
172
- ```ruby
173
- gem 'dynomite'
174
- ```
175
-
176
- And then execute:
177
-
178
- $ bundle
179
-
180
- Or install it yourself as:
181
-
182
- $ gem install dynomite
183
-
184
- ## Development
185
-
186
- 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.
187
-
188
- 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)
189
6
 
190
- ## Contributing
7
+ [![BoltOps Learn Badge](https://img.boltops.com/boltops-learn/boltops-learn.png)](https://learn.boltops.com)
191
8
 
192
- Bug reports and pull requests are welcome on GitHub at https://github.com/tongueroo/dynomite.
9
+ A DynamoDB ORM that is ActiveModel compatible.
193
10
 
194
- ### TODO
11
+ ## Dynomite Docs
195
12
 
196
- * improve Post.where. Something like `Post.index_name("user_id").where(category_name: "Entertainment")` would be nice.
197
- * 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