dynomite 1.2.7 → 2.0.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.
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