pakyow-data 1.0.0.rc1
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 +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +4 -0
- data/README.md +29 -0
- data/lib/pakyow/data/adapters/abstract.rb +58 -0
- data/lib/pakyow/data/adapters/sql/commands.rb +58 -0
- data/lib/pakyow/data/adapters/sql/dataset_methods.rb +29 -0
- data/lib/pakyow/data/adapters/sql/differ.rb +76 -0
- data/lib/pakyow/data/adapters/sql/migrator/adapter_methods.rb +95 -0
- data/lib/pakyow/data/adapters/sql/migrator.rb +181 -0
- data/lib/pakyow/data/adapters/sql/migrators/automator.rb +49 -0
- data/lib/pakyow/data/adapters/sql/migrators/finalizer.rb +96 -0
- data/lib/pakyow/data/adapters/sql/runner.rb +49 -0
- data/lib/pakyow/data/adapters/sql/source_extension.rb +31 -0
- data/lib/pakyow/data/adapters/sql/types.rb +50 -0
- data/lib/pakyow/data/adapters/sql.rb +247 -0
- data/lib/pakyow/data/behavior/config.rb +28 -0
- data/lib/pakyow/data/behavior/lookup.rb +75 -0
- data/lib/pakyow/data/behavior/serialization.rb +40 -0
- data/lib/pakyow/data/connection.rb +103 -0
- data/lib/pakyow/data/container.rb +273 -0
- data/lib/pakyow/data/errors.rb +169 -0
- data/lib/pakyow/data/framework.rb +42 -0
- data/lib/pakyow/data/helpers.rb +11 -0
- data/lib/pakyow/data/lookup.rb +85 -0
- data/lib/pakyow/data/migrator.rb +182 -0
- data/lib/pakyow/data/object.rb +98 -0
- data/lib/pakyow/data/proxy.rb +262 -0
- data/lib/pakyow/data/result.rb +53 -0
- data/lib/pakyow/data/sources/abstract.rb +82 -0
- data/lib/pakyow/data/sources/ephemeral.rb +72 -0
- data/lib/pakyow/data/sources/relational/association.rb +43 -0
- data/lib/pakyow/data/sources/relational/associations/belongs_to.rb +47 -0
- data/lib/pakyow/data/sources/relational/associations/has_many.rb +54 -0
- data/lib/pakyow/data/sources/relational/associations/has_one.rb +54 -0
- data/lib/pakyow/data/sources/relational/associations/through.rb +67 -0
- data/lib/pakyow/data/sources/relational/command.rb +531 -0
- data/lib/pakyow/data/sources/relational/migrator.rb +101 -0
- data/lib/pakyow/data/sources/relational.rb +587 -0
- data/lib/pakyow/data/subscribers/adapters/memory.rb +153 -0
- data/lib/pakyow/data/subscribers/adapters/redis/pipeliner.rb +45 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/_shared.lua +73 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/expire.lua +16 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/persist.lua +15 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/register.lua +37 -0
- data/lib/pakyow/data/subscribers/adapters/redis.rb +209 -0
- data/lib/pakyow/data/subscribers.rb +148 -0
- data/lib/pakyow/data/tasks/bootstrap.rake +18 -0
- data/lib/pakyow/data/tasks/create.rake +22 -0
- data/lib/pakyow/data/tasks/drop.rake +32 -0
- data/lib/pakyow/data/tasks/finalize.rake +56 -0
- data/lib/pakyow/data/tasks/migrate.rake +24 -0
- data/lib/pakyow/data/tasks/reset.rake +18 -0
- data/lib/pakyow/data/types.rb +37 -0
- data/lib/pakyow/data.rb +27 -0
- data/lib/pakyow/environment/data/auto_migrate.rb +31 -0
- data/lib/pakyow/environment/data/config.rb +54 -0
- data/lib/pakyow/environment/data/connections.rb +76 -0
- data/lib/pakyow/environment/data/memory_db.rb +23 -0
- data/lib/pakyow/validations/unique.rb +26 -0
- metadata +186 -0
@@ -0,0 +1,587 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/makeable"
|
4
|
+
require "pakyow/support/class_state"
|
5
|
+
require "pakyow/support/inflector"
|
6
|
+
|
7
|
+
require "pakyow/data/sources/abstract"
|
8
|
+
|
9
|
+
module Pakyow
|
10
|
+
module Data
|
11
|
+
module Sources
|
12
|
+
# A relational data source through which you interact with a persistence
|
13
|
+
# layer such as a sql database, redis, or http. Defines the schema, queries,
|
14
|
+
# and other adapter-specific metadata (e.g. sql table).
|
15
|
+
#
|
16
|
+
# Each adapter provides its own interface for interacting with the underlying
|
17
|
+
# persistence layer. For example, the sql adapter exposes +Sequel::Dataset+
|
18
|
+
# provided by the *fantastic* Sequel gem.
|
19
|
+
#
|
20
|
+
# In normal use, the underlying dataset is inaccessible from outside of the
|
21
|
+
# source. Instead, access to the dataset occurs through queries defined on
|
22
|
+
# the source that interact with the dataset and return a result.
|
23
|
+
#
|
24
|
+
# Results are always returned as a new source instance (or when used from
|
25
|
+
# the app, a {Pakyow::Data::Proxy} object). Access to the underlying value
|
26
|
+
# is provided through methods such as +one+, +to_a+, and +each+.
|
27
|
+
# (@see Pakyow::Data::Container#wrap_defined_queries!)
|
28
|
+
#
|
29
|
+
# Mutations occur through commands. Commands do not implement validation
|
30
|
+
# other than checking for required attributes and checking that the given
|
31
|
+
# attributes are defined on the source. Use the input verifier pattern to
|
32
|
+
# verify and validate input before passing it to a command
|
33
|
+
# (@see Pakyow::Verifier).
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# source :posts, adapter: :sql, connection: :default do
|
37
|
+
# table :posts
|
38
|
+
#
|
39
|
+
# primary_id
|
40
|
+
# timestamps
|
41
|
+
#
|
42
|
+
# attribute :title, :string
|
43
|
+
#
|
44
|
+
# command :create do |params|
|
45
|
+
# insert(params)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def by_id(id)
|
49
|
+
# where(id: id)
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# data.posts.create(title: "foo")
|
54
|
+
# data.posts.by_id(1).first
|
55
|
+
# => #<Pakyow::Data::Object @values={:id => 1, :title => "foo", :created_at => "2018-11-30 10:55:05 -0800", :updated_at => "2018-11-30 10:55:05 -0800"}>
|
56
|
+
#
|
57
|
+
class Relational < Sources::Abstract
|
58
|
+
require "pakyow/data/sources/relational/associations/belongs_to"
|
59
|
+
require "pakyow/data/sources/relational/associations/has_many"
|
60
|
+
require "pakyow/data/sources/relational/associations/has_one"
|
61
|
+
require "pakyow/data/sources/relational/associations/through"
|
62
|
+
|
63
|
+
require "pakyow/data/sources/relational/command"
|
64
|
+
require "pakyow/data/sources/relational/migrator"
|
65
|
+
|
66
|
+
# @api private
|
67
|
+
attr_reader :included
|
68
|
+
|
69
|
+
def initialize(*)
|
70
|
+
super
|
71
|
+
|
72
|
+
@wrap_as = self.class.singular_name
|
73
|
+
@included = []
|
74
|
+
|
75
|
+
if default_query = self.class.__default_query
|
76
|
+
result = if default_query.is_a?(Proc)
|
77
|
+
instance_exec(&default_query)
|
78
|
+
else
|
79
|
+
public_send(self.class.__default_query)
|
80
|
+
end
|
81
|
+
|
82
|
+
result = case result
|
83
|
+
when self.class
|
84
|
+
result.__getobj__
|
85
|
+
else
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
__setobj__(result)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def including(association_name, &block)
|
94
|
+
tap do
|
95
|
+
association_name = association_name.to_sym
|
96
|
+
|
97
|
+
association_to_include = self.class.associations.values.flatten.find { |association|
|
98
|
+
association.name == association_name
|
99
|
+
} || raise(UnknownAssociation.new("unknown association `#{association_name}'").tap { |error| error.context = self.class })
|
100
|
+
|
101
|
+
included_source = association_to_include.associated_source.instance
|
102
|
+
|
103
|
+
if association_to_include.query
|
104
|
+
included_source = included_source.send(association_to_include.query)
|
105
|
+
end
|
106
|
+
|
107
|
+
final_source = if block_given?
|
108
|
+
included_source.instance_exec(&block) || included_source
|
109
|
+
else
|
110
|
+
included_source
|
111
|
+
end
|
112
|
+
|
113
|
+
@included << [association_to_include, final_source]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def as(object)
|
118
|
+
tap do
|
119
|
+
@wrap_as = object
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def limit(count)
|
124
|
+
__setobj__(__getobj__.limit(count)); self
|
125
|
+
end
|
126
|
+
|
127
|
+
def order(*ordering)
|
128
|
+
__setobj__(
|
129
|
+
__getobj__.order(
|
130
|
+
*ordering.flat_map { |order|
|
131
|
+
case order
|
132
|
+
when Array
|
133
|
+
Sequel.public_send(order[1].to_sym, order[0].to_sym)
|
134
|
+
when Hash
|
135
|
+
order.each_pair.map { |key, value|
|
136
|
+
Sequel.public_send(value.to_sym, key.to_sym)
|
137
|
+
}
|
138
|
+
else
|
139
|
+
Sequel.asc(order.to_s.to_sym)
|
140
|
+
end
|
141
|
+
}
|
142
|
+
)
|
143
|
+
); self
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_a
|
147
|
+
return @results if instance_variable_defined?(:@results)
|
148
|
+
@results = self.class.to_a(__getobj__)
|
149
|
+
include_results!(@results)
|
150
|
+
@results.map! { |result|
|
151
|
+
finalize(result)
|
152
|
+
}
|
153
|
+
end
|
154
|
+
alias all to_a
|
155
|
+
|
156
|
+
def one
|
157
|
+
return @results.first if instance_variable_defined?(:@results)
|
158
|
+
return @result if instance_variable_defined?(:@result)
|
159
|
+
|
160
|
+
if result = self.class.one(__getobj__)
|
161
|
+
include_results!([result])
|
162
|
+
@result = finalize(result)
|
163
|
+
else
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def transaction(&block)
|
169
|
+
self.class.container.connection.transaction(&block)
|
170
|
+
end
|
171
|
+
|
172
|
+
def transaction?
|
173
|
+
self.class.container.connection.adapter.connection.in_transaction?
|
174
|
+
end
|
175
|
+
|
176
|
+
def on_commit(&block)
|
177
|
+
self.class.container.connection.adapter.connection.after_commit(&block)
|
178
|
+
end
|
179
|
+
|
180
|
+
def on_rollback(&block)
|
181
|
+
self.class.container.connection.adapter.connection.after_rollback(&block)
|
182
|
+
end
|
183
|
+
|
184
|
+
def command(command_name)
|
185
|
+
if command = self.class.commands[command_name]
|
186
|
+
Command.new(
|
187
|
+
command_name,
|
188
|
+
block: command[:block],
|
189
|
+
source: self,
|
190
|
+
provides_dataset: command[:provides_dataset],
|
191
|
+
performs_create: command[:performs_create],
|
192
|
+
performs_update: command[:performs_update],
|
193
|
+
performs_delete: command[:performs_delete]
|
194
|
+
)
|
195
|
+
else
|
196
|
+
raise(
|
197
|
+
UnknownCommand.new_with_message(command: command_name).tap do |error|
|
198
|
+
error.context = self.class
|
199
|
+
end
|
200
|
+
)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def count
|
205
|
+
if self.class.respond_to?(:count)
|
206
|
+
self.class.count(__getobj__)
|
207
|
+
else
|
208
|
+
super
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# @api private
|
213
|
+
IVARS_TO_RELOAD = %i(
|
214
|
+
@results @result
|
215
|
+
)
|
216
|
+
|
217
|
+
def reload
|
218
|
+
IVARS_TO_RELOAD.select { |ivar|
|
219
|
+
instance_variable_defined?(ivar)
|
220
|
+
}.each do |ivar|
|
221
|
+
remove_instance_variable(ivar)
|
222
|
+
end
|
223
|
+
|
224
|
+
self
|
225
|
+
end
|
226
|
+
|
227
|
+
def to_json(*)
|
228
|
+
to_a.to_json
|
229
|
+
end
|
230
|
+
|
231
|
+
# @api private
|
232
|
+
def source_name
|
233
|
+
self.class.__object_name.name
|
234
|
+
end
|
235
|
+
|
236
|
+
# @api private
|
237
|
+
def command?(maybe_command_name)
|
238
|
+
self.class.commands.include?(maybe_command_name)
|
239
|
+
end
|
240
|
+
|
241
|
+
# @api private
|
242
|
+
def query?(maybe_query_name)
|
243
|
+
self.class.queries.include?(maybe_query_name)
|
244
|
+
end
|
245
|
+
|
246
|
+
# @api private
|
247
|
+
MODIFIER_METHODS = %i(as including limit order).freeze
|
248
|
+
# @api private
|
249
|
+
def modifier?(maybe_modifier_name)
|
250
|
+
MODIFIER_METHODS.include?(maybe_modifier_name)
|
251
|
+
end
|
252
|
+
|
253
|
+
# @api private
|
254
|
+
NESTED_METHODS = %i(including).freeze
|
255
|
+
# @api private
|
256
|
+
def block_for_nested_source?(maybe_nested_name)
|
257
|
+
NESTED_METHODS.include?(maybe_nested_name)
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def finalize(result)
|
263
|
+
wrap(typecast(result))
|
264
|
+
end
|
265
|
+
|
266
|
+
def typecast(result)
|
267
|
+
result.each do |key, value|
|
268
|
+
unless value.nil? || !self.class.attributes.include?(key)
|
269
|
+
result[key] = self.class.attributes[key][value]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
result
|
274
|
+
end
|
275
|
+
|
276
|
+
def wrap(result)
|
277
|
+
wrapped_result = if @wrap_as.is_a?(Class)
|
278
|
+
@wrap_as.new(result)
|
279
|
+
else
|
280
|
+
self.class.container.object(@wrap_as).new(result)
|
281
|
+
end
|
282
|
+
|
283
|
+
if wrapped_result.is_a?(Object)
|
284
|
+
wrapped_result.originating_source = self.class
|
285
|
+
end
|
286
|
+
|
287
|
+
wrapped_result
|
288
|
+
end
|
289
|
+
|
290
|
+
def include_results!(results)
|
291
|
+
@included.each do |association, combined_source|
|
292
|
+
group_by_key, assign_by_key, remove_keys = if association.type == :through
|
293
|
+
joining_source = association.joining_source.instance
|
294
|
+
|
295
|
+
if combined_source.class == association.joining_source
|
296
|
+
combined_source.__setobj__(
|
297
|
+
combined_source.class.container.connection.adapter.result_for_attribute_value(
|
298
|
+
combined_source.class.container.connection.adapter.qualify_attribute(
|
299
|
+
association.right_foreign_key_field, combined_source
|
300
|
+
),
|
301
|
+
results.map { |result| result[association.associated_query_field] },
|
302
|
+
combined_source
|
303
|
+
)
|
304
|
+
)
|
305
|
+
else
|
306
|
+
aliased = SecureRandom.hex(4).to_sym
|
307
|
+
|
308
|
+
if joining_source.class.container.connection == combined_source.class.container.connection
|
309
|
+
# Optimize with joins.
|
310
|
+
#
|
311
|
+
combined_source.__setobj__(
|
312
|
+
combined_source.class.container.connection.adapter.restrict_to_source(
|
313
|
+
combined_source,
|
314
|
+
combined_source.class.container.connection.adapter.result_for_attribute_value(
|
315
|
+
combined_source.class.container.connection.adapter.qualify_attribute(
|
316
|
+
association.right_foreign_key_field, joining_source
|
317
|
+
),
|
318
|
+
joining_source.class.container.connection.adapter.restrict_to_attribute(
|
319
|
+
association.query_field, source_from_self(__getobj__.dup)
|
320
|
+
),
|
321
|
+
combined_source.class.container.connection.adapter.merge_results(
|
322
|
+
association.left_foreign_key_field,
|
323
|
+
association.associated_source.primary_key_field,
|
324
|
+
joining_source,
|
325
|
+
combined_source
|
326
|
+
)
|
327
|
+
),
|
328
|
+
combined_source.class.container.connection.adapter.alias_attribute(
|
329
|
+
combined_source.class.container.connection.adapter.qualify_attribute(
|
330
|
+
association.right_foreign_key_field, joining_source
|
331
|
+
), aliased
|
332
|
+
)
|
333
|
+
)
|
334
|
+
)
|
335
|
+
else
|
336
|
+
# Manually join.
|
337
|
+
#
|
338
|
+
self_ids = self.class.container.connection.adapter.restrict_to_attribute(
|
339
|
+
self.class.primary_key_field, self
|
340
|
+
).map { |result|
|
341
|
+
result[self.class.primary_key_field]
|
342
|
+
}
|
343
|
+
|
344
|
+
joined_results = joining_source.class.container.connection.adapter.restrict_to_attribute(
|
345
|
+
[association.right_foreign_key_field, association.left_foreign_key_field],
|
346
|
+
joining_source.class.container.connection.adapter.result_for_attribute_value(
|
347
|
+
association.right_foreign_key_field, self_ids, joining_source
|
348
|
+
)
|
349
|
+
)
|
350
|
+
|
351
|
+
combined_results = combined_source.class.container.connection.adapter.result_for_attribute_value(
|
352
|
+
combined_source.class.primary_key_field, joined_results.map { |result| result[association.left_foreign_key_field] }, combined_source
|
353
|
+
)
|
354
|
+
|
355
|
+
combined_results = joined_results.map { |joined_result|
|
356
|
+
combined_results.find { |result|
|
357
|
+
result[combined_source.class.primary_key_field] == joined_result[association.left_foreign_key_field]
|
358
|
+
}.dup.tap do |combined_result|
|
359
|
+
combined_result[aliased] = joined_result[association.right_foreign_key_field]
|
360
|
+
end
|
361
|
+
}
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
[aliased, association.name, [aliased]]
|
366
|
+
else
|
367
|
+
combined_source.__setobj__(
|
368
|
+
combined_source.class.container.connection.adapter.result_for_attribute_value(
|
369
|
+
association.associated_query_field,
|
370
|
+
results.map { |result| result[association.query_field] },
|
371
|
+
combined_source
|
372
|
+
)
|
373
|
+
)
|
374
|
+
|
375
|
+
[association.associated_query_field, association.name, []]
|
376
|
+
end
|
377
|
+
|
378
|
+
# Group the raw results by associated column value.
|
379
|
+
#
|
380
|
+
combined_results = (combined_results || combined_source).to_a.group_by { |combined_result|
|
381
|
+
combined_result[group_by_key]
|
382
|
+
}
|
383
|
+
|
384
|
+
# Add each result group to its associated object.
|
385
|
+
#
|
386
|
+
results.map! { |result|
|
387
|
+
combined_results_for_result = combined_results[result[association.query_field]].to_a.map! { |combined_result|
|
388
|
+
if combined_result.is_a?(Pakyow::Data::Object)
|
389
|
+
combined_result = combined_result.values.dup
|
390
|
+
end
|
391
|
+
|
392
|
+
# Remove any keys, such as temporary values used for grouping.
|
393
|
+
#
|
394
|
+
remove_keys.each do |remove_key|
|
395
|
+
combined_result.delete(remove_key)
|
396
|
+
end
|
397
|
+
|
398
|
+
# Wrap the result into the appropriate data object.
|
399
|
+
#
|
400
|
+
combined_source.send(:wrap, combined_result)
|
401
|
+
}
|
402
|
+
|
403
|
+
result[assign_by_key] = if association.result_type == :one
|
404
|
+
combined_results_for_result[0]
|
405
|
+
else
|
406
|
+
combined_results_for_result
|
407
|
+
end
|
408
|
+
|
409
|
+
result
|
410
|
+
}
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
extend Support::Makeable
|
415
|
+
extend Support::ClassState
|
416
|
+
|
417
|
+
class_state :__default_query
|
418
|
+
class_state :timestamp_fields
|
419
|
+
class_state :primary_key_field
|
420
|
+
class_state :attributes, default: {}
|
421
|
+
class_state :qualifications, default: {}, getter: false
|
422
|
+
class_state :associations, default: { belongs_to: [], has_many: [], has_one: [] }
|
423
|
+
class_state :commands, default: {}
|
424
|
+
|
425
|
+
class << self
|
426
|
+
attr_reader :name, :adapter, :connection
|
427
|
+
|
428
|
+
def command(command_name, provides_dataset: true, performs_create: false, performs_update: false, performs_delete: false, &block)
|
429
|
+
@commands[command_name] = {
|
430
|
+
block: block,
|
431
|
+
provides_dataset: provides_dataset,
|
432
|
+
performs_create: performs_create,
|
433
|
+
performs_update: performs_update,
|
434
|
+
performs_delete: performs_delete
|
435
|
+
}
|
436
|
+
end
|
437
|
+
|
438
|
+
def queries
|
439
|
+
instance_methods - superclass.instance_methods
|
440
|
+
end
|
441
|
+
|
442
|
+
def query(query_name = nil, &block)
|
443
|
+
@__default_query = query_name || block
|
444
|
+
end
|
445
|
+
|
446
|
+
def timestamps(create: :created_at, update: :updated_at)
|
447
|
+
@timestamp_fields = {
|
448
|
+
create: create,
|
449
|
+
update: update
|
450
|
+
}
|
451
|
+
|
452
|
+
attribute create, :datetime
|
453
|
+
attribute update, :datetime
|
454
|
+
end
|
455
|
+
|
456
|
+
def primary_id
|
457
|
+
primary_key :id
|
458
|
+
attribute :id, default_primary_key_type
|
459
|
+
end
|
460
|
+
|
461
|
+
def primary_key(field)
|
462
|
+
@primary_key_field = field
|
463
|
+
end
|
464
|
+
|
465
|
+
def primary_key_type
|
466
|
+
case primary_key_attribute
|
467
|
+
when Hash
|
468
|
+
primary_key_attribute[:type]
|
469
|
+
else
|
470
|
+
primary_key_attribute.meta[:mapping]
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def primary_key_attribute
|
475
|
+
attributes[@primary_key_field]
|
476
|
+
end
|
477
|
+
|
478
|
+
def default_primary_key_type
|
479
|
+
:integer
|
480
|
+
end
|
481
|
+
|
482
|
+
def attribute(name, type = :string, **options)
|
483
|
+
attributes[name.to_sym] = {
|
484
|
+
type: type,
|
485
|
+
options: options
|
486
|
+
}
|
487
|
+
end
|
488
|
+
|
489
|
+
def subscribe(query_name, qualifications)
|
490
|
+
@qualifications[query_name] = qualifications
|
491
|
+
end
|
492
|
+
|
493
|
+
def qualifications(query_name)
|
494
|
+
@qualifications.dig(query_name) || {}
|
495
|
+
end
|
496
|
+
|
497
|
+
def belongs_to(association_name, query: nil, source: association_name)
|
498
|
+
Associations::BelongsTo.new(
|
499
|
+
name: association_name, query: query, source: self, associated_source_name: source
|
500
|
+
).tap do |association|
|
501
|
+
@associations[:belongs_to] << association
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# rubocop:disable Naming/PredicateName
|
506
|
+
def has_many(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise)
|
507
|
+
Associations::HasMany.new(
|
508
|
+
name: association_name, query: query, source: self, associated_source_name: source, as: as, dependent: dependent
|
509
|
+
).tap do |association|
|
510
|
+
@associations[:has_many] << association
|
511
|
+
|
512
|
+
if through
|
513
|
+
setup_as_through(association, through: through)
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
# rubocop:enable Naming/PredicateName
|
518
|
+
|
519
|
+
# rubocop:disable Naming/PredicateName
|
520
|
+
def has_one(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise)
|
521
|
+
Associations::HasOne.new(
|
522
|
+
name: association_name, query: query, source: self, associated_source_name: source, as: as, dependent: dependent
|
523
|
+
).tap do |association|
|
524
|
+
@associations[:has_one] << association
|
525
|
+
|
526
|
+
if through
|
527
|
+
setup_as_through(association, through: through)
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
# rubocop:enable Naming/PredicateName
|
532
|
+
|
533
|
+
def setup_as_through(association, through:)
|
534
|
+
Associations::Through.new(association, joining_source_name: through).tap do |through_association|
|
535
|
+
associations[association.specific_type][
|
536
|
+
associations[association.specific_type].index(association)
|
537
|
+
] = through_association
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
def make(name, adapter: Pakyow.config.data.default_adapter, connection: Pakyow.config.data.default_connection, state: nil, parent: nil, primary_id: true, timestamps: true, **kwargs, &block)
|
542
|
+
super(name, state: state, parent: parent, adapter: adapter, connection: connection, attributes: {}, **kwargs) do
|
543
|
+
adapter_class = Connection.adapter(adapter)
|
544
|
+
|
545
|
+
if adapter_class.const_defined?("SourceExtension")
|
546
|
+
# Extend the source with any adapter-specific behavior.
|
547
|
+
#
|
548
|
+
extension_module = adapter_class.const_get("SourceExtension")
|
549
|
+
unless ancestors.include?(extension_module)
|
550
|
+
include(extension_module)
|
551
|
+
end
|
552
|
+
|
553
|
+
# Define default fields
|
554
|
+
#
|
555
|
+
self.primary_id if primary_id
|
556
|
+
self.timestamps if timestamps
|
557
|
+
end
|
558
|
+
|
559
|
+
# Call the original block.
|
560
|
+
#
|
561
|
+
class_eval(&block) if block_given?
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
# @api private
|
566
|
+
def source_from_source(*)
|
567
|
+
super.tap(&:reload)
|
568
|
+
end
|
569
|
+
|
570
|
+
# @api private
|
571
|
+
def find_association_to_source(source)
|
572
|
+
associations.values.flatten.find { |association|
|
573
|
+
association.associated_source == source.class
|
574
|
+
}
|
575
|
+
end
|
576
|
+
|
577
|
+
# @api private
|
578
|
+
def association_with_name?(name)
|
579
|
+
associations.values.flatten.find { |association|
|
580
|
+
association.name == name
|
581
|
+
}
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|