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.
- checksums.yaml +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +18 -0
- data/Gemfile +1 -5
- data/LICENSE.txt +22 -0
- data/README.md +6 -190
- data/Rakefile +13 -1
- data/dynomite.gemspec +9 -2
- data/exe/dynomite +14 -0
- data/lib/dynomite/associations/association.rb +126 -0
- data/lib/dynomite/associations/belongs_to.rb +35 -0
- data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
- data/lib/dynomite/associations/has_many.rb +19 -0
- data/lib/dynomite/associations/has_one.rb +19 -0
- data/lib/dynomite/associations/many_association.rb +257 -0
- data/lib/dynomite/associations/single_association.rb +157 -0
- data/lib/dynomite/associations.rb +248 -0
- data/lib/dynomite/autoloader.rb +25 -0
- data/lib/dynomite/cli.rb +48 -0
- data/lib/dynomite/client.rb +118 -0
- data/lib/dynomite/command.rb +89 -0
- data/lib/dynomite/completer/script.rb +6 -0
- data/lib/dynomite/completer/script.sh +10 -0
- data/lib/dynomite/completer.rb +159 -0
- data/lib/dynomite/config.rb +39 -0
- data/lib/dynomite/core.rb +18 -19
- data/lib/dynomite/engine.rb +45 -0
- data/lib/dynomite/erb.rb +5 -3
- data/lib/dynomite/error.rb +12 -0
- data/lib/dynomite/help/completion.md +20 -0
- data/lib/dynomite/help/completion_script.md +3 -0
- data/lib/dynomite/help/migrate.md +3 -0
- data/lib/dynomite/help.rb +9 -0
- data/lib/dynomite/install.rb +4 -0
- data/lib/dynomite/item/abstract.rb +15 -0
- data/lib/dynomite/item/components.rb +33 -0
- data/lib/dynomite/item/dsl.rb +101 -0
- data/lib/dynomite/item/id.rb +41 -0
- data/lib/dynomite/item/indexes/finder.rb +58 -0
- data/lib/dynomite/item/indexes/index.rb +21 -0
- data/lib/dynomite/item/indexes/primary_index.rb +18 -0
- data/lib/dynomite/item/indexes.rb +25 -0
- data/lib/dynomite/item/locking.rb +53 -0
- data/lib/dynomite/item/magic_fields.rb +66 -0
- data/lib/dynomite/item/primary_key.rb +85 -0
- data/lib/dynomite/item/query/delegates.rb +28 -0
- data/lib/dynomite/item/query/params/base.rb +42 -0
- data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
- data/lib/dynomite/item/query/params/filter.rb +41 -0
- data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
- data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
- data/lib/dynomite/item/query/params/function/base.rb +33 -0
- data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
- data/lib/dynomite/item/query/params/function/contains.rb +7 -0
- data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
- data/lib/dynomite/item/query/params/helpers.rb +94 -0
- data/lib/dynomite/item/query/params/key_condition.rb +34 -0
- data/lib/dynomite/item/query/params.rb +115 -0
- data/lib/dynomite/item/query/partiql/executer.rb +72 -0
- data/lib/dynomite/item/query/partiql.rb +67 -0
- data/lib/dynomite/item/query/relation/chain.rb +125 -0
- data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
- data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
- data/lib/dynomite/item/query/relation/delete.rb +38 -0
- data/lib/dynomite/item/query/relation/ids.rb +21 -0
- data/lib/dynomite/item/query/relation/math.rb +19 -0
- data/lib/dynomite/item/query/relation/where_field.rb +32 -0
- data/lib/dynomite/item/query/relation/where_group.rb +78 -0
- data/lib/dynomite/item/query/relation.rb +127 -0
- data/lib/dynomite/item/query.rb +7 -0
- data/lib/dynomite/item/read/find.rb +196 -0
- data/lib/dynomite/item/read/find_with_event.rb +42 -0
- data/lib/dynomite/item/read.rb +90 -0
- data/lib/dynomite/item/sti.rb +43 -0
- data/lib/dynomite/item/table_namespace.rb +43 -0
- data/lib/dynomite/item/typecaster.rb +106 -0
- data/lib/dynomite/item/waiter_methods.rb +18 -0
- data/lib/dynomite/item/write/base.rb +15 -0
- data/lib/dynomite/item/write/delete_item.rb +14 -0
- data/lib/dynomite/item/write/put_item.rb +99 -0
- data/lib/dynomite/item/write/update_item.rb +73 -0
- data/lib/dynomite/item/write.rb +204 -0
- data/lib/dynomite/item.rb +113 -286
- data/lib/dynomite/migration/dsl/accessor.rb +19 -0
- data/lib/dynomite/migration/dsl/index/base.rb +42 -0
- data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
- data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
- data/lib/dynomite/migration/dsl/index.rb +72 -0
- data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
- data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
- data/lib/dynomite/migration/dsl.rb +89 -142
- data/lib/dynomite/migration/file_info.rb +28 -0
- data/lib/dynomite/migration/generator.rb +30 -16
- data/lib/dynomite/migration/helpers.rb +7 -0
- data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
- data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
- data/lib/dynomite/migration/runner.rb +178 -0
- data/lib/dynomite/migration/templates/create_table.rb +7 -23
- data/lib/dynomite/migration/templates/delete_table.rb +7 -0
- data/lib/dynomite/migration/templates/update_table.rb +3 -18
- data/lib/dynomite/migration.rb +53 -10
- data/lib/dynomite/reserved_words.rb +13 -3
- data/lib/dynomite/seed.rb +12 -0
- data/lib/dynomite/types.rb +22 -0
- data/lib/dynomite/version.rb +1 -1
- data/lib/dynomite/waiter.rb +40 -0
- data/lib/dynomite.rb +11 -17
- data/lib/generators/application_item/application_item_generator.rb +30 -0
- data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
- data/lib/jets/commands/dynamodb_command.rb +29 -0
- data/lib/jets/commands/help/generate.md +33 -0
- data/lib/jets/commands/help/migrate.md +3 -0
- metadata +201 -17
- data/docs/migrations/long-example.rb +0 -127
- data/docs/migrations/short-example.rb +0 -40
- data/lib/dynomite/db_config.rb +0 -121
- data/lib/dynomite/errors.rb +0 -15
- data/lib/dynomite/log.rb +0 -15
- data/lib/dynomite/migration/common.rb +0 -86
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
- 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
|