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,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/inflector"
|
4
|
+
|
5
|
+
require "pakyow/data/types"
|
6
|
+
|
7
|
+
module Pakyow
|
8
|
+
module Data
|
9
|
+
class Container
|
10
|
+
attr_reader :connection, :sources
|
11
|
+
|
12
|
+
def initialize(connection:, sources:, objects:)
|
13
|
+
@connection, @sources = connection, sources
|
14
|
+
|
15
|
+
@object_map = objects.each_with_object({}) { |object, map|
|
16
|
+
map[object.__object_name.name] = object
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def source(source_name)
|
21
|
+
plural_source_name = Support.inflector.pluralize(source_name).to_sym
|
22
|
+
|
23
|
+
if found_source = sources.find { |source|
|
24
|
+
source.plural_name == plural_source_name
|
25
|
+
}
|
26
|
+
|
27
|
+
found_source.new(
|
28
|
+
@connection.dataset_for_source(found_source)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def object(object_name)
|
34
|
+
@object_map.fetch(object_name, Object)
|
35
|
+
end
|
36
|
+
|
37
|
+
def finalize_associations!(other_containers)
|
38
|
+
@sources.each do |source|
|
39
|
+
discover_has_and_belongs_to!(source, other_containers)
|
40
|
+
end
|
41
|
+
|
42
|
+
@sources.each do |source|
|
43
|
+
set_container_for_source!(source)
|
44
|
+
define_reciprocal_associations!(source, other_containers)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def finalize_sources!(other_containers)
|
49
|
+
@sources.each do |source|
|
50
|
+
mixin_commands!(source)
|
51
|
+
mixin_dataset_methods!(source)
|
52
|
+
define_attributes_for_associations!(source, other_containers)
|
53
|
+
define_queries_for_attributes!(source)
|
54
|
+
wrap_defined_queries!(source)
|
55
|
+
define_methods_for_associations!(source)
|
56
|
+
define_methods_for_objects!(source)
|
57
|
+
finalize_source_types!(source)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def adapter
|
64
|
+
@connection.adapter
|
65
|
+
end
|
66
|
+
|
67
|
+
def discover_has_and_belongs_to!(source, other_containers)
|
68
|
+
source.associations.values.flatten.select { |association|
|
69
|
+
# Only look for has_* associations that aren't already setup through another source.
|
70
|
+
#
|
71
|
+
association.type == :has
|
72
|
+
}.each do |association|
|
73
|
+
reciprocal_association = nil
|
74
|
+
reciprocal_source = (@sources + other_containers.flat_map(&:sources)).reject { |potentially_reciprocal_source|
|
75
|
+
potentially_reciprocal_source == source
|
76
|
+
}.find { |potentially_reciprocal_source|
|
77
|
+
reciprocal_association = potentially_reciprocal_source.associations.values.flatten.find { |potentially_reciprocal_association|
|
78
|
+
potentially_reciprocal_association.specific_type == association.specific_type &&
|
79
|
+
potentially_reciprocal_association.associated_source_name == source.plural_name &&
|
80
|
+
Support.inflector.pluralize(potentially_reciprocal_association.name) == Support.inflector.pluralize(association.associated_name) &&
|
81
|
+
Support.inflector.pluralize(potentially_reciprocal_association.associated_name) == Support.inflector.pluralize(association.name)
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
if reciprocal_source
|
86
|
+
joining_source_name = [source.plural_name, reciprocal_source.plural_name].sort.join("_")
|
87
|
+
joining_source = (@sources + other_containers.flat_map(&:sources)).find { |potentially_joining_source|
|
88
|
+
potentially_joining_source.plural_name == joining_source_name
|
89
|
+
}
|
90
|
+
|
91
|
+
unless joining_source
|
92
|
+
joining_source = source.ancestors.find { |ancestor|
|
93
|
+
ancestor != source && ancestor.ancestors.include?(Sources::Abstract)
|
94
|
+
}.make(
|
95
|
+
joining_source_name,
|
96
|
+
adapter: source.adapter,
|
97
|
+
connection: source.connection,
|
98
|
+
within: source.__object_name.namespace
|
99
|
+
)
|
100
|
+
|
101
|
+
@sources << joining_source
|
102
|
+
end
|
103
|
+
|
104
|
+
# Modify both sides of the association to be through the joining source.
|
105
|
+
#
|
106
|
+
source.setup_as_through(association, through: joining_source_name).internal!
|
107
|
+
reciprocal_source.setup_as_through(reciprocal_association, through: joining_source_name).internal!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_container_for_source!(source)
|
113
|
+
source.container = self
|
114
|
+
end
|
115
|
+
|
116
|
+
def mixin_commands!(source)
|
117
|
+
source.include adapter.class.const_get("Commands")
|
118
|
+
end
|
119
|
+
|
120
|
+
def mixin_dataset_methods!(source)
|
121
|
+
source.extend adapter.class.const_get("DatasetMethods")
|
122
|
+
end
|
123
|
+
|
124
|
+
def define_attributes_for_associations!(source, other_containers)
|
125
|
+
source.associations.values.flatten.each do |association|
|
126
|
+
associated_source = (@sources + other_containers.flat_map(&:sources)).find { |potentially_associated_source|
|
127
|
+
potentially_associated_source.plural_name == association.associated_source_name
|
128
|
+
}
|
129
|
+
|
130
|
+
if associated_source
|
131
|
+
association.associated_source = associated_source
|
132
|
+
|
133
|
+
if association.type == :through
|
134
|
+
association.joining_source = (@sources + other_containers.flat_map(&:sources)).find { |potentially_joining_source|
|
135
|
+
potentially_joining_source.plural_name == association.joining_source_name
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
if association.type == :belongs
|
140
|
+
# Define an attribute for the foreign key.
|
141
|
+
#
|
142
|
+
source.attribute(
|
143
|
+
association.foreign_key_field,
|
144
|
+
association.foreign_key_type,
|
145
|
+
foreign_key: association.associated_source_name
|
146
|
+
)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def define_reciprocal_associations!(source, other_containers)
|
153
|
+
(source.associations[:has_many] + source.associations[:has_one]).each do |association|
|
154
|
+
associated_source = (@sources + other_containers.flat_map(&:sources)).find { |potentially_associated_source|
|
155
|
+
potentially_associated_source.plural_name == association.associated_source_name
|
156
|
+
}
|
157
|
+
|
158
|
+
if associated_source
|
159
|
+
if association.type == :through
|
160
|
+
joining_source = (@sources + other_containers.flat_map(&:sources)).find { |potential_joining_source|
|
161
|
+
potential_joining_source.plural_name == association.joining_source_name
|
162
|
+
}
|
163
|
+
|
164
|
+
if joining_source
|
165
|
+
unless joining_source.associations[:belongs_to].any? { |current_association| current_association.name == association.left_name }
|
166
|
+
joining_source.belongs_to(association.left_name, source: associated_source.plural_name)
|
167
|
+
end
|
168
|
+
|
169
|
+
unless joining_source.associations[:belongs_to].any? { |current_association| current_association.name == association.right_name }
|
170
|
+
joining_source.belongs_to(association.right_name, source: source.plural_name)
|
171
|
+
end
|
172
|
+
|
173
|
+
unless association.internal?
|
174
|
+
unless associated_source.associations[association.specific_type].any? { |current_association| current_association.joining_source_name == association.joining_source_name }
|
175
|
+
associated_source.send(association.specific_type, association.associated_name, source: source.plural_name, as: association.left_name, through: association.joining_source_name, dependent: association.dependent)
|
176
|
+
end
|
177
|
+
|
178
|
+
unless source.associations[association.specific_type].any? { |current_association| current_association.associated_source_name == association.joining_source_name }
|
179
|
+
source.send(association.specific_type, association.joining_source_name, source: joining_source.plural_name, as: Support.inflector.singularize(association.associated_name), dependent: association.dependent)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
else
|
184
|
+
unless associated_source.associations[:belongs_to].any? { |current_association| current_association.name == Support.inflector.singularize(association.associated_name).to_sym }
|
185
|
+
associated_source.belongs_to(association.associated_name, source: source.plural_name)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def define_queries_for_attributes!(source)
|
193
|
+
source.attributes.keys.each do |attribute|
|
194
|
+
method_name = :"by_#{attribute}"
|
195
|
+
unless source.instance_methods.include?(method_name)
|
196
|
+
source.class_eval do
|
197
|
+
define_method method_name do |value|
|
198
|
+
self.class.container.connection.adapter.result_for_attribute_value(attribute, value, self)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Qualify the query.
|
202
|
+
#
|
203
|
+
subscribe :"by_#{attribute}", attribute => :__arg0__
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def define_methods_for_associations!(source)
|
210
|
+
source.associations.values.flatten.each do |association|
|
211
|
+
method_name = :"with_#{association.name}"
|
212
|
+
unless source.instance_methods.include?(method_name)
|
213
|
+
source.class_eval do
|
214
|
+
define_method method_name do
|
215
|
+
including(association.name)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def define_methods_for_objects!(source)
|
223
|
+
@object_map.keys.each do |object_name|
|
224
|
+
method_name = :"as_#{object_name}"
|
225
|
+
unless source.instance_methods.include?(method_name)
|
226
|
+
source.class_eval do
|
227
|
+
define_method method_name do
|
228
|
+
as(object_name)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Override queries with methods that update the source with a new dataset.
|
236
|
+
#
|
237
|
+
def wrap_defined_queries!(source)
|
238
|
+
local_queries = source.queries
|
239
|
+
source.prepend(
|
240
|
+
Module.new do
|
241
|
+
local_queries.each do |query|
|
242
|
+
define_method query do |*args, &block|
|
243
|
+
tap do
|
244
|
+
result = super(*args, &block)
|
245
|
+
case result
|
246
|
+
when self.class
|
247
|
+
result
|
248
|
+
else
|
249
|
+
__setobj__(result)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
258
|
+
def finalize_source_types!(source)
|
259
|
+
source.attributes.each do |attribute_name, attribute_info|
|
260
|
+
if attribute_info.is_a?(Hash)
|
261
|
+
type = Types.type_for(attribute_info[:type], connection.types)
|
262
|
+
|
263
|
+
if attribute_name == source.primary_key_field
|
264
|
+
type = type.meta(primary_key: true)
|
265
|
+
end
|
266
|
+
|
267
|
+
source.attributes[attribute_name] = type.meta(attribute_info[:options])
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/error"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Data
|
7
|
+
class Error < Pakyow::Error
|
8
|
+
end
|
9
|
+
|
10
|
+
class ConnectionError < Error
|
11
|
+
def contextual_message
|
12
|
+
String.new(
|
13
|
+
<<~MESSAGE
|
14
|
+
Connection for #{@context.type}.#{@context.name} could not be established.
|
15
|
+
MESSAGE
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ConstraintViolation < Error
|
21
|
+
class_state :messages, default: {
|
22
|
+
associate_many_missing: "can't associate results as `{association}' because at least one value could not be found",
|
23
|
+
associate_missing: "can't find associated {source} with {field} of `{value}'",
|
24
|
+
associate_multiple: "can't associate multiple results as `{association}'",
|
25
|
+
dependent_delete: "can't delete {source} because of {count} dependent {dependent}"
|
26
|
+
}.freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
class MissingAdapter < Error
|
30
|
+
end
|
31
|
+
|
32
|
+
class NotNullViolation < Error
|
33
|
+
class_state :messages, default: {
|
34
|
+
default: "`{attribute}' is a required attribute"
|
35
|
+
}.freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
class QueryError < Error
|
39
|
+
end
|
40
|
+
|
41
|
+
class Rollback < Error
|
42
|
+
end
|
43
|
+
|
44
|
+
class TypeMismatch < Error
|
45
|
+
class_state :messages, default: {
|
46
|
+
default: "can't convert `{type}' into {mapping}",
|
47
|
+
associate_many_not_object: "can't associate results as `{association}' because at least one value is not a Pakyow::Data::Object",
|
48
|
+
associate_many_wrong_source: "can't associate results as `{association}' because at least one value did not originate from {source}",
|
49
|
+
associate_unknown_object: "can't associate an object with an unknown source as `{association}'",
|
50
|
+
associate_wrong_object: "can't associate an object from {source} as `{association}'",
|
51
|
+
associate_wrong_source: "can't associate {source} as `{association}'",
|
52
|
+
associate_wrong_type: "can't associate {type} as `{association}'"
|
53
|
+
}.freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
class UniqueViolation < Error
|
57
|
+
end
|
58
|
+
|
59
|
+
class UnknownAdapter < Error
|
60
|
+
class_state :messages, default: {
|
61
|
+
default: "`{type}' is not a known adapter"
|
62
|
+
}.freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
class UnknownAttribute < Error
|
66
|
+
class_state :messages, default: {
|
67
|
+
default: "`{attribute}' is not a known attribute for {source}"
|
68
|
+
}.freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
class UnknownAssociation < Error
|
72
|
+
def contextual_message
|
73
|
+
if associations.any?
|
74
|
+
String.new(
|
75
|
+
<<~MESSAGE
|
76
|
+
The following associations exist for #{@context.__object_name.name}:
|
77
|
+
MESSAGE
|
78
|
+
).tap do |message|
|
79
|
+
associations.each do |association|
|
80
|
+
message << " * #{association.name}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
else
|
84
|
+
String.new(
|
85
|
+
<<~MESSAGE
|
86
|
+
No associations exist for #{@context.__object_name.name}.
|
87
|
+
MESSAGE
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def associations
|
95
|
+
@context.associations.values.flatten
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class UnknownCommand < Error
|
100
|
+
class_state :messages, default: {
|
101
|
+
default: "`{command}' is not a known command"
|
102
|
+
}.freeze
|
103
|
+
|
104
|
+
def contextual_message
|
105
|
+
if commands.any?
|
106
|
+
String.new(
|
107
|
+
<<~MESSAGE
|
108
|
+
The following commands are defined for #{@context.__object_name.name}:
|
109
|
+
MESSAGE
|
110
|
+
).tap do |message|
|
111
|
+
commands.keys.each do |command|
|
112
|
+
message << " * #{command}\n"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
else
|
116
|
+
String.new(
|
117
|
+
<<~MESSAGE
|
118
|
+
No commands are defined for #{@context.__object_name.name}.
|
119
|
+
MESSAGE
|
120
|
+
)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def commands
|
127
|
+
@context.commands
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class UnknownSource < Error
|
132
|
+
class_state :messages, default: {
|
133
|
+
default: "unknown source `{association_source}' for association: {source} {association_type} {association_name}"
|
134
|
+
}.freeze
|
135
|
+
|
136
|
+
def contextual_message
|
137
|
+
if sources.any?
|
138
|
+
String.new(
|
139
|
+
<<~MESSAGE
|
140
|
+
The following sources are defined:
|
141
|
+
MESSAGE
|
142
|
+
).tap do |message|
|
143
|
+
sources.keys.each do |source|
|
144
|
+
message << " * #{source}\n"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
else
|
148
|
+
String.new(
|
149
|
+
<<~MESSAGE
|
150
|
+
No sources are defined.
|
151
|
+
MESSAGE
|
152
|
+
)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def sources
|
159
|
+
@context.sources
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class UnknownSubscriberAdapter < Error
|
164
|
+
class_state :messages, default: {
|
165
|
+
default: "Failed to load subscriber adapter named `{adapter}'"
|
166
|
+
}.freeze
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/framework"
|
4
|
+
|
5
|
+
require "pakyow/data/object"
|
6
|
+
require "pakyow/data/helpers"
|
7
|
+
|
8
|
+
require "pakyow/data/behavior/config"
|
9
|
+
require "pakyow/data/behavior/lookup"
|
10
|
+
require "pakyow/data/behavior/serialization"
|
11
|
+
|
12
|
+
require "pakyow/data/sources/relational"
|
13
|
+
|
14
|
+
module Pakyow
|
15
|
+
module Data
|
16
|
+
class Framework < Pakyow::Framework(:data)
|
17
|
+
def boot
|
18
|
+
object.class_eval do
|
19
|
+
isolate Sources::Relational
|
20
|
+
isolate Object
|
21
|
+
|
22
|
+
stateful :source, isolated(:Relational)
|
23
|
+
stateful :object, isolated(:Object)
|
24
|
+
|
25
|
+
# Autoload sources from the `sources` directory.
|
26
|
+
#
|
27
|
+
aspect :sources
|
28
|
+
|
29
|
+
# Autoload objects from the `objects` directory.
|
30
|
+
#
|
31
|
+
aspect :objects
|
32
|
+
|
33
|
+
register_helper :active, Helpers
|
34
|
+
|
35
|
+
include Behavior::Config
|
36
|
+
include Behavior::Lookup
|
37
|
+
include Behavior::Serialization
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/data/errors"
|
4
|
+
require "pakyow/data/proxy"
|
5
|
+
require "pakyow/data/sources/ephemeral"
|
6
|
+
|
7
|
+
module Pakyow
|
8
|
+
module Data
|
9
|
+
class Lookup
|
10
|
+
# @api private
|
11
|
+
attr_reader :subscribers, :sources, :containers
|
12
|
+
|
13
|
+
def initialize(containers:, subscribers:, app:)
|
14
|
+
@subscribers = subscribers
|
15
|
+
@subscribers.lookup = self
|
16
|
+
@app = app
|
17
|
+
|
18
|
+
@sources = {}
|
19
|
+
@containers = containers
|
20
|
+
@containers.each do |container|
|
21
|
+
container.sources.each do |source|
|
22
|
+
@sources[source.__object_name.name] = source
|
23
|
+
define_singleton_method source.__object_name.name do
|
24
|
+
Proxy.new(
|
25
|
+
container.source(
|
26
|
+
source.__object_name.name
|
27
|
+
),
|
28
|
+
|
29
|
+
@subscribers, @app
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
validate!
|
36
|
+
end
|
37
|
+
|
38
|
+
def ephemeral(type, **qualifications)
|
39
|
+
Proxy.new(
|
40
|
+
Sources::Ephemeral.new(type, **qualifications),
|
41
|
+
@subscribers, @app
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def unsubscribe(subscriber)
|
46
|
+
@subscribers.unsubscribe(subscriber)
|
47
|
+
end
|
48
|
+
|
49
|
+
def expire(subscriber, seconds)
|
50
|
+
@subscribers.expire(subscriber, seconds)
|
51
|
+
end
|
52
|
+
|
53
|
+
def persist(subscriber)
|
54
|
+
@subscribers.persist(subscriber)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def validate!
|
60
|
+
validate_associated_sources!
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_associated_sources!
|
64
|
+
@sources.values.each do |source|
|
65
|
+
source.associations.values.flatten.each do |association|
|
66
|
+
association.dependent_source_names.compact.each do |source_name|
|
67
|
+
unless @sources.key?(source_name)
|
68
|
+
raise(
|
69
|
+
UnknownSource.new_with_message(
|
70
|
+
source: source.__object_name.name,
|
71
|
+
association_source: source_name,
|
72
|
+
association_type: association.specific_type,
|
73
|
+
association_name: association.name
|
74
|
+
).tap do |error|
|
75
|
+
error.context = self
|
76
|
+
end
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|