pakyow-data 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|