datasource 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +88 -19
- data/lib/datasource/adapters/active_record.rb +51 -17
- data/lib/datasource/adapters/sequel.rb +47 -13
- data/lib/datasource/attributes/computed_attribute.rb +8 -1
- data/lib/datasource/attributes/loader.rb +17 -0
- data/lib/datasource/base.rb +84 -37
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89a34ecb59d8f4183295c43b4badd33aca5f5e62
|
4
|
+
data.tar.gz: b408320273ce49236bd022d7db00c1fce06bdfd5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0db668df6f4ae71223655a544d59b805e6bdd2694ab540f37dacc7962700d427799f00308d0cdf03a180a7e45f7d3891ff795e73def10c180bf9df5d92942df3
|
7
|
+
data.tar.gz: c69ae3fe12467516c7f24d090e7240a398e9e7d341dd82a1c54b459c57dd4fb2f9c796a6180a85e02fc304ea45bbf96361579af3249974df4fd73cc0a02dd997
|
data/README.md
CHANGED
@@ -18,12 +18,12 @@ Run install generator:
|
|
18
18
|
rails g datasource:install
|
19
19
|
```
|
20
20
|
|
21
|
-
|
21
|
+
#### ORM support
|
22
22
|
|
23
23
|
- ActiveRecord
|
24
24
|
- Sequel
|
25
25
|
|
26
|
-
|
26
|
+
#### Serializer support
|
27
27
|
|
28
28
|
- active_model_serializers
|
29
29
|
|
@@ -66,18 +66,16 @@ SELECT id, title, user_id FROM posts WHERE id IN (?)
|
|
66
66
|
```
|
67
67
|
|
68
68
|
### Model Methods / Virtual Attributes
|
69
|
-
You need to
|
70
|
-
The method itself can be either in the serializer or in your model, it doesn't matter.
|
71
|
-
|
72
|
-
You can list multiple dependency columns.
|
69
|
+
You need to use `computed` in a `datasource_module` block to specify what a method depends on. It can depend on database columns, other computed attributes or loaders.
|
73
70
|
|
74
71
|
```ruby
|
75
72
|
class User < ActiveRecord::Base
|
76
73
|
datasource_module do
|
77
74
|
computed :first_name_initial, :first_name
|
78
|
-
computed :
|
75
|
+
computed :both_initials, :first_name, :last_name
|
79
76
|
end
|
80
77
|
|
78
|
+
# method can be in model
|
81
79
|
def first_name_initial
|
82
80
|
first_name[0].upcase
|
83
81
|
end
|
@@ -86,8 +84,9 @@ end
|
|
86
84
|
class UserSerializer < ActiveModel::Serializer
|
87
85
|
attributes :first_name_initial, :last_name_initial
|
88
86
|
|
89
|
-
|
90
|
-
|
87
|
+
# method can also be in serializer
|
88
|
+
def both_initials
|
89
|
+
object.last_name[0].upcase + object.last_name[0].upcase
|
91
90
|
end
|
92
91
|
end
|
93
92
|
```
|
@@ -100,15 +99,15 @@ You will be reminded with an exception if you forget to do this.
|
|
100
99
|
|
101
100
|
### Show action
|
102
101
|
|
103
|
-
You will probably want to reuse the same preloading
|
104
|
-
You
|
105
|
-
the serializer class as an argument.
|
102
|
+
You will probably want to reuse the same preloading logic in your show action.
|
103
|
+
You will need to call `for_serializer` on the scope before you call `find`.
|
104
|
+
You can optionally give it the serializer class as an argument.
|
106
105
|
|
107
106
|
```ruby
|
108
|
-
class
|
107
|
+
class PostsController < ApplicationController
|
109
108
|
def show
|
110
109
|
post = Post.for_serializer.find(params[:id])
|
111
|
-
#
|
110
|
+
# more explicit:
|
112
111
|
# post = Post.for_serializer(PostSerializer).find(params[:id])
|
113
112
|
|
114
113
|
render json: post
|
@@ -116,9 +115,22 @@ class UsersController < ApplicationController
|
|
116
115
|
end
|
117
116
|
```
|
118
117
|
|
118
|
+
You can also use it on an existing record, but doing it this way may result in
|
119
|
+
an additional SQL query (for example if you use query attributes).
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class UsersController < ApplicationController
|
123
|
+
def show
|
124
|
+
user = current_user.for_serializer
|
125
|
+
|
126
|
+
render json: user
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
119
131
|
## Advanced Usage
|
120
132
|
|
121
|
-
### Query
|
133
|
+
### Query attribute
|
122
134
|
|
123
135
|
You can specify a SQL fragment for `SELECT` and use that as an attribute on your
|
124
136
|
model. As a simple example you can concatenate 2 strings together in SQL:
|
@@ -141,11 +153,13 @@ end
|
|
141
153
|
SELECT users.id, (users.first_name || ' ' || users.last_name) AS full_name FROM users
|
142
154
|
```
|
143
155
|
|
144
|
-
|
156
|
+
Note: If you need data from another table, use a join in a loader (see below).
|
157
|
+
|
158
|
+
### Loader
|
145
159
|
|
146
160
|
You might want to have some more complex preloading logic. In that case you can use a loader.
|
147
|
-
|
148
|
-
The key of the hash must be the id of the record for which the
|
161
|
+
A loader will receive ids of the records, and needs to return a hash.
|
162
|
+
The key of the hash must be the id of the record for which the value is.
|
149
163
|
|
150
164
|
A loader will only be executed if a computed attribute depends on it. If an attribute depends
|
151
165
|
on multiple loaders, pass an array of loaders like so `computed :attr, loaders: [:loader1, :loader2]`.
|
@@ -156,7 +170,7 @@ will be nil. However you can use the `default` option for such cases (see below
|
|
156
170
|
```ruby
|
157
171
|
class User < ActiveRecord::Base
|
158
172
|
datasource_module do
|
159
|
-
computed :post_count,
|
173
|
+
computed :post_count, loader: :post_counts
|
160
174
|
loader :post_counts, array_to_hash: true, default: 0 do |user_ids|
|
161
175
|
results = Post
|
162
176
|
.where(user_id: user_ids)
|
@@ -210,3 +224,58 @@ loader :stuff, group_by: "user_id", one: true do |ids|
|
|
210
224
|
# { 10 => { "title" => "Something", "user_id" => 10 } }
|
211
225
|
end
|
212
226
|
```
|
227
|
+
|
228
|
+
### Loaded
|
229
|
+
|
230
|
+
Loaded is the same as loader, but it also creates a computed attribute and defines
|
231
|
+
a method with the same name on your model.
|
232
|
+
|
233
|
+
Here is the previous example with `loaded` instead of `loader`:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
class User < ActiveRecord::Base
|
237
|
+
datasource_module do
|
238
|
+
loaded :post_count, array_to_hash: true, default: 0 do |user_ids|
|
239
|
+
results = Post
|
240
|
+
.where(user_id: user_ids)
|
241
|
+
.group(:user_id)
|
242
|
+
.pluck("user_id, COUNT(id)")
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class UserSerializer < ActiveModel::Serializer
|
248
|
+
attributes :id, :post_count
|
249
|
+
# Note that the User now has a generated post_count method
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
When using `loaded`, if you already have the method with this name defined in your
|
254
|
+
model, datasource will automatically create a 'wrapper' method that will use the
|
255
|
+
loaded value if available (when you are using a serializer/datasource), otherwise
|
256
|
+
it will fallback to your original method. This way you can still use the same
|
257
|
+
method when you are not using a serializer/datasource. For example:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
class User < ActiveRecord::Base
|
261
|
+
datasource_module do
|
262
|
+
loaded :post_count, array_to_hash: true, default: 0 do |user_ids|
|
263
|
+
results = Post
|
264
|
+
.where(user_id: user_ids)
|
265
|
+
.group(:user_id)
|
266
|
+
.pluck("user_id, COUNT(id)")
|
267
|
+
end
|
268
|
+
|
269
|
+
def post_count
|
270
|
+
posts.count
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class UserSerializer < ActiveModel::Serializer
|
276
|
+
attributes :id, :post_count # <- post_count will be read from loaded_values
|
277
|
+
end
|
278
|
+
|
279
|
+
User.first.post_count # <- your method will be called
|
280
|
+
|
281
|
+
```
|
@@ -20,17 +20,22 @@ module Datasource
|
|
20
20
|
self
|
21
21
|
end
|
22
22
|
|
23
|
+
def get_datasource
|
24
|
+
datasource = @datasource.new(self)
|
25
|
+
datasource.select(*Array(@datasource_select))
|
26
|
+
if @datasource_serializer
|
27
|
+
select = []
|
28
|
+
Datasource::Base.consumer_adapter.to_datasource_select(select, @datasource.orm_klass, @datasource_serializer, nil, datasource.adapter)
|
29
|
+
|
30
|
+
datasource.select(*select)
|
31
|
+
end
|
32
|
+
datasource
|
33
|
+
end
|
34
|
+
|
23
35
|
private
|
24
36
|
def exec_queries
|
25
37
|
if @datasource
|
26
|
-
datasource =
|
27
|
-
datasource.select(*Array(@datasource_select))
|
28
|
-
if @datasource_serializer
|
29
|
-
select = []
|
30
|
-
Datasource::Base.consumer_adapter.to_datasource_select(select, @datasource.orm_klass, @datasource_serializer, nil, datasource.adapter)
|
31
|
-
|
32
|
-
datasource.select(*select)
|
33
|
-
end
|
38
|
+
datasource = get_datasource
|
34
39
|
|
35
40
|
@loaded = true
|
36
41
|
@records = datasource.results
|
@@ -47,6 +52,22 @@ module Datasource
|
|
47
52
|
attr_accessor :loaded_values
|
48
53
|
end
|
49
54
|
|
55
|
+
def for_serializer(serializer = nil)
|
56
|
+
datasource_class = self.class.default_datasource
|
57
|
+
pk = datasource_class.primary_key.to_sym
|
58
|
+
|
59
|
+
scope = self.class
|
60
|
+
.with_datasource(datasource_class)
|
61
|
+
.for_serializer(serializer).where(pk => send(pk))
|
62
|
+
|
63
|
+
datasource = scope.get_datasource
|
64
|
+
if datasource.can_upgrade?(self)
|
65
|
+
datasource.upgrade_records(self).first
|
66
|
+
else
|
67
|
+
scope.first
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
50
71
|
module ClassMethods
|
51
72
|
def for_serializer(serializer = nil)
|
52
73
|
scope = if all.respond_to?(:use_datasource_serializer)
|
@@ -67,7 +88,7 @@ module Datasource
|
|
67
88
|
end
|
68
89
|
|
69
90
|
def default_datasource
|
70
|
-
@default_datasource ||=
|
91
|
+
@default_datasource ||= Datasource::From(self)
|
71
92
|
end
|
72
93
|
|
73
94
|
def datasource_module(&block)
|
@@ -103,6 +124,10 @@ module Datasource
|
|
103
124
|
scope.loaded?
|
104
125
|
end
|
105
126
|
|
127
|
+
def has_attribute?(record, name)
|
128
|
+
record.attributes.key?(name.to_s)
|
129
|
+
end
|
130
|
+
|
106
131
|
def association_klass(reflection)
|
107
132
|
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
|
108
133
|
fail Datasource::Error, "polymorphic belongs_to not supported, write custom loader"
|
@@ -111,7 +136,7 @@ module Datasource
|
|
111
136
|
end
|
112
137
|
end
|
113
138
|
|
114
|
-
def
|
139
|
+
def load_association(records, name)
|
115
140
|
return if records.empty?
|
116
141
|
return if records.first.association(name.to_sym).loaded?
|
117
142
|
klass = records.first.class
|
@@ -125,7 +150,7 @@ module Datasource
|
|
125
150
|
scope = assoc_class.all
|
126
151
|
datasource = datasource_class.new(scope)
|
127
152
|
datasource_select = serializer_class._attributes.dup
|
128
|
-
Datasource::Base.reflection_select(
|
153
|
+
Datasource::Base.reflection_select(association_reflection(klass, name.to_sym), [], datasource_select)
|
129
154
|
datasource.select(*datasource_select)
|
130
155
|
select_values = datasource.get_select_values
|
131
156
|
|
@@ -139,7 +164,7 @@ module Datasource
|
|
139
164
|
|
140
165
|
assoc_records = records.flat_map { |record| record.send(name) }.compact
|
141
166
|
serializer_class._associations.each_pair do |assoc_name, options|
|
142
|
-
|
167
|
+
load_association(assoc_records, assoc_name)
|
143
168
|
end
|
144
169
|
datasource.results(assoc_records)
|
145
170
|
end
|
@@ -161,10 +186,21 @@ module Datasource
|
|
161
186
|
ds.scope.select(*ds.get_select_values)
|
162
187
|
end
|
163
188
|
|
189
|
+
def upgrade_records(ds, records)
|
190
|
+
load_associations(ds, records)
|
191
|
+
ds.results(records)
|
192
|
+
end
|
193
|
+
|
194
|
+
def load_associations(ds, records)
|
195
|
+
ds.expose_associations.each_pair do |assoc_name, assoc_select|
|
196
|
+
load_association(records, assoc_name)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
164
200
|
def get_rows(ds)
|
165
201
|
append_select = []
|
166
202
|
ds.expose_associations.each_pair do |assoc_name, assoc_select|
|
167
|
-
if reflection =
|
203
|
+
if reflection = association_reflection(ds.class.orm_klass, assoc_name.to_sym)
|
168
204
|
Datasource::Base.reflection_select(reflection, append_select, [])
|
169
205
|
end
|
170
206
|
end
|
@@ -176,9 +212,7 @@ module Datasource
|
|
176
212
|
end
|
177
213
|
scope.includes_values = []
|
178
214
|
scope.to_a.tap do |records|
|
179
|
-
ds
|
180
|
-
Adapters::ActiveRecord.preload_association(records, assoc_name)
|
181
|
-
end
|
215
|
+
load_associations(ds, records)
|
182
216
|
end
|
183
217
|
end
|
184
218
|
|
@@ -214,7 +248,7 @@ module Datasource
|
|
214
248
|
Datasource::Adapters::ActiveRecord
|
215
249
|
end
|
216
250
|
|
217
|
-
|
251
|
+
define_singleton_method(:primary_key) do
|
218
252
|
klass.primary_key.to_sym
|
219
253
|
end
|
220
254
|
end
|
@@ -14,6 +14,18 @@ module Datasource
|
|
14
14
|
self
|
15
15
|
end
|
16
16
|
|
17
|
+
def get_datasource
|
18
|
+
datasource = @datasource.new(self)
|
19
|
+
datasource.select(*Array(@datasource_select))
|
20
|
+
if @datasource_serializer
|
21
|
+
select = []
|
22
|
+
Datasource::Base.consumer_adapter.to_datasource_select(select, @datasource.orm_klass, @datasource_serializer, nil, datasource.adapter)
|
23
|
+
|
24
|
+
datasource.select(*select)
|
25
|
+
end
|
26
|
+
datasource
|
27
|
+
end
|
28
|
+
|
17
29
|
def datasource_select(*args)
|
18
30
|
@datasource_select = Array(@datasource_select) + args
|
19
31
|
self
|
@@ -21,14 +33,7 @@ module Datasource
|
|
21
33
|
|
22
34
|
def each(&block)
|
23
35
|
if @datasource
|
24
|
-
datasource =
|
25
|
-
datasource.select(*Array(@datasource_select))
|
26
|
-
if @datasource_serializer
|
27
|
-
select = []
|
28
|
-
Datasource::Base.consumer_adapter.to_datasource_select(select, @datasource.orm_klass, @datasource_serializer, nil, datasource.adapter)
|
29
|
-
|
30
|
-
datasource.select(*select)
|
31
|
-
end
|
36
|
+
datasource = get_datasource
|
32
37
|
|
33
38
|
datasource.results.each(&block)
|
34
39
|
else
|
@@ -64,9 +69,25 @@ module Datasource
|
|
64
69
|
end
|
65
70
|
end
|
66
71
|
|
72
|
+
def for_serializer(serializer = nil)
|
73
|
+
datasource_class = self.class.default_datasource
|
74
|
+
pk = datasource_class.primary_key.to_sym
|
75
|
+
|
76
|
+
scope = self.class
|
77
|
+
.with_datasource(datasource_class)
|
78
|
+
.for_serializer(serializer).where(pk => send(pk))
|
79
|
+
|
80
|
+
datasource = scope.get_datasource
|
81
|
+
if datasource.can_upgrade?(self)
|
82
|
+
datasource.upgrade_records(self).first
|
83
|
+
else
|
84
|
+
scope.first
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
67
88
|
module ClassMethods
|
68
89
|
def default_datasource
|
69
|
-
@default_datasource ||=
|
90
|
+
@default_datasource ||= Datasource::From(self)
|
70
91
|
end
|
71
92
|
|
72
93
|
def datasource_module(&block)
|
@@ -113,8 +134,12 @@ module Datasource
|
|
113
134
|
false
|
114
135
|
end
|
115
136
|
|
137
|
+
def has_attribute?(record, name)
|
138
|
+
record.values.key?(name.to_sym)
|
139
|
+
end
|
140
|
+
|
116
141
|
def get_assoc_eager_options(klass, name, assoc_select, append_select)
|
117
|
-
if reflection =
|
142
|
+
if reflection = association_reflection(klass, name)
|
118
143
|
self_append_select = []
|
119
144
|
Datasource::Base.reflection_select(reflection, append_select, self_append_select)
|
120
145
|
assoc_class = reflection[:klass]
|
@@ -144,7 +169,12 @@ module Datasource
|
|
144
169
|
ds.scope.select(*get_sequel_select_values(ds.get_select_values))
|
145
170
|
end
|
146
171
|
|
147
|
-
def
|
172
|
+
def upgrade_records(ds, records)
|
173
|
+
get_final_scope(ds).send :post_load, records
|
174
|
+
ds.results(records)
|
175
|
+
end
|
176
|
+
|
177
|
+
def get_final_scope(ds)
|
148
178
|
eager = {}
|
149
179
|
append_select = []
|
150
180
|
ds.expose_associations.each_pair do |assoc_name, assoc_select|
|
@@ -158,7 +188,11 @@ module Datasource
|
|
158
188
|
end
|
159
189
|
scope
|
160
190
|
.select_append(*get_sequel_select_values(append_select.map { |v| primary_scope_table(ds) + ".#{v}" }))
|
161
|
-
.eager(eager)
|
191
|
+
.eager(eager)
|
192
|
+
end
|
193
|
+
|
194
|
+
def get_rows(ds)
|
195
|
+
get_final_scope(ds).all
|
162
196
|
end
|
163
197
|
|
164
198
|
def primary_scope_table(ds)
|
@@ -187,7 +221,7 @@ module Datasource
|
|
187
221
|
Datasource::Adapters::Sequel
|
188
222
|
end
|
189
223
|
|
190
|
-
|
224
|
+
define_singleton_method(:primary_key) do
|
191
225
|
klass.primary_key
|
192
226
|
end
|
193
227
|
end
|
@@ -2,16 +2,23 @@ module Datasource
|
|
2
2
|
module Attributes
|
3
3
|
class ComputedAttribute
|
4
4
|
class << self
|
5
|
-
attr_accessor :_depends
|
5
|
+
attr_accessor :_depends, :_loader_depends
|
6
6
|
|
7
7
|
def inherited(base)
|
8
8
|
base._depends = (_depends || {}).dup # TODO: deep dup?
|
9
|
+
base._loader_depends = (_loader_depends || []).dup # TODO: deep dup?
|
9
10
|
end
|
10
11
|
|
11
12
|
def depends(*args)
|
12
13
|
args.each do |dep|
|
13
14
|
_depends.deep_merge!(dep)
|
14
15
|
end
|
16
|
+
_depends.delete_if do |key, value|
|
17
|
+
if [:loaders, :loader].include?(key.to_sym)
|
18
|
+
self._loader_depends += Array(value).map(&:to_sym)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
@@ -65,5 +65,22 @@ module Datasource
|
|
65
65
|
end
|
66
66
|
@_loaders[name.to_sym] = klass
|
67
67
|
end
|
68
|
+
|
69
|
+
def self.loaded(name, _options = {}, &block)
|
70
|
+
loader(name, _options, &block)
|
71
|
+
renamed_existing_method = :"#{name}_without_datasource"
|
72
|
+
orm_klass.class_eval do
|
73
|
+
alias_method renamed_existing_method, name if method_defined?(name)
|
74
|
+
fail "#{name} already defined on #{to_s}, would be overridden by datasource loaded method" if method_defined?(name)
|
75
|
+
define_method name do |*args, &block|
|
76
|
+
if loaded_values || !method_defined?(renamed_existing_method)
|
77
|
+
loaded_values[name.to_sym]
|
78
|
+
else
|
79
|
+
send(renamed_existing_method, *args, &block)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
computed name, loader: name
|
84
|
+
end
|
68
85
|
end
|
69
86
|
end
|
data/lib/datasource/base.rb
CHANGED
@@ -24,6 +24,10 @@ module Datasource
|
|
24
24
|
fail Datasource::Error, "Model class not set for #{name}. You should define it:\nclass YourDatasource\n @orm_klass = MyModelClass\nend"
|
25
25
|
end
|
26
26
|
|
27
|
+
def primary_key
|
28
|
+
:id
|
29
|
+
end
|
30
|
+
|
27
31
|
def reflection_select(reflection, parent_select, assoc_select)
|
28
32
|
# append foreign key depending on assoication
|
29
33
|
if reflection[:macro] == :belongs_to
|
@@ -81,16 +85,14 @@ module Datasource
|
|
81
85
|
@expose_associations = {}
|
82
86
|
end
|
83
87
|
|
84
|
-
def primary_key
|
85
|
-
:id
|
86
|
-
end
|
87
|
-
|
88
88
|
def select_all
|
89
89
|
@expose_attributes = self.class._attributes.keys.dup
|
90
90
|
end
|
91
91
|
|
92
92
|
def select(*names)
|
93
93
|
failure = ->(name) { fail Datasource::Error, "attribute or association #{name} doesn't exist for #{self.class.orm_klass.name}, did you forget to call \"computed :#{name}, <dependencies>\" in your datasource_module?" }
|
94
|
+
newly_exposed_attributes = []
|
95
|
+
missing_attributes = []
|
94
96
|
names.each do |name|
|
95
97
|
if name.kind_of?(Hash)
|
96
98
|
name.each_pair do |assoc_name, assoc_select|
|
@@ -100,53 +102,80 @@ module Datasource
|
|
100
102
|
@expose_associations[assoc_name] += Array(assoc_select)
|
101
103
|
@expose_associations[assoc_name].uniq!
|
102
104
|
else
|
105
|
+
missing_attributes << assoc_name
|
103
106
|
failure.call(assoc_name)
|
104
107
|
end
|
105
108
|
end
|
106
109
|
else
|
107
110
|
name = name.to_s
|
108
111
|
if self.class._attributes.key?(name)
|
109
|
-
@expose_attributes.
|
112
|
+
unless @expose_attributes.include?(name)
|
113
|
+
@expose_attributes.push(name)
|
114
|
+
newly_exposed_attributes.push(name)
|
115
|
+
end
|
110
116
|
else
|
117
|
+
missing_attributes << name
|
111
118
|
failure.call(name)
|
112
119
|
end
|
113
120
|
end
|
114
121
|
end
|
115
|
-
|
122
|
+
update_dependencies(newly_exposed_attributes) unless newly_exposed_attributes.empty?
|
123
|
+
fail_missing_attributes(missing_attributes) unless missing_attributes.blank?
|
116
124
|
self
|
117
125
|
end
|
118
126
|
|
127
|
+
def fail_missing_attributes(names)
|
128
|
+
message = if names.size > 1
|
129
|
+
"attributes or associations #{names.join(', ')} don't exist "
|
130
|
+
else
|
131
|
+
"attribute or association #{names.first} doesn't exist "
|
132
|
+
end
|
133
|
+
message += "for #{self.class.orm_klass.name}, "
|
134
|
+
message += "did you forget to call \"computed :#{name}, <dependencies>\" in your datasource_module?"
|
135
|
+
fail Datasource::Error, message
|
136
|
+
end
|
137
|
+
|
138
|
+
def update_dependencies(names)
|
139
|
+
scope_table = adapter.primary_scope_table(self)
|
140
|
+
|
141
|
+
self.class._attributes.values.each do |att|
|
142
|
+
next unless names.include?(att[:name])
|
143
|
+
next unless att[:klass]
|
144
|
+
|
145
|
+
if att[:klass].ancestors.include?(Attributes::ComputedAttribute)
|
146
|
+
att[:klass]._depends.each_pair do |key, value|
|
147
|
+
if key.to_s == scope_table
|
148
|
+
select(*value)
|
149
|
+
else
|
150
|
+
select(key => value)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
|
154
|
+
att[:klass]._depends.each do |name|
|
155
|
+
next if name == scope_table
|
156
|
+
adapter.ensure_table_join!(self, name, att)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
119
162
|
def get_select_values
|
120
163
|
scope_table = adapter.primary_scope_table(self)
|
164
|
+
|
165
|
+
# SQL select values
|
121
166
|
select_values = Set.new
|
122
|
-
select_values.add("#{scope_table}.#{primary_key}")
|
167
|
+
select_values.add("#{scope_table}.#{self.class.primary_key}")
|
123
168
|
|
124
169
|
self.class._attributes.values.each do |att|
|
125
170
|
if attribute_exposed?(att[:name])
|
126
171
|
if att[:klass] == nil
|
127
172
|
select_values.add("#{scope_table}.#{att[:name]}")
|
128
|
-
elsif att[:klass].ancestors.include?(Attributes::ComputedAttribute)
|
129
|
-
att[:klass]._depends.keys.map(&:to_s).each do |name|
|
130
|
-
next if name == scope_table
|
131
|
-
next if name == "loaders"
|
132
|
-
adapter.ensure_table_join!(self, name, att)
|
133
|
-
end
|
134
|
-
att[:klass]._depends.each_pair do |table, names|
|
135
|
-
next if table.to_sym == :loaders
|
136
|
-
Array(names).each do |name|
|
137
|
-
select_values.add("#{table}.#{name}")
|
138
|
-
end
|
139
|
-
# TODO: handle depends on virtual attribute
|
140
|
-
end
|
141
173
|
elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
|
142
174
|
select_values.add("(#{att[:klass].select_value}) as #{att[:name]}")
|
143
|
-
att[:klass]._depends.each do |name|
|
144
|
-
next if name == scope_table
|
145
|
-
adapter.ensure_table_join!(self, name, att)
|
146
|
-
end
|
147
175
|
end
|
148
176
|
end
|
149
177
|
end
|
178
|
+
|
150
179
|
select_values.to_a
|
151
180
|
end
|
152
181
|
|
@@ -154,6 +183,27 @@ module Datasource
|
|
154
183
|
@expose_attributes.include?(name.to_s)
|
155
184
|
end
|
156
185
|
|
186
|
+
# assume records have all attributes selected (default ORM record)
|
187
|
+
def can_upgrade?(records)
|
188
|
+
query_attributes = @expose_attributes.select do |name|
|
189
|
+
klass = self.class._attributes[name][:klass]
|
190
|
+
if klass
|
191
|
+
klass.ancestors.include?(Attributes::QueryAttribute)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
return true if query_attributes.empty?
|
196
|
+
Array(records).all? do |record|
|
197
|
+
query_attributes.all? do |name|
|
198
|
+
adapter.has_attribute?(record, name)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def upgrade_records(records)
|
204
|
+
adapter.upgrade_records(self, Array(records))
|
205
|
+
end
|
206
|
+
|
157
207
|
def results(rows = nil)
|
158
208
|
rows ||= adapter.get_rows(self)
|
159
209
|
|
@@ -166,23 +216,20 @@ module Datasource
|
|
166
216
|
next if rows.empty?
|
167
217
|
|
168
218
|
if att[:klass].ancestors.include?(Attributes::ComputedAttribute)
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
if loaded_values = loader.load(rows.map(&primary_key), rows, @scope)
|
174
|
-
unless rows.first.loaded_values
|
175
|
-
rows.each do |row|
|
176
|
-
row.loaded_values = {}
|
177
|
-
end
|
178
|
-
end
|
219
|
+
att[:klass]._loader_depends.each do |name|
|
220
|
+
if loader = self.class._loaders[name]
|
221
|
+
if loaded_values = loader.load(rows.map(&self.class.primary_key), rows, @scope)
|
222
|
+
unless rows.first.loaded_values
|
179
223
|
rows.each do |row|
|
180
|
-
row.loaded_values
|
224
|
+
row.loaded_values = {}
|
181
225
|
end
|
182
226
|
end
|
183
|
-
|
184
|
-
|
227
|
+
rows.each do |row|
|
228
|
+
row.loaded_values[name] = loaded_values[row.send(self.class.primary_key)] || loader.default_value
|
229
|
+
end
|
185
230
|
end
|
231
|
+
else
|
232
|
+
raise Datasource::Error, "loader with name :#{name} could not be found"
|
186
233
|
end
|
187
234
|
end
|
188
235
|
end
|