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
@@ -0,0 +1,257 @@
1
+ module Dynomite
2
+ module Associations
3
+ module ManyAssociation
4
+ include Association
5
+
6
+ attr_accessor :query
7
+
8
+ def initialize(*args)
9
+ @query = {}
10
+ super
11
+ end
12
+
13
+ include Enumerable
14
+
15
+ # Delegate methods to the records the association represents.
16
+ delegate :first, :last, :empty?, :size, :class, to: :records
17
+
18
+ # @return the has many association. IE: user.posts
19
+ def find_target
20
+ return [] if source_ids.empty?
21
+
22
+ # IE: user.posts - target class is Post
23
+ if target_class.partition_key_field == "id" && target_class.sort_key_field.nil?
24
+ # Quick find lookup
25
+ Array(target_class.find(source_ids.to_a, raise_error: false))
26
+ else
27
+ relation.to_a
28
+ end
29
+ end
30
+
31
+ def relation
32
+ return [] if source_ids.empty? # check again in case user calls relation directly. IE: user.posts.relation
33
+ # Slow scan lookup because of the in operator
34
+ target_class.where("id.in": source_ids.to_a)
35
+ end
36
+
37
+ def records
38
+ if query.empty?
39
+ target
40
+ else
41
+ results_with_query(target)
42
+ end
43
+ end
44
+
45
+ # Alias convenience methods for the associations.
46
+ alias all records
47
+ alias count size
48
+ alias nil? empty?
49
+
50
+ # Delegate include? to the records.
51
+ def include?(item)
52
+ records.include?(item)
53
+ end
54
+
55
+ # Add an item or array of items to an association.
56
+ #
57
+ # tag.posts << post
58
+ # tag.posts << [post1, post2, post3]
59
+ #
60
+ # This preserves the current records in the association (if any) and adds
61
+ # the item to the target association if it is detected to exist.
62
+ #
63
+ # It saves both models immediately - the source model and the target one
64
+ # so any not saved changes will be saved as well.
65
+ #
66
+ # @param item [Dynomite::Item|Array] model (or array of models) to add to the association
67
+ # @return [Dynomite::Item] the added model
68
+ def <<(item)
69
+ item = coerce_to_item(item)
70
+ # normal relationship
71
+ associate_one_way(item)
72
+
73
+ # inverse relationship
74
+ if target_association
75
+ Array(item).each { |obj| obj.send(target_association).associate_one_way(source) }
76
+ end
77
+
78
+ item
79
+ end
80
+ alias associate <<
81
+
82
+ def associate_one_way(item)
83
+ items = Array(item)
84
+ ids = items.collect { |o| coerce_to_id(o) }
85
+ ids = source_ids.merge(ids)
86
+ source.update_attribute_presence(source_attribute, ids)
87
+ end
88
+
89
+ # Removes an item or array of items from the association.
90
+ #
91
+ # tag.posts.disassociate(post)
92
+ # tag.posts.disassociate(post1, post2, post3)
93
+ # tag.posts.disassociate([post1, post2, post3])
94
+ #
95
+ # This removes their records from the association field on the source,
96
+ # and attempts to remove the source from the target association if it is
97
+ # detected to exist.
98
+ #
99
+ # It saves both models immediately - the source model and the target one
100
+ # so any not saved changes will be saved as well.
101
+ #
102
+ # @param item [Dynomite::Item|Array] model (or array of models) to remove from the association
103
+ # @return [Dynomite::Item|Array] the deleted model
104
+ def disassociate(*items)
105
+ items.flatten!
106
+ items.map! { |item| coerce_to_item(item) }
107
+ # normal relationship
108
+ items.each { |item| disassociate_one_way(item) }
109
+
110
+ # inverse relationship
111
+ if target_association
112
+ items.each { |obj| obj.send(target_association).disassociate_one_way(source) }
113
+ end
114
+ end
115
+
116
+ def disassociate_one_way(item)
117
+ ids = source_ids - Array(coerce_to_id(item))
118
+ source.update_attribute_presence(source_attribute, ids)
119
+ end
120
+
121
+ def disassociate_all
122
+ # target is all items. IE: user.posts
123
+ target.each do |item|
124
+ disassociate(item)
125
+ end
126
+ end
127
+
128
+ # Replace an association with item or array of items. This removes all of the existing associated records and replaces them with
129
+ # the passed item(s), and associates the target association if it is detected to exist.
130
+ #
131
+ # @param [Dynomite::Item] item the item (or array of items) to add to the association
132
+ #
133
+ # @return [Dynomite::Item|Array] the added item
134
+ def setter(item)
135
+ target.each { |i| disassociate(i) }
136
+ self << item
137
+ item
138
+ end
139
+
140
+ # Create a new instance of the target class, persist it and add directly
141
+ # to the association.
142
+ #
143
+ # tag.posts.create!(title: 'foo')
144
+ #
145
+ # Several models can be created at once when an array of attributes
146
+ # specified:
147
+ #
148
+ # tag.posts.create!([{ title: 'foo' }, {title: 'bar'} ])
149
+ #
150
+ # If the creation fails an exception will be raised.
151
+ #
152
+ # @param attributes [Hash] attribute values for the new item
153
+ # @return [Dynomite::Item|Array] the newly-created item
154
+ def create!(attributes = {})
155
+ self << target_class.create!(attributes)
156
+ end
157
+
158
+ # Create a new instance of the target class, persist it and add directly
159
+ # to the association.
160
+ #
161
+ # tag.posts.create(title: 'foo')
162
+ #
163
+ # Several models can be created at once when an array of attributes
164
+ # specified:
165
+ #
166
+ # tag.posts.create([{ title: 'foo' }, {title: 'bar'} ])
167
+ #
168
+ # @param attributes [Hash] attribute values for the new item
169
+ # @return [Dynomite::Item|Array] the newly-created item
170
+ def create(attributes = {})
171
+ self << target_class.create(attributes)
172
+ end
173
+
174
+ # Create a new instance of the target class and add it directly to the association. If the create fails an exception will be raised.
175
+ #
176
+ # @return [Dynomite::Item] the newly-created item
177
+ def each(&block)
178
+ records.each(&block)
179
+ end
180
+
181
+ # Destroys all members of the association and removes them from the
182
+ # association.
183
+ #
184
+ # tag.posts.destroy_all
185
+ #
186
+ def destroy_all
187
+ objs = target
188
+ source.update_attribute_presence(source_attribute, nil)
189
+ objs.each(&:destroy)
190
+ end
191
+
192
+ # Deletes all members of the association and removes them from the
193
+ # association.
194
+ #
195
+ # tag.posts.delete_all
196
+ #
197
+ def delete_all
198
+ objs = target
199
+ source.update_attribute_presence(source_attribute, nil)
200
+ objs.each(&:delete)
201
+ end
202
+
203
+ def destroy_all
204
+ objs = target
205
+ source.update_attribute_presence(source_attribute, nil)
206
+ objs.each(&:destroy)
207
+ end
208
+
209
+ # Naive association filtering.
210
+ #
211
+ # tag.posts.where(title: 'foo')
212
+ #
213
+ # It loads lazily all the associated models and checks provided
214
+ # conditions. That's why only equality conditions can be specified.
215
+ #
216
+ # @param args [Hash] A hash of attributes; each must match every returned item's attribute exactly.
217
+ # @return [Dynomite::Association] the association this method was called on (for chaining purposes)
218
+ def where(args)
219
+ filtered = clone
220
+ filtered.query = query.clone
221
+ args.each { |k, v| filtered.query[k] = v }
222
+ filtered
223
+ end
224
+
225
+ # Is this array equal to the association's records?
226
+ #
227
+ # @return [Boolean] true/false
228
+ def ==(other)
229
+ records == Array(other)
230
+ end
231
+
232
+ # Delegate methods we don't find directly to the records array.
233
+ def method_missing(method, *args)
234
+ if records.respond_to?(method)
235
+ records.send(method, *args)
236
+ else
237
+ super
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ # If a query exists, filter all existing results based on that query.
244
+ #
245
+ # @param [Array] results the raw results for the association
246
+ #
247
+ # @return [Array] the filtered results for the query
248
+ def results_with_query(results)
249
+ results.find_all do |result|
250
+ query.all? do |attribute, value|
251
+ result.send(attribute) == value
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,157 @@
1
+ module Dynomite
2
+ module Associations
3
+ module SingleAssociation
4
+ include Association
5
+
6
+ # target field name. IE: posts.user_id
7
+ def declaration_field_name
8
+ options[:foreign_key] || "#{name}_id"
9
+ end
10
+
11
+ def reader_target
12
+ target
13
+ end
14
+
15
+ def setter(item)
16
+ if item.nil?
17
+ disassociate
18
+ else
19
+ associate(item)
20
+ end
21
+ end
22
+
23
+ def associate(item)
24
+ item = coerce_to_item(item)
25
+ associate_one_way(item)
26
+
27
+ # inverse relationship
28
+ should_reload = false
29
+ Array(target).each do |target_entry|
30
+ if target_entry && target_association
31
+ target_entry.send(target_association).associate_one_way(source)
32
+ should_reload = true
33
+ end
34
+ end
35
+ target.reload if should_reload
36
+
37
+ self.target = item
38
+ end
39
+
40
+ def associate_one_way(item)
41
+ # normal relationship
42
+ source.update_attribute_presence(source_attribute, coerce_to_id(item))
43
+ end
44
+
45
+ def disassociate(_ = nil)
46
+ # inverse relationship: user.posts removal. run first before target is nil
47
+ should_reload = false
48
+ Array(target).each do |target_entry|
49
+ if target_entry && target_association
50
+ target_entry.send(target_association).disassociate_one_way(source)
51
+ should_reload = true
52
+ end
53
+ end
54
+ target.reload if should_reload
55
+
56
+ # normal relationship: post.user removal
57
+ disassociate_one_way
58
+
59
+ self.target = nil
60
+ end
61
+
62
+ # Delete a model from the association.
63
+ #
64
+ # post.logo.disassociate # => nil
65
+ #
66
+ # Saves both models immediately - a source model and a target one so any
67
+ # unsaved changes will be saved. Doesn't delete an associated model from
68
+ # DynamoDB.
69
+ #
70
+ # _ = nil so can keep the same interface for removing has_many_and_belongs_to associations
71
+ #
72
+ def disassociate_one_way(_ = nil)
73
+ # normal relationship: post.user removal
74
+ source.update_attribute_presence(source_attribute, nil)
75
+ end
76
+
77
+ # Create a new instance of the target class, persist it and associate.
78
+ #
79
+ # post.logo.create!(hight: 50, width: 90)
80
+ #
81
+ # If the creation fails an exception will be raised.
82
+ #
83
+ # @param attributes [Hash] attributes of a model to create
84
+ # @return [Dynomite::Item] created model
85
+ def create!(attributes = {})
86
+ setter(target_class.create!(attributes))
87
+ end
88
+
89
+ # Create a new instance of the target class, persist it and associate.
90
+ #
91
+ # post.logo.create(hight: 50, width: 90)
92
+ #
93
+ # @param attributes [Hash] attributes of a model to create
94
+ # @return [Dynomite::Item] created model
95
+ def create(attributes = {})
96
+ setter(target_class.create(attributes))
97
+ end
98
+
99
+ # Is this item equal to the association's target?
100
+ #
101
+ # @return [Boolean] true/false
102
+ def ==(other)
103
+ target == other
104
+ end
105
+
106
+ if ::RUBY_VERSION < '2.7'
107
+ # Delegate methods we don't find directly to the target.
108
+ def method_missing(method, *args, &block)
109
+ if target.respond_to?(method)
110
+ target.send(method, *args, &block)
111
+ else
112
+ super
113
+ end
114
+ end
115
+ else
116
+ # Delegate methods we don't find directly to the target.
117
+ def method_missing(method, *args, **kwargs, &block)
118
+ if target.respond_to?(method)
119
+ target.send(method, *args, **kwargs, &block)
120
+ else
121
+ super
122
+ end
123
+ end
124
+ end
125
+
126
+ def respond_to_missing?(method_name, include_private = false)
127
+ target.respond_to?(method_name, include_private) || super
128
+ end
129
+
130
+ def nil?
131
+ target.nil?
132
+ end
133
+
134
+ def empty?
135
+ # This is needed to that ActiveSupport's #blank? and #present?
136
+ # methods work as expected for SingleAssociations.
137
+ target.nil?
138
+ end
139
+
140
+ private
141
+
142
+ # Find the target of the has_one association.
143
+ #
144
+ # @return [Dynomite::Item] the found target (or nil if nothing)
145
+ def find_target
146
+ return if source_ids.empty?
147
+
148
+ target_class.find(source_ids.first, raise_error: false)
149
+ end
150
+
151
+ def target=(item)
152
+ @target = item
153
+ @loaded = true
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynomite
4
+ # Connects models together through the magic of associations. We enjoy four different kinds of associations presently:
5
+ # * belongs_to
6
+ # * has_and_belongs_to_many
7
+ # * has_many
8
+ # * has_one
9
+ module Associations
10
+ extend ActiveSupport::Concern
11
+
12
+ module ClassMethods
13
+ # Create the association tracking attribute and initialize it to an empty hash.
14
+ def inherited(base)
15
+ base.class_attribute :associations, instance_accessor: false
16
+ base.associations = {}
17
+ end
18
+
19
+ # Declare a +has_many+ association for this document.
20
+ #
21
+ # class Category < ApplicationItem
22
+ # has_many :posts
23
+ # end
24
+ #
25
+ # Association is an enumerable collection and supports following addition
26
+ # operations:
27
+ #
28
+ # * +create+
29
+ # * +create!+
30
+ # * +destroy_all+
31
+ # * +delete_all+
32
+ # * +delete+
33
+ # * +<<+
34
+ # * +where+
35
+ # * +all+
36
+ # * +empty?+
37
+ # * +size+
38
+ #
39
+ # When a name of an associated class doesn't match an association name a
40
+ # class name should be specified explicitly either with +class+ or
41
+ # +class_name+ option:
42
+ #
43
+ # has_many :labels, class: Tag
44
+ # has_many :labels, class_name: 'Tag'
45
+ #
46
+ # When associated class has own +belongs_to+ association to
47
+ # the current class and the name doesn't match a name of the current
48
+ # class this name can be specified with +inverse_of+ option:
49
+ #
50
+ # class Post < ApplicationItem
51
+ # belongs_to :item, class_name: 'Tag'
52
+ # end
53
+ #
54
+ # class Tag < ApplicationItem
55
+ # has_many :posts, inverse_of: :item
56
+ # end
57
+ #
58
+ # @param name [Symbol] the name of the association
59
+ # @param options [Hash] options to pass to the association constructor
60
+ # @option options [Class] :class the target class of the has_many association; that is, the belongs_to class
61
+ # @option options [String] :class_name the name of the target class of the association; that is, the name of the belongs_to class
62
+ # @option options [Symbol] :inverse_of the name of the association on the target class; that is, if the class has a belongs_to association, the name of that association
63
+ #
64
+ # @since 0.2.0
65
+ def has_many(name, options = {})
66
+ association(:has_many, name, options)
67
+ end
68
+
69
+ # Declare a +has_one+ association for this document.
70
+ #
71
+ # class Image < ApplicationItem
72
+ # has_one :post
73
+ # end
74
+ #
75
+ # Association supports following operations:
76
+ #
77
+ # * +create+
78
+ # * +create!+
79
+ # * +delete+
80
+ #
81
+ # When a name of an associated class doesn't match an association name a
82
+ # class name should be specified explicitly either with +class+ or
83
+ # +class_name+ option:
84
+ #
85
+ # has_one :item, class: Post
86
+ # has_one :item, class_name: 'Post'
87
+ #
88
+ # When associated class has own +belong_to+ association to the current
89
+ # class and the name doesn't match a name of the current class this name
90
+ # can be specified with +inverse_of+ option:
91
+ #
92
+ # class Post < ApplicationItem
93
+ # belongs_to :logo, class_name: 'Image'
94
+ # end
95
+ #
96
+ # class Image < ApplicationItem
97
+ # has_one :post, inverse_of: :logo
98
+ # end
99
+ #
100
+ # @param name [Symbol] the name of the association
101
+ # @param options [Hash] options to pass to the association constructor
102
+ # @option options [Class] :class the target class of the has_one association; that is, the belongs_to class
103
+ # @option options [String] :class_name the name of the target class of the association; that is, the name of the belongs_to class
104
+ # @option options [Symbol] :inverse_of the name of the association on the target class; that is, if the class has a belongs_to association, the name of that association
105
+ #
106
+ # @since 0.2.0
107
+ def has_one(name, options = {})
108
+ association(:has_one, name, options)
109
+ end
110
+
111
+ # Declare a +belongs_to+ association for this document.
112
+ #
113
+ # class Post < ApplicationItem
114
+ # belongs_to :categories
115
+ # end
116
+ #
117
+ # Association supports following operations:
118
+ #
119
+ # * +create+
120
+ # * +create!+
121
+ # * +delete+
122
+ #
123
+ # When a name of an associated class doesn't match an association name a
124
+ # class name should be specified explicitly either with +class+ or
125
+ # +class_name+ option:
126
+ #
127
+ # belongs_to :item, class: Post
128
+ # belongs_to :item, class_name: 'Post'
129
+ #
130
+ # When associated class has own +has_many+ or +has_one+ association to
131
+ # the current class and the name doesn't match a name of the current
132
+ # class this name can be specified with +inverse_of+ option:
133
+ #
134
+ # class Category < ApplicationItem
135
+ # has_many :items, class_name: 'Post'
136
+ # end
137
+ #
138
+ # class Post < ApplicationItem
139
+ # belongs_to :categories, inverse_of: :items
140
+ # end
141
+ #
142
+ # By default a hash key attribute name is +id+. If an associated class
143
+ # uses another name for a hash key attribute it should be specified in
144
+ # the +belongs_to+ association:
145
+ #
146
+ # belongs_to :categories, foreign_key: :uuid
147
+ #
148
+ # @param name [Symbol] the name of the association
149
+ # @param options [Hash] options to pass to the association constructor
150
+ # @option options [Class] :class the target class of the has_one association; that is, the has_many or has_one class
151
+ # @option options [String] :class_name the name of the target class of the association; that is, the name of the has_many or has_one class
152
+ # @option options [Symbol] :inverse_of the name of the association on the target class; that is, if the class has a has_many or has_one association, the name of that association
153
+ # @option options [Symbol] :foreign_key the name of a hash key attribute in the target class
154
+ #
155
+ # @since 0.2.0
156
+ def belongs_to(name, options = {})
157
+ association(:belongs_to, name, options)
158
+ end
159
+
160
+ # Declare a +has_and_belongs_to_many+ association for this document.
161
+ #
162
+ # class Post < ApplicationItem
163
+ # has_and_belongs_to_many :tags
164
+ # end
165
+ #
166
+ # Association is an enumerable collection and supports following addition
167
+ # operations:
168
+ #
169
+ # * +create+
170
+ # * +create!+
171
+ # * +destroy_all+
172
+ # * +delete_all+
173
+ # * +delete+
174
+ # * +<<+
175
+ # * +where+
176
+ # * +all+
177
+ # * +empty?+
178
+ # * +size+
179
+ #
180
+ # When a name of an associated class doesn't match an association name a
181
+ # class name should be specified explicitly either with +class+ or
182
+ # +class_name+ option:
183
+ #
184
+ # has_and_belongs_to_many :labels, class: Tag
185
+ # has_and_belongs_to_many :labels, class_name: 'Tag'
186
+ #
187
+ # When associated class has own +has_and_belongs_to_many+ association to
188
+ # the current class and the name doesn't match a name of the current
189
+ # class this name can be specified with +inverse_of+ option:
190
+ #
191
+ # class Tag < ApplicationItem
192
+ # has_and_belongs_to_many :items, class_name: 'Post'
193
+ # end
194
+ #
195
+ # class Post < ApplicationItem
196
+ # has_and_belongs_to_many :tags, inverse_of: :items
197
+ # end
198
+ #
199
+ # @param name [Symbol] the name of the association
200
+ # @param options [Hash] options to pass to the association constructor
201
+ # @option options [Class] :class the target class of the has_and_belongs_to_many association; that is, the belongs_to class
202
+ # @option options [String] :class_name the name of the target class of the association; that is, the name of the belongs_to class
203
+ # @option options [Symbol] :inverse_of the name of the association on the target class; that is, if the class has a belongs_to association, the name of that association
204
+ #
205
+ # @since 0.2.0
206
+ def has_and_belongs_to_many(name, options = {})
207
+ association(:has_and_belongs_to_many, name, options)
208
+ end
209
+
210
+ private
211
+
212
+ # create getters and setters for an association.
213
+ #
214
+ # @param type [Symbol] the type (:has_one, :has_many, :has_and_belongs_to_many, :belongs_to) of the association
215
+ # @param name [Symbol] the name of the association
216
+ # @param options [Hash] options to pass to the association constructor; see above for all valid options
217
+ #
218
+ # @since 0.2.0
219
+ def association(type, name, options = {})
220
+ # Declare document field.
221
+ # In simple case it's equivalent to
222
+ # field "#{name}_ids".to_sym, :set
223
+ assoc = Dynomite::Associations.const_get(type.to_s.camelcase).new(nil, name, options)
224
+ field_name = assoc.declaration_field_name
225
+ field_type = assoc.declaration_field_type
226
+
227
+ field field_name.to_sym, type: field_type
228
+
229
+ associations[name] = options.merge(type: type)
230
+
231
+ define_method(name) do
232
+ # Do not cache reader method. it causes issues with user.posts << post. The user.posts is stale.
233
+ Dynomite::Associations.const_get(type.to_s.camelcase).new(self, name, options).reader_target
234
+ end
235
+
236
+ # hidden method so we still have access to the association object
237
+ define_method("_#{name}_association") do
238
+ @associations[:"#{name}_association"] ||= Dynomite::Associations.const_get(type.to_s.camelcase).new(self, name, options)
239
+ end
240
+
241
+ define_method("#{name}=".to_sym) do |objects|
242
+ @associations[:"#{name}_association"] ||= Dynomite::Associations.const_get(type.to_s.camelcase).new(self, name, options)
243
+ @associations[:"#{name}_association"].setter(objects)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,25 @@
1
+ require "zeitwerk"
2
+
3
+ module Dynomite
4
+ class Autoloader
5
+ class Inflector < Zeitwerk::Inflector
6
+ def camelize(basename, _abspath)
7
+ map = { cli: "CLI", version: "VERSION" }
8
+ map[basename.to_sym] || super
9
+ end
10
+ end
11
+
12
+ class << self
13
+ def setup
14
+ loader = Zeitwerk::Loader.new
15
+ loader.inflector = Inflector.new
16
+ loader.push_dir(File.dirname(__dir__)) # lib
17
+ loader.ignore("#{File.dirname(__dir__)}/jets/commands")
18
+ loader.ignore("#{__dir__}/migration/internal/*")
19
+ loader.ignore("#{__dir__}/migration/templates/*")
20
+ loader.ignore("#{__dir__}/reserved_words.rb")
21
+ loader.setup
22
+ end
23
+ end
24
+ end
25
+ end