datasource 0.0.6 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 89a34ecb59d8f4183295c43b4badd33aca5f5e62
4
- data.tar.gz: b408320273ce49236bd022d7db00c1fce06bdfd5
3
+ metadata.gz: 50befc547d1d02c68a29f7a73b5a75ae3038ee16
4
+ data.tar.gz: a0866c2dfbfa920696d27d2f83ef4252088c5fe7
5
5
  SHA512:
6
- metadata.gz: 0db668df6f4ae71223655a544d59b805e6bdd2694ab540f37dacc7962700d427799f00308d0cdf03a180a7e45f7d3891ff795e73def10c180bf9df5d92942df3
7
- data.tar.gz: c69ae3fe12467516c7f24d090e7240a398e9e7d341dd82a1c54b459c57dd4fb2f9c796a6180a85e02fc304ea45bbf96361579af3249974df4fd73cc0a02dd997
6
+ metadata.gz: 1d628e4fc124ece6b97cd373438009a27b37d78f13bc18fbef0fcdc410d1c47fdff43f174feef8f06de694141271950c05449c1da8644c8cf3d150fb22edfcd0
7
+ data.tar.gz: f334b8022a8db68d276d22168aaf4e296de0dd9a496c6df22a0a59323a87a2332f0141fa69a4806430f6e9312cfdf983ec560154c7cc07f0eb7d479858a68b56
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Datasource
2
2
 
3
+ **Make sure you are reading the README corresponding to the version your are using**
4
+
3
5
  Automatically preload your ORM records for your serializer.
4
6
 
5
- ## Install
7
+ #### Install
6
8
 
7
9
  Add to Gemfile
8
10
 
@@ -10,9 +12,12 @@ Add to Gemfile
10
12
  gem 'datasource'
11
13
  ```
12
14
 
13
- And `bundle install`.
15
+ ```
16
+ bundle install
17
+ rails g datasource:install
18
+ ```
14
19
 
15
- Run install generator:
20
+ #### Upgrade
16
21
 
17
22
  ```
18
23
  rails g datasource:install
@@ -27,25 +32,18 @@ rails g datasource:install
27
32
 
28
33
  - active_model_serializers
29
34
 
30
- ## Basic Usage
31
-
32
- ### Attributes
33
- You don't have to do anything special.
34
-
35
- ```ruby
36
- class UserSerializer < ActiveModel::Serializer
37
- attributes :id, :email
38
- end
39
- ```
40
-
41
- But you get an optimized query for free:
35
+ ## Simple Mode
42
36
 
43
- ```sql
44
- SELECT id, email FROM users
45
- ```
37
+ Datasource is configured to run in Simple mode by default, which makes it easier
38
+ to start with, but disables some advanced optimizations. See
39
+ [Advanced mode](https://github.com/mrbrdo/datasource/wiki/Advanced-mode) for more
40
+ information after you understand Simple mode.
46
41
 
47
42
  ### Associations
48
- You don't have to do anything special.
43
+
44
+ The most noticable magic effect of using Datasource in Simple mode (Advanced mode
45
+ has other benefits) is that associations will automatically be preloaded using a
46
+ single query.
49
47
 
50
48
  ```ruby
51
49
  class PostSerializer < ActiveModel::Serializer
@@ -57,45 +55,13 @@ class UserSerializer < ActiveModel::Serializer
57
55
  has_many :posts
58
56
  end
59
57
  ```
60
-
61
- But you get automatic association preloading ("includes") with optimized queries for free:
62
-
63
58
  ```sql
64
- SELECT id FROM users
65
- SELECT id, title, user_id FROM posts WHERE id IN (?)
59
+ SELECT users.* FROM users
60
+ SELECT posts.* FROM posts WHERE id IN (?)
66
61
  ```
67
62
 
68
- ### Model Methods / Virtual Attributes
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.
70
-
71
- ```ruby
72
- class User < ActiveRecord::Base
73
- datasource_module do
74
- computed :first_name_initial, :first_name
75
- computed :both_initials, :first_name, :last_name
76
- end
77
-
78
- # method can be in model
79
- def first_name_initial
80
- first_name[0].upcase
81
- end
82
- end
83
-
84
- class UserSerializer < ActiveModel::Serializer
85
- attributes :first_name_initial, :last_name_initial
86
-
87
- # method can also be in serializer
88
- def both_initials
89
- object.last_name[0].upcase + object.last_name[0].upcase
90
- end
91
- end
92
- ```
93
-
94
- ```sql
95
- SELECT first_name, last_name FROM users
96
- ```
97
-
98
- You will be reminded with an exception if you forget to do this.
63
+ This means you **do not** need to call `includes` yourself. It will be done
64
+ automatically by Datasource.
99
65
 
100
66
  ### Show action
101
67
 
@@ -128,8 +94,6 @@ class UsersController < ApplicationController
128
94
  end
129
95
  ```
130
96
 
131
- ## Advanced Usage
132
-
133
97
  ### Query attribute
134
98
 
135
99
  You can specify a SQL fragment for `SELECT` and use that as an attribute on your
@@ -150,7 +114,7 @@ end
150
114
  ```
151
115
 
152
116
  ```sql
153
- SELECT users.id, (users.first_name || ' ' || users.last_name) AS full_name FROM users
117
+ SELECT users.*, (users.first_name || ' ' || users.last_name) AS full_name FROM users
154
118
  ```
155
119
 
156
120
  Note: If you need data from another table, use a join in a loader (see below).
@@ -161,8 +125,13 @@ You might want to have some more complex preloading logic. In that case you can
161
125
  A loader will receive ids of the records, and needs to return a hash.
162
126
  The key of the hash must be the id of the record for which the value is.
163
127
 
164
- A loader will only be executed if a computed attribute depends on it. If an attribute depends
165
- on multiple loaders, pass an array of loaders like so `computed :attr, loaders: [:loader1, :loader2]`.
128
+ A loader will only be executed if a computed attribute depends on it. See
129
+ [Advanced mode](https://github.com/mrbrdo/datasource/wiki/Advanced-mode) for
130
+ information about computed attributes (but this works the same way in Simple mode).
131
+ A more simple alternative to loader which doesn't require computed attributes is to use
132
+ [Loaded](#loaded).
133
+ If an attribute depends on multiple loaders, pass an array of loaders like
134
+ so `computed :attr, loaders: [:loader1, :loader2]`.
166
135
 
167
136
  Be careful that if your hash does not contain a value for the object ID, the loaded value
168
137
  will be nil. However you can use the `default` option for such cases (see below example).
@@ -191,7 +160,7 @@ end
191
160
  ```
192
161
 
193
162
  ```sql
194
- SELECT users.id FROM users
163
+ SELECT users.* FROM users
195
164
  SELECT user_id, COUNT(id) FROM posts WHERE user_id IN (?)
196
165
  ```
197
166
 
@@ -227,8 +196,8 @@ end
227
196
 
228
197
  ### Loaded
229
198
 
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.
199
+ Loaded is the same as loader, but it automatically creates a computed attribute
200
+ and defines a method with the same name on your model.
232
201
 
233
202
  Here is the previous example with `loaded` instead of `loader`:
234
203
 
@@ -265,10 +234,10 @@ class User < ActiveRecord::Base
265
234
  .group(:user_id)
266
235
  .pluck("user_id, COUNT(id)")
267
236
  end
237
+ end
268
238
 
269
- def post_count
270
- posts.count
271
- end
239
+ def post_count
240
+ posts.count
272
241
  end
273
242
  end
274
243
 
@@ -279,3 +248,17 @@ end
279
248
  User.first.post_count # <- your method will be called
280
249
 
281
250
  ```
251
+
252
+ ## Getting Help
253
+
254
+ If you find a bug, please report an [Issue](https://github.com/mrbrdo/datasource/issues/new).
255
+
256
+ If you have a question, you can also open an Issue.
257
+
258
+ ## Contributing
259
+
260
+ 1. Fork it ( https://github.com/mrbrdo/datasource/fork )
261
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
262
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
263
+ 4. Push to the branch (`git push origin my-new-feature`)
264
+ 5. Create a new Pull Request
@@ -136,22 +136,20 @@ module Datasource
136
136
  end
137
137
  end
138
138
 
139
- def load_association(records, name)
139
+ def load_association(records, name, assoc_select)
140
140
  return if records.empty?
141
141
  return if records.first.association(name.to_sym).loaded?
142
142
  klass = records.first.class
143
143
  if reflection = klass.reflections[name.to_sym]
144
144
  assoc_class = association_klass(reflection)
145
145
  datasource_class = assoc_class.default_datasource
146
- # TODO: extract serializer_class from parent serializer association
147
- serializer_class = Datasource::Base.consumer_adapter.get_serializer_for(assoc_class)
148
146
 
149
- # TODO: can we make it use datasource scope (with_serializer)? like Sequel
150
147
  scope = assoc_class.all
151
148
  datasource = datasource_class.new(scope)
152
- datasource_select = serializer_class._attributes.dup
153
- Datasource::Base.reflection_select(association_reflection(klass, name.to_sym), [], datasource_select)
154
- datasource.select(*datasource_select)
149
+ assoc_select_attributes = assoc_select.reject { |att| att.kind_of?(Hash) }
150
+ assoc_select_associations = assoc_select.select { |att| att.kind_of?(Hash) }
151
+ Datasource::Base.reflection_select(association_reflection(klass, name.to_sym), [], assoc_select_attributes)
152
+ datasource.select(*assoc_select_attributes)
155
153
  select_values = datasource.get_select_values
156
154
 
157
155
  begin
@@ -163,8 +161,10 @@ module Datasource
163
161
  end
164
162
 
165
163
  assoc_records = records.flat_map { |record| record.send(name) }.compact
166
- serializer_class._associations.each_pair do |assoc_name, options|
167
- load_association(assoc_records, assoc_name)
164
+ assoc_select_associations.each do |assocs|
165
+ assocs.each_pair do |assoc_name, assoc_select|
166
+ load_association(assoc_records, assoc_name, assoc_select)
167
+ end
168
168
  end
169
169
  datasource.results(assoc_records)
170
170
  end
@@ -193,7 +193,7 @@ module Datasource
193
193
 
194
194
  def load_associations(ds, records)
195
195
  ds.expose_associations.each_pair do |assoc_name, assoc_select|
196
- load_association(records, assoc_name)
196
+ load_association(records, assoc_name, assoc_select)
197
197
  end
198
198
  end
199
199
 
@@ -149,7 +149,7 @@ module Datasource
149
149
  {
150
150
  name => ->(ds) {
151
151
  ds.with_datasource(datasource_class)
152
- .datasource_select(*(self_append_select + assoc_select))
152
+ .datasource_select(*(assoc_select + self_append_select))
153
153
  }
154
154
  }
155
155
  else
@@ -83,14 +83,29 @@ module Datasource
83
83
  end
84
84
  @expose_attributes = []
85
85
  @expose_associations = {}
86
+ @select_all_columns = false
87
+ end
88
+
89
+ def select_all_columns
90
+ column_attributes = self.class._attributes.values.select do |att|
91
+ att[:klass].nil?
92
+ end
93
+ columns = column_attributes.map { |att| att[:name] }
94
+ select(*columns)
95
+ @select_all_columns = true
96
+
97
+ columns
86
98
  end
87
99
 
88
100
  def select_all
89
- @expose_attributes = self.class._attributes.keys.dup
101
+ attributes = self.class._attributes.keys
102
+ select(*attributes)
103
+ @select_all_columns = true
104
+
105
+ attributes
90
106
  end
91
107
 
92
108
  def select(*names)
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
109
  newly_exposed_attributes = []
95
110
  missing_attributes = []
96
111
  names.each do |name|
@@ -99,23 +114,23 @@ module Datasource
99
114
  assoc_name = assoc_name.to_s
100
115
  if self.class._associations.key?(assoc_name)
101
116
  @expose_associations[assoc_name] ||= []
102
- @expose_associations[assoc_name] += Array(assoc_select)
117
+ @expose_associations[assoc_name].concat(Array(assoc_select))
103
118
  @expose_associations[assoc_name].uniq!
104
119
  else
105
120
  missing_attributes << assoc_name
106
- failure.call(assoc_name)
107
121
  end
108
122
  end
109
123
  else
110
124
  name = name.to_s
111
- if self.class._attributes.key?(name)
125
+ if name == "*"
126
+ newly_exposed_attributes.concat(select_all_columns.map(&:to_s))
127
+ elsif self.class._attributes.key?(name)
112
128
  unless @expose_attributes.include?(name)
113
129
  @expose_attributes.push(name)
114
130
  newly_exposed_attributes.push(name)
115
131
  end
116
132
  else
117
133
  missing_attributes << name
118
- failure.call(name)
119
134
  end
120
135
  end
121
136
  end
@@ -161,17 +176,28 @@ module Datasource
161
176
 
162
177
  def get_select_values
163
178
  scope_table = adapter.primary_scope_table(self)
164
-
165
- # SQL select values
166
179
  select_values = Set.new
167
- select_values.add("#{scope_table}.#{self.class.primary_key}")
168
180
 
169
- self.class._attributes.values.each do |att|
170
- if attribute_exposed?(att[:name])
171
- if att[:klass] == nil
172
- select_values.add("#{scope_table}.#{att[:name]}")
173
- elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
174
- select_values.add("(#{att[:klass].select_value}) as #{att[:name]}")
181
+ if @select_all_columns
182
+ select_values.add("#{scope_table}.*")
183
+
184
+ self.class._attributes.values.each do |att|
185
+ if att[:klass] && attribute_exposed?(att[:name])
186
+ if att[:klass].ancestors.include?(Attributes::QueryAttribute)
187
+ select_values.add("(#{att[:klass].select_value}) as #{att[:name]}")
188
+ end
189
+ end
190
+ end
191
+ else
192
+ select_values.add("#{scope_table}.#{self.class.primary_key}")
193
+
194
+ self.class._attributes.values.each do |att|
195
+ if attribute_exposed?(att[:name])
196
+ if att[:klass] == nil
197
+ select_values.add("#{scope_table}.#{att[:name]}")
198
+ elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
199
+ select_values.add("(#{att[:klass].select_value}) as #{att[:name]}")
200
+ end
175
201
  end
176
202
  end
177
203
  end
@@ -207,29 +233,33 @@ module Datasource
207
233
  def results(rows = nil)
208
234
  rows ||= adapter.get_rows(self)
209
235
 
210
- @expose_attributes.each do |name|
211
- att = self.class._attributes[name]
212
- fail Datasource::Error, "attribute #{name} doesn't exist for #{self.class.orm_klass.name}, did you forget to call \"computed :#{name}, :db_column_dependency\" in your datasource_module? See https://github.com/mrbrdo/datasource#model-methods--virtual-attributes" unless att
213
- klass = att[:klass]
214
- next unless klass
215
-
216
- next if rows.empty?
217
-
218
- if att[:klass].ancestors.include?(Attributes::ComputedAttribute)
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
236
+ unless rows.empty?
237
+ @expose_attributes.each do |name|
238
+ att = self.class._attributes[name]
239
+ klass = att[:klass]
240
+ next unless klass
241
+
242
+ if att[:klass].ancestors.include?(Attributes::ComputedAttribute)
243
+ att[:klass]._loader_depends.each do |name|
244
+ if loader = self.class._loaders[name]
245
+ if loaded_values = loader.load(rows.map(&self.class.primary_key), rows, @scope)
246
+ unless rows.first.loaded_values
247
+ rows.each do |row|
248
+ row.loaded_values = {}
249
+ end
250
+ end
223
251
  rows.each do |row|
224
- row.loaded_values = {}
252
+ key = row.send(self.class.primary_key)
253
+ if loaded_values.key?(key)
254
+ row.loaded_values[name] = loaded_values[key]
255
+ elsif loader.default_value
256
+ row.loaded_values[name] = loader.default_value
257
+ end
225
258
  end
226
259
  end
227
- rows.each do |row|
228
- row.loaded_values[name] = loaded_values[row.send(self.class.primary_key)] || loader.default_value
229
- end
260
+ else
261
+ raise Datasource::Error, "loader with name :#{name} could not be found"
230
262
  end
231
- else
232
- raise Datasource::Error, "loader with name :#{name} could not be found"
233
263
  end
234
264
  end
235
265
  end
@@ -0,0 +1,23 @@
1
+ module Datasource
2
+ module Configuration
3
+ include ActiveSupport::Configurable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do |base|
7
+ base.config.adapters = Configuration.default_adapters
8
+ base.config.simple_mode = false
9
+ end
10
+
11
+ def self.default_adapters
12
+ default_adapters = []
13
+ if defined? ActiveRecord
14
+ default_adapters.push(:activerecord)
15
+ elsif defined? Sequel
16
+ default_adapters.push(:sequel)
17
+ end
18
+ if defined? ActiveModel::Serializer
19
+ default_adapters.push(:ams)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -36,6 +36,7 @@ module Datasource
36
36
  def to_datasource_select(result, klass, serializer = nil, serializer_assoc = nil, adapter = nil)
37
37
  adapter ||= Datasource::Base.default_adapter
38
38
  serializer ||= get_serializer_for(klass, serializer_assoc)
39
+ result.unshift("*") if Datasource.config.simple_mode
39
40
  result.concat(serializer._attributes)
40
41
  result_assocs = {}
41
42
  result.push(result_assocs)
data/lib/datasource.rb CHANGED
@@ -1,6 +1,8 @@
1
+ require 'datasource/configuration'
1
2
  module Datasource
2
3
  Error = Class.new(StandardError)
3
4
  RecursionError = Class.new(StandardError)
5
+ include Configuration
4
6
 
5
7
  AdapterPaths = {
6
8
  activerecord: 'datasource/adapters/active_record',
@@ -12,25 +14,23 @@ module Datasource
12
14
 
13
15
  module_function
14
16
  def load(*adapters)
15
- if adapters.empty?
16
- adapters = []
17
- if defined? ActiveRecord
18
- adapters.push(:activerecord)
19
- elsif defined? Sequel
20
- adapters.push(:sequel)
21
- end
22
- if defined? ActiveModel::Serializer
23
- adapters.push(:ams)
24
- end
17
+ unless adapters.empty?
18
+ warn "[DEPRECATION] passing adapters to Datasource.load is deprecated. Use Datasource.setup instead."
19
+ config.adapters = adapters
25
20
  end
26
21
 
27
- adapters.each do |adapter|
22
+ config.adapters.each do |adapter|
28
23
  adapter = AdapterPaths[adapter]
29
24
  adapter = AdapterPaths[adapter] if adapter.is_a?(Symbol)
30
25
  require adapter
31
26
  end
32
27
  end
33
28
 
29
+ def setup
30
+ yield(config)
31
+ load
32
+ end
33
+
34
34
  def orm_adapters
35
35
  @orm_adapters ||= begin
36
36
  Datasource::Adapters.constants.map { |name| Datasource::Adapters.const_get(name) }
@@ -1 +1,10 @@
1
- Datasource.load(:activerecord, :active_model_serializers)
1
+ Datasource.setup do |config|
2
+ # Adapters to load
3
+ # Available ORM adapters: activerecord, sequel
4
+ # Available Serializer adapters: active_model_serializers
5
+ config.adapters = [:activerecord, :active_model_serializers]
6
+
7
+ # Enable simple mode, which will always select all model database columns,
8
+ # making Datasource easier to use. See documentation for details.
9
+ config.simple_mode = true
10
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datasource
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Berdajs
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,20 +94,6 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0.9'
83
- - !ruby/object:Gem::Dependency
84
- name: activesupport
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '4'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '4'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: sequel
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -139,6 +139,7 @@ files:
139
139
  - lib/datasource/attributes/loader.rb
140
140
  - lib/datasource/attributes/query_attribute.rb
141
141
  - lib/datasource/base.rb
142
+ - lib/datasource/configuration.rb
142
143
  - lib/datasource/consumer_adapters/active_model_serializers.rb
143
144
  - lib/datasource/serializer.rb
144
145
  - lib/generators/datasource/install_generator.rb