pluck_map 0.6.2 → 2.0.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 +4 -4
- data/.travis.yml +8 -3
- data/Appraisals +5 -0
- data/CHANGELOG.md +19 -0
- data/README.md +99 -9
- data/gemfiles/rails_6.0.gemfile +8 -0
- data/lib/pluck_map/association_scope.rb +102 -0
- data/lib/pluck_map/attribute.rb +26 -13
- data/lib/pluck_map/attribute_builder.rb +20 -3
- data/lib/pluck_map/attributes.rb +18 -2
- data/lib/pluck_map/errors.rb +4 -0
- data/lib/pluck_map/model_context.rb +22 -1
- data/lib/pluck_map/nodes.rb +40 -0
- data/lib/pluck_map/presenter.rb +15 -29
- data/lib/pluck_map/presenters/to_csv.rb +16 -4
- data/lib/pluck_map/presenters/to_h.rb +10 -4
- data/lib/pluck_map/presenters/to_json.rb +78 -2
- data/lib/pluck_map/relationships.rb +89 -0
- data/lib/pluck_map/relationships/base.rb +26 -0
- data/lib/pluck_map/relationships/many.rb +34 -0
- data/lib/pluck_map/relationships/one.rb +24 -0
- data/lib/pluck_map/relationships/polymorphic_one.rb +58 -0
- data/lib/pluck_map/struct.rb +13 -0
- data/lib/pluck_map/structured_attribute.rb +37 -0
- data/lib/pluck_map/version.rb +1 -1
- data/lib/pluck_map/visitors.rb +4 -0
- data/lib/pluck_map/visitors/mysql.rb +36 -0
- data/lib/pluck_map/visitors/postgresql.rb +30 -0
- data/lib/pluck_map/visitors/sqlite.rb +30 -0
- data/pluck_map.gemspec +4 -1
- metadata +23 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 597612beb6033e9f03ae69a121aa0a91eab830e400917e3dc8e1f4b42052545b
|
|
4
|
+
data.tar.gz: 0bac2743a30f9795a6bfa572ef2faf7ee230aea78b75b9d0b0ac41399fd37077
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5dab317224c87b728585ebfd45267be0780854fe0a383b31cabcf88781c58900845af6d3dcf760f2beb35199b58fc7636f3a55dad125ead09579414cdd867fa2
|
|
7
|
+
data.tar.gz: 2cc8bfb68996ca3a350a0cd3324e9d56309485975622b72c0a13584c523f47a7bf1f90ca59b16fee0afc145c4283da12cf3504f04e2b8cfb71c43b42eb9ba26a
|
data/.travis.yml
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
language: ruby
|
|
2
|
-
rvm: 2.
|
|
2
|
+
rvm: 2.6.3
|
|
3
|
+
|
|
4
|
+
# we need MySQL 5.7+ to support JSON aggregation
|
|
5
|
+
dist: xenial
|
|
3
6
|
|
|
4
|
-
# Rails 6 requires Postgres 9.3+
|
|
5
7
|
addons:
|
|
6
|
-
|
|
8
|
+
# Rails 6 requires Postgres 9.3+
|
|
9
|
+
# We need Postgres 9.4+ to support JSON aggregation
|
|
10
|
+
postgresql: "9.4"
|
|
7
11
|
|
|
8
12
|
services:
|
|
9
13
|
- postgresql
|
|
@@ -17,6 +21,7 @@ matrix:
|
|
|
17
21
|
- gemfile: gemfiles/rails_5.0.gemfile
|
|
18
22
|
- gemfile: gemfiles/rails_5.1.gemfile
|
|
19
23
|
- gemfile: gemfiles/rails_5.2.gemfile
|
|
24
|
+
- gemfile: gemfiles/rails_6.0.gemfile
|
|
20
25
|
- gemfile: gemfiles/rails_edge.gemfile
|
|
21
26
|
allow_failures:
|
|
22
27
|
- gemfile: gemfiles/rails_edge.gemfile
|
data/Appraisals
CHANGED
|
@@ -19,6 +19,11 @@ appraise "rails-5.2" do
|
|
|
19
19
|
gem "sqlite3", "~> 1.3.6"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
appraise "rails-6.0" do
|
|
23
|
+
gem "activerecord", "~> 6.0.0"
|
|
24
|
+
gem "sqlite3", "~> 1.4.0"
|
|
25
|
+
end
|
|
26
|
+
|
|
22
27
|
appraise "rails-edge" do
|
|
23
28
|
gem "rails", git: "https://github.com/rails/rails.git", branch: "master", require: "activerecord"
|
|
24
29
|
gem "sqlite3", "~> 1.4.0"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
* BREAKING: `define` returns a subclass of `PluckMap::Presenter` instead of an instance (@boblail)
|
|
4
|
+
* FEATURE: `define` also creates structs to correspond to each presenter (@boblail)
|
|
5
|
+
|
|
6
|
+
## v1.0.0 (2019 Jul 17)
|
|
7
|
+
|
|
8
|
+
* FIX: Respect default_scopes for relationships (@boblail)
|
|
9
|
+
|
|
10
|
+
## v1.0.0.rc2 (2019 Jun 17)
|
|
11
|
+
|
|
12
|
+
* FEATURE: Add structured attributes to allow nesting attributes as a hash (@kobsy)
|
|
13
|
+
|
|
14
|
+
## v1.0.0.rc1 (2019 May 12)
|
|
15
|
+
|
|
16
|
+
* BREAKING: Remove deprecated features/methods (@boblail)
|
|
17
|
+
* FEATURE: Optimize `to_json` when a presenter doesn't need to process values in Ruby (@boblail)
|
|
18
|
+
* FEATURE: Add `has_many` and `has_one` DSL for presenting nested resources (@boblail)
|
|
19
|
+
|
|
1
20
|
## v0.6.2 (2019 Jun 13)
|
|
2
21
|
|
|
3
22
|
* FIX: Allow presenting a subclass of the presenter's model (@kobsy)
|
data/README.md
CHANGED
|
@@ -8,13 +8,14 @@ This library provides a DSL for presenting ActiveRecord::Relations without insta
|
|
|
8
8
|
|
|
9
9
|
### Table of Contents
|
|
10
10
|
|
|
11
|
-
- [Why PluckMap?](
|
|
11
|
+
- [Why PluckMap?](#why-pluckmap)
|
|
12
12
|
- Usage
|
|
13
|
-
- [Defining attributes to present](
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
13
|
+
- [Defining attributes to present](#defining-attributes-to-present)
|
|
14
|
+
- [Relationships](#relationships)
|
|
15
|
+
- [Presenting Records](#presenting-records)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Requirements](#requirements)
|
|
18
|
+
- [Development & Contributing](#development)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
## Why PluckMap?
|
|
@@ -207,6 +208,93 @@ presenter = PluckMap[Person].define do
|
|
|
207
208
|
end
|
|
208
209
|
```
|
|
209
210
|
|
|
211
|
+
### Structured attributes
|
|
212
|
+
|
|
213
|
+
You can also nest attributes by passing a block to the attribute method:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
presenter = PluckMap[Person].define do
|
|
217
|
+
parent do
|
|
218
|
+
id select: :parent_id
|
|
219
|
+
type "Parent"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Relationships
|
|
225
|
+
|
|
226
|
+
PluckMap can also describe nested data. There are two special methods in the `define` block that introduce child resources:
|
|
227
|
+
|
|
228
|
+
1. `has_one` will treat the resource as a nested object or null
|
|
229
|
+
2. `has_many` will treat the resource as an array of nested objects (which may be empty)
|
|
230
|
+
|
|
231
|
+
The first argument to either of these methods is the name of an association on the presented model.
|
|
232
|
+
|
|
233
|
+
You can use either of these methods with any kind of ActiveRecord relation (`belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`), although it generally makes more sense to use `has_one` with Rails' singular associations and `has_many` with Rails' plural associations.
|
|
234
|
+
|
|
235
|
+
#### `has_one`
|
|
236
|
+
|
|
237
|
+
In the example below, assume
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class Book < ActiveRecord::Base
|
|
241
|
+
belongs_to :author
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
This presenter :point_down: selects the title of every book as well as its author's name:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
presenter = PluckMap[Book].define do
|
|
249
|
+
title
|
|
250
|
+
has_one :author do
|
|
251
|
+
name
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
(We can also write it using block variables, if that's easier to read.)
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
presenter = PluckMap[Book].define do |book|
|
|
260
|
+
book.title
|
|
261
|
+
book.has_one :author do |author|
|
|
262
|
+
author.name
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Attributes defined for a relationship support all the same features as [attributes defined at the root level](#defining-attributes-to-present).
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
#### `has_many`
|
|
271
|
+
|
|
272
|
+
We can present the reverse of the above example with `has_many`. This example will select a list of authors and, for each, a list of the books they wrote:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
presenter = PluckMap[Author].define do
|
|
276
|
+
name
|
|
277
|
+
has_many :books do
|
|
278
|
+
title
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### scopes
|
|
284
|
+
|
|
285
|
+
An optional second argument to both `has_one` and `has_many` is a scope block that you can use to modify the query that would select the associated records. You can use any of ActiveRecord's standard [querying methods](https://guides.rubyonrails.org/active_record_querying.html) inside the scope block.
|
|
286
|
+
|
|
287
|
+
In this example, we've altered our last presenter to ensure that books are listed alphabetically:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
presenter = PluckMap[Author].define do
|
|
291
|
+
name
|
|
292
|
+
has_many :books, -> { order(title: :asc) } do
|
|
293
|
+
title
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
210
298
|
|
|
211
299
|
### Presenting Records
|
|
212
300
|
|
|
@@ -299,9 +387,11 @@ The gem's only runtime requirement is:
|
|
|
299
387
|
|
|
300
388
|
It supports these databases out of the box:
|
|
301
389
|
|
|
302
|
-
- PostgreSQL
|
|
303
|
-
- MySQL
|
|
304
|
-
- SQLite
|
|
390
|
+
- PostgreSQL 9.4+
|
|
391
|
+
- MySQL 5.7.22+
|
|
392
|
+
- SQLite 3.10.0+
|
|
393
|
+
|
|
394
|
+
(Note: the versions given above are when certain JSON aggregate functions were introduced in each supported database. `PluckMap`'s core behavior will work with earlier versions of the database above but certain features like optimizations to `to_json` and relationships require the specified versions.)
|
|
305
395
|
|
|
306
396
|
|
|
307
397
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "active_record"
|
|
2
|
+
|
|
3
|
+
# ActiveRecord::Associations::AssociationScope assumes that values
|
|
4
|
+
# for Owner's fields will be concrete values that need to be type-cast.
|
|
5
|
+
#
|
|
6
|
+
# But our AbstractOwner returns field references (Arel::Attributes::Attribute)
|
|
7
|
+
# and we need them to bypass type-casting.
|
|
8
|
+
#
|
|
9
|
+
module PluckMap
|
|
10
|
+
module AssociationScope
|
|
11
|
+
def self.[](version)
|
|
12
|
+
const_get "Rails#{version.to_s.delete(".")}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.create
|
|
16
|
+
case ActiveRecord.version.segments.take(2)
|
|
17
|
+
when [4,2] then self[4.2].create
|
|
18
|
+
when [5,0] then self[5.0].create
|
|
19
|
+
else self::Current.create
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Rails 5.1+
|
|
25
|
+
class Current < ActiveRecord::Associations::AssociationScope
|
|
26
|
+
def apply_scope(scope, table, key, value)
|
|
27
|
+
if value.is_a?(Arel::Attributes::Attribute)
|
|
28
|
+
scope.where!(table[key].eq(value))
|
|
29
|
+
else
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def scope(association)
|
|
35
|
+
if ActiveRecord.version.version < "5.2"
|
|
36
|
+
super(association, association.reflection.active_record.connection)
|
|
37
|
+
else
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# In Rails 5.0, `apply_scope` isn't extracted from `last_chain_scope`
|
|
45
|
+
# and `next_chain_scope` so we have to override the entire methods to
|
|
46
|
+
# extract `apply_scope` and bypass type-casting.
|
|
47
|
+
#
|
|
48
|
+
# Refer to https://github.com/rails/rails/blob/v5.0.7.2/activerecord/lib/active_record/associations/association_scope.rb#L61-L94
|
|
49
|
+
#
|
|
50
|
+
class Rails50 < Current
|
|
51
|
+
def last_chain_scope(scope, table, reflection, owner, association_klass)
|
|
52
|
+
join_keys = reflection.join_keys(association_klass)
|
|
53
|
+
key = join_keys.key
|
|
54
|
+
foreign_key = join_keys.foreign_key
|
|
55
|
+
|
|
56
|
+
value = transform_value(owner[foreign_key])
|
|
57
|
+
scope = apply_scope(scope, table, key, value)
|
|
58
|
+
|
|
59
|
+
if reflection.type
|
|
60
|
+
polymorphic_type = transform_value(owner.class.base_class.name)
|
|
61
|
+
scope = scope.where(table.name => { reflection.type => polymorphic_type })
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
scope
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
|
|
68
|
+
join_keys = reflection.join_keys(association_klass)
|
|
69
|
+
key = join_keys.key
|
|
70
|
+
foreign_key = join_keys.foreign_key
|
|
71
|
+
|
|
72
|
+
constraint = table[key].eq(foreign_table[foreign_key])
|
|
73
|
+
|
|
74
|
+
if reflection.type
|
|
75
|
+
value = transform_value(next_reflection.klass.base_class.name)
|
|
76
|
+
scope = apply_scope(scope, table, reflection.type, value)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
scope = scope.joins(join(foreign_table, constraint))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply_scope(scope, table, key, value)
|
|
83
|
+
if value.is_a?(Arel::Attributes::Attribute)
|
|
84
|
+
scope.where(table[key].eq(value))
|
|
85
|
+
else
|
|
86
|
+
scope.where(table.name => { key => value })
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Rails42 < Current
|
|
93
|
+
def bind(_, _, _, value, _)
|
|
94
|
+
if value.is_a?(Arel::Attributes::Attribute)
|
|
95
|
+
value
|
|
96
|
+
else
|
|
97
|
+
super
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/pluck_map/attribute.rb
CHANGED
|
@@ -14,16 +14,14 @@ module PluckMap
|
|
|
14
14
|
@value = options[:value]
|
|
15
15
|
@selects = []
|
|
16
16
|
else
|
|
17
|
-
raise ArgumentError, "You must select at least one column" if selects.empty?
|
|
17
|
+
raise ArgumentError, "You must select at least one column" if @selects.empty?
|
|
18
18
|
raise ArgumentError, "You must define a block if you are going to select " <<
|
|
19
|
-
"more than one expression from the database" if selects.length > 1 &&
|
|
19
|
+
"more than one expression from the database" if @selects.length > 1 && !@block
|
|
20
20
|
|
|
21
|
-
@selects
|
|
21
|
+
@selects.each do |select|
|
|
22
22
|
if select.is_a?(String) && !select.is_a?(Arel::Nodes::SqlLiteral)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else
|
|
26
|
-
select
|
|
23
|
+
raise ArgumentError, "#{select.inspect} is not a valid value for :select. " <<
|
|
24
|
+
"If a string of raw SQL is safe, wrap it in Arel.sql()."
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
end
|
|
@@ -33,10 +31,21 @@ module PluckMap
|
|
|
33
31
|
block.call(*object)
|
|
34
32
|
end
|
|
35
33
|
|
|
34
|
+
def value?
|
|
35
|
+
defined?(@value)
|
|
36
|
+
end
|
|
37
|
+
|
|
36
38
|
def will_map?
|
|
37
39
|
!block.nil?
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def nested?
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def preload!(results)
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
# When the PluckMapPresenter performs the query, it will
|
|
41
50
|
# receive an array of rows. Each row will itself be an
|
|
42
51
|
# array of values.
|
|
@@ -44,18 +53,22 @@ module PluckMap
|
|
|
44
53
|
# This method constructs a Ruby expression that will
|
|
45
54
|
# extract the appropriate values from each row that
|
|
46
55
|
# correspond to this Attribute.
|
|
47
|
-
def to_ruby
|
|
48
|
-
if
|
|
49
|
-
puts "DEPRECATION WARNING: PluckMap::Attribute#to_ruby no longer requires an argument. Replace `attribute.to_ruby(keys)` with `attribute.to_ruby`."
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
return @value.inspect if defined?(@value)
|
|
56
|
+
def to_ruby
|
|
57
|
+
return @value.inspect if value?
|
|
53
58
|
return "values[#{indexes[0]}]" if indexes.length == 1 && !block
|
|
54
59
|
ruby = "values.values_at(#{indexes.join(", ")})"
|
|
55
60
|
ruby = "invoke(:\"#{id}\", #{ruby})" if block
|
|
56
61
|
ruby
|
|
57
62
|
end
|
|
58
63
|
|
|
64
|
+
def exec(values)
|
|
65
|
+
return @value if value?
|
|
66
|
+
return values[indexes[0]] if indexes.length == 1 && !block
|
|
67
|
+
_values = values.values_at(*indexes)
|
|
68
|
+
_values = apply(_values) if block
|
|
69
|
+
_values
|
|
70
|
+
end
|
|
71
|
+
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
def values
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "pluck_map/attribute"
|
|
2
|
+
require "pluck_map/structured_attribute"
|
|
2
3
|
require "pluck_map/attributes"
|
|
4
|
+
require "pluck_map/relationships"
|
|
3
5
|
|
|
4
6
|
module PluckMap
|
|
5
7
|
class AttributeBuilder < BasicObject
|
|
@@ -12,7 +14,7 @@ module PluckMap
|
|
|
12
14
|
else
|
|
13
15
|
builder.instance_eval(&block)
|
|
14
16
|
end
|
|
15
|
-
Attributes.new(attributes)
|
|
17
|
+
Attributes.new(attributes, model)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def initialize(attributes, model)
|
|
@@ -20,12 +22,27 @@ module PluckMap
|
|
|
20
22
|
@model = model
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
def method_missing(attribute_name, *args)
|
|
25
|
+
def method_missing(attribute_name, *args, &block)
|
|
24
26
|
options = args.extract_options!
|
|
25
27
|
options[:value] = args.first unless args.empty?
|
|
26
|
-
@attributes.push Attribute.new(attribute_name, @model, options)
|
|
28
|
+
@attributes.push block.nil? ? Attribute.new(attribute_name, @model, options) :
|
|
29
|
+
StructuredAttribute.new(attribute_name, @model, block, options)
|
|
27
30
|
:attribute_added
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
def has_many(name, *args, &block)
|
|
34
|
+
options = args.extract_options!
|
|
35
|
+
options[:scope_block] = args.first unless args.empty?
|
|
36
|
+
@attributes.push Relationships.many(@model, name, block, options)
|
|
37
|
+
:relationship_added
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def has_one(name, *args, &block)
|
|
41
|
+
options = args.extract_options!
|
|
42
|
+
options[:scope_block] = args.first unless args.empty?
|
|
43
|
+
@attributes.push Relationships.one(@model, name, block, options)
|
|
44
|
+
:relationship_added
|
|
45
|
+
end
|
|
46
|
+
|
|
30
47
|
end
|
|
31
48
|
end
|
data/lib/pluck_map/attributes.rb
CHANGED
|
@@ -2,9 +2,10 @@ module PluckMap
|
|
|
2
2
|
class Attributes
|
|
3
3
|
include Enumerable
|
|
4
4
|
|
|
5
|
-
attr_reader :selects
|
|
5
|
+
attr_reader :selects, :model
|
|
6
6
|
|
|
7
|
-
def initialize(attributes)
|
|
7
|
+
def initialize(attributes, model)
|
|
8
|
+
@model = model
|
|
8
9
|
@_attributes = attributes.freeze
|
|
9
10
|
@_attributes_by_id = {}
|
|
10
11
|
@selects = []
|
|
@@ -36,16 +37,31 @@ module PluckMap
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
def ids
|
|
41
|
+
_attributes_by_id.keys
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
def by_id
|
|
40
45
|
_attributes_by_id
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
def to_json_array
|
|
49
|
+
PluckMap::BuildJsonArray.new(*selects.map do |select|
|
|
50
|
+
select = model.arel_table[select] if select.is_a?(Symbol)
|
|
51
|
+
select
|
|
52
|
+
end)
|
|
53
|
+
end
|
|
54
|
+
|
|
43
55
|
|
|
44
56
|
|
|
45
57
|
def will_map?
|
|
46
58
|
_attributes.any?(&:will_map?)
|
|
47
59
|
end
|
|
48
60
|
|
|
61
|
+
def nested?
|
|
62
|
+
_attributes.any?(&:nested?)
|
|
63
|
+
end
|
|
64
|
+
|
|
49
65
|
|
|
50
66
|
|
|
51
67
|
def ==(other)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "pluck_map/presenter"
|
|
2
|
+
require "pluck_map/struct"
|
|
2
3
|
|
|
3
4
|
module PluckMap
|
|
4
5
|
class ModelContext
|
|
@@ -8,7 +9,27 @@ module PluckMap
|
|
|
8
9
|
|
|
9
10
|
def define(&block)
|
|
10
11
|
attributes = PluckMap::AttributeBuilder.build(model: @model, &block)
|
|
11
|
-
|
|
12
|
+
define_class!(@model, attributes)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def define_class!(model, attributes)
|
|
18
|
+
# Create a new subclass of PluckMap::Presenter
|
|
19
|
+
klass = Class.new(PluckMap::Presenter)
|
|
20
|
+
|
|
21
|
+
# Partially apply initialize with the parameters passed to this method
|
|
22
|
+
klass.define_method(:initialize) do |query|
|
|
23
|
+
super(model, attributes, query)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Generate a Struct constant in the namespace of the new subclass
|
|
27
|
+
struct = ::Struct.new(*attributes.ids, keyword_init: true)
|
|
28
|
+
struct.extend PluckMap::Struct::ClassMethods
|
|
29
|
+
struct.instance_variable_set :@presenter, klass
|
|
30
|
+
klass.const_set :Struct, struct
|
|
31
|
+
|
|
32
|
+
klass
|
|
12
33
|
end
|
|
13
34
|
end
|
|
14
35
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "arel"
|
|
2
|
+
|
|
3
|
+
module PluckMap
|
|
4
|
+
class BuildJsonObject < Arel::Nodes::Node
|
|
5
|
+
include Arel::AliasPredication
|
|
6
|
+
|
|
7
|
+
attr_reader :args
|
|
8
|
+
|
|
9
|
+
def initialize(*args)
|
|
10
|
+
@args = args
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class BuildJsonArray < Arel::Nodes::Node
|
|
15
|
+
include Arel::AliasPredication
|
|
16
|
+
|
|
17
|
+
attr_reader :args
|
|
18
|
+
|
|
19
|
+
def initialize(*args)
|
|
20
|
+
@args = args
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class JsonArrayAggregate < Arel::Nodes::Node
|
|
25
|
+
attr_reader :arg
|
|
26
|
+
|
|
27
|
+
def initialize(arg)
|
|
28
|
+
@arg = arg
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class JsonSubqueryAggregate < Arel::Nodes::Node
|
|
33
|
+
attr_reader :scope, :select
|
|
34
|
+
|
|
35
|
+
def initialize(scope, select)
|
|
36
|
+
@scope = scope
|
|
37
|
+
@select = select
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/pluck_map/presenter.rb
CHANGED
|
@@ -1,45 +1,35 @@
|
|
|
1
1
|
require "pluck_map/attribute_builder"
|
|
2
|
+
require "pluck_map/errors"
|
|
3
|
+
require "pluck_map/nodes"
|
|
2
4
|
require "pluck_map/presenters"
|
|
5
|
+
require "pluck_map/visitors"
|
|
3
6
|
require "active_record"
|
|
4
7
|
|
|
5
8
|
module PluckMap
|
|
6
9
|
class Presenter
|
|
7
10
|
include CsvPresenter, HashPresenter, JsonPresenter
|
|
8
11
|
|
|
9
|
-
attr_reader :model, :attributes
|
|
12
|
+
attr_reader :model, :attributes, :query
|
|
10
13
|
|
|
11
|
-
def initialize(model
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@attributes = PluckMap::AttributeBuilder.build(model: nil, &block)
|
|
15
|
-
else
|
|
16
|
-
@model = model
|
|
17
|
-
@attributes = attributes
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
if respond_to?(:define_presenters!, true)
|
|
21
|
-
puts "DEPRECATION WARNING: `define_presenters!` is deprecated; instead mix in a module that implements your presenter method (e.g. `to_h`). Optionally have the method redefine itself the first time it is called."
|
|
22
|
-
# because overridden `define_presenters!` will probably call `super`
|
|
23
|
-
PluckMap::Presenter.class_eval 'protected def define_presenters!; end'
|
|
24
|
-
define_presenters!
|
|
14
|
+
def initialize(model, attributes, query)
|
|
15
|
+
unless query.model <= model
|
|
16
|
+
raise ArgumentError, "Query for #{query.model} but #{model} expected"
|
|
25
17
|
end
|
|
26
|
-
end
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
@model = model
|
|
20
|
+
@attributes = attributes
|
|
21
|
+
@query = query
|
|
31
22
|
end
|
|
32
23
|
|
|
33
24
|
protected
|
|
34
25
|
|
|
35
|
-
def pluck
|
|
36
|
-
unless model.nil? || query.model <= model
|
|
37
|
-
raise ArgumentError, "Query for #{query.model} but #{model} expected"
|
|
38
|
-
end
|
|
39
|
-
|
|
26
|
+
def pluck
|
|
40
27
|
# puts "\e[95m#{query.select(*selects).to_sql}\e[0m"
|
|
41
28
|
results = benchmark("pluck(#{query.table_name})") { query.pluck(*selects) }
|
|
42
29
|
return results unless block_given?
|
|
30
|
+
attributes.each do |attribute|
|
|
31
|
+
attribute.preload!(results)
|
|
32
|
+
end
|
|
43
33
|
benchmark("map(#{query.table_name})") { yield results }
|
|
44
34
|
end
|
|
45
35
|
|
|
@@ -81,6 +71,7 @@ module PluckMap
|
|
|
81
71
|
|
|
82
72
|
# On Rails 4.2, `pluck` can't accept Arel nodes
|
|
83
73
|
select = Arel.sql(select.to_sql) if ActiveRecord.version.segments.take(2) == [4,2] && select.respond_to?(:to_sql)
|
|
74
|
+
|
|
84
75
|
select
|
|
85
76
|
}
|
|
86
77
|
end
|
|
@@ -89,10 +80,5 @@ module PluckMap
|
|
|
89
80
|
attributes.by_id
|
|
90
81
|
end
|
|
91
82
|
|
|
92
|
-
def keys
|
|
93
|
-
puts "DEPRECATION WARNING: PluckMap::Presenter#keys is deprecated; use #selects instead"
|
|
94
|
-
selects
|
|
95
|
-
end
|
|
96
|
-
|
|
97
83
|
end
|
|
98
84
|
end
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
require "pluck_map/errors"
|
|
2
|
+
|
|
1
3
|
module PluckMap
|
|
2
4
|
module CsvPresenter
|
|
3
5
|
|
|
4
|
-
def
|
|
6
|
+
def self.included(base)
|
|
7
|
+
def base.to_csv(query, **kargs)
|
|
8
|
+
new(query).to_csv(**kargs)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_csv
|
|
13
|
+
if attributes.nested?
|
|
14
|
+
raise PluckMap::UnsupportedAttributeError, "to_csv can not be used to present nested attributes"
|
|
15
|
+
end
|
|
16
|
+
|
|
5
17
|
define_to_csv!
|
|
6
|
-
to_csv
|
|
18
|
+
to_csv
|
|
7
19
|
end
|
|
8
20
|
|
|
9
21
|
private def define_to_csv!
|
|
@@ -11,8 +23,8 @@ module PluckMap
|
|
|
11
23
|
|
|
12
24
|
headers = CSV.generate_line(attributes.map(&:name))
|
|
13
25
|
ruby = <<-RUBY
|
|
14
|
-
def to_csv
|
|
15
|
-
pluck
|
|
26
|
+
def to_csv
|
|
27
|
+
pluck do |results|
|
|
16
28
|
rows = [#{headers.inspect}]
|
|
17
29
|
results.each_with_object(rows) do |values, rows|
|
|
18
30
|
values = Array(values)
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
module PluckMap
|
|
2
2
|
module HashPresenter
|
|
3
3
|
|
|
4
|
-
def
|
|
4
|
+
def self.included(base)
|
|
5
|
+
def base.to_h(query, **kargs)
|
|
6
|
+
new(query).to_h(**kargs)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_h
|
|
5
11
|
define_to_h!
|
|
6
|
-
to_h
|
|
12
|
+
to_h
|
|
7
13
|
end
|
|
8
14
|
|
|
9
15
|
private def define_to_h!
|
|
10
16
|
ruby = <<-RUBY
|
|
11
|
-
def to_h
|
|
12
|
-
pluck
|
|
17
|
+
def to_h
|
|
18
|
+
pluck do |results|
|
|
13
19
|
results.map { |values| values = Array(values); { #{attributes.map { |attribute| "#{attribute.name.inspect} => #{attribute.to_ruby}" }.join(", ")} } }
|
|
14
20
|
end
|
|
15
21
|
end
|
|
@@ -3,12 +3,88 @@ require "json"
|
|
|
3
3
|
module PluckMap
|
|
4
4
|
module JsonPresenter
|
|
5
5
|
|
|
6
|
-
def
|
|
7
|
-
|
|
6
|
+
def self.included(base)
|
|
7
|
+
def base.to_json(query, **kargs)
|
|
8
|
+
new(query).to_json(**kargs)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_json(json: default_json, **)
|
|
13
|
+
if attributes.will_map?
|
|
14
|
+
to_json__default(json: json)
|
|
15
|
+
else
|
|
16
|
+
to_json__optimized
|
|
17
|
+
end
|
|
8
18
|
end
|
|
9
19
|
|
|
10
20
|
private
|
|
11
21
|
|
|
22
|
+
def to_json__default(json: default_json, **)
|
|
23
|
+
json.dump(to_h)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_json__optimized(**)
|
|
27
|
+
define_to_json__optimized!
|
|
28
|
+
to_json__optimized
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def define_to_json__optimized!
|
|
32
|
+
sql = compile(to_json_object(attributes).as("object"))
|
|
33
|
+
|
|
34
|
+
ruby = <<-RUBY
|
|
35
|
+
private def to_json__optimized(**)
|
|
36
|
+
sql = wrap_aggregate(query.select(Arel.sql(#{sql.inspect})))
|
|
37
|
+
query.connection.select_value(sql)
|
|
38
|
+
end
|
|
39
|
+
RUBY
|
|
40
|
+
# puts "\e[34m#{ruby}\e[0m" # <-- helps debugging PluckMapPresenter
|
|
41
|
+
class_eval ruby, __FILE__, __LINE__ - ruby.length
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_json_object(attributes)
|
|
45
|
+
args = []
|
|
46
|
+
attributes.each do |attribute|
|
|
47
|
+
args << Arel::Nodes::Quoted.new(attribute.name)
|
|
48
|
+
args << send(:"prepare_#{attribute.class.name.gsub("::", "_")}", attribute)
|
|
49
|
+
end
|
|
50
|
+
PluckMap::BuildJsonObject.new(*args)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prepare_PluckMap_Attribute(attribute)
|
|
54
|
+
return Arel::Nodes::Quoted.new(attribute.value) if attribute.value?
|
|
55
|
+
arg = attribute.selects[0]
|
|
56
|
+
arg = attribute.model.arel_table[arg] if arg.is_a?(Symbol)
|
|
57
|
+
arg
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prepare_PluckMap_StructuredAttribute(attribute)
|
|
61
|
+
to_json_object(attribute.attributes)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def prepare_PluckMap_Relationships_Many(attribute)
|
|
65
|
+
PluckMap::JsonSubqueryAggregate.new(attribute.scope, to_json_object(attribute.attributes))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def prepare_PluckMap_Relationships_One(attribute)
|
|
69
|
+
Arel.sql("(#{attribute.scope.select(to_json_object(attribute.attributes)).to_sql})")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def wrap_aggregate(subquery)
|
|
73
|
+
"SELECT #{compile(aggregate(Arel.sql("d.object")))} FROM (#{subquery.to_sql}) AS d"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def aggregate(object)
|
|
77
|
+
PluckMap::JsonArrayAggregate.new(object)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def compile(node)
|
|
81
|
+
visitor.compile(node)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def visitor
|
|
85
|
+
model.connection.visitor
|
|
86
|
+
end
|
|
87
|
+
|
|
12
88
|
def default_json
|
|
13
89
|
if defined?(MultiJson)
|
|
14
90
|
MultiJson
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "pluck_map/association_scope"
|
|
2
|
+
require "pluck_map/relationships/base"
|
|
3
|
+
require "pluck_map/relationships/many"
|
|
4
|
+
require "pluck_map/relationships/one"
|
|
5
|
+
require "pluck_map/relationships/polymorphic_one"
|
|
6
|
+
|
|
7
|
+
module PluckMap
|
|
8
|
+
module Relationships
|
|
9
|
+
class << self
|
|
10
|
+
|
|
11
|
+
def one(model, name, block, options)
|
|
12
|
+
reflection = reflection_for(model, name)
|
|
13
|
+
if reflection.polymorphic?
|
|
14
|
+
Relationships::PolymorphicOne.new(name, reflection, block, options)
|
|
15
|
+
else
|
|
16
|
+
Relationships::One.new(name, scope_for_reflection(reflection), block, options)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def many(model, name, block, options)
|
|
21
|
+
Relationships::Many.new(name, scope(model, name), block, options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def scope(model, name)
|
|
27
|
+
scope_for_reflection(reflection_for(model, name))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def scope_for_reflection(reflection)
|
|
31
|
+
scope_for(association_for(reflection))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reflection_for(model, name)
|
|
35
|
+
# Use `_reflections.fetch(name)` instead of `reflect_on_association(name)`
|
|
36
|
+
# because they have different behavior when it comes to HasAndBelongsToMany
|
|
37
|
+
# associations.
|
|
38
|
+
#
|
|
39
|
+
# `reflect_on_association` will return a HasAndBelongsToManyReflection
|
|
40
|
+
# while `_reflections.fetch(name)` will return a ThroughReflection that
|
|
41
|
+
# wraps a HasAndBelongsToManyReflection.
|
|
42
|
+
#
|
|
43
|
+
# ActiveRecord::Associations::AssociationScope expects the latter.
|
|
44
|
+
#
|
|
45
|
+
model._reflections.fetch(name.to_s) do
|
|
46
|
+
raise ArgumentError, "#{name} is not an association on #{model}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def association_for(reflection)
|
|
51
|
+
owner = AbstractOwner.new(reflection)
|
|
52
|
+
reflection.association_class.new(owner, reflection)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def scope_for(association)
|
|
56
|
+
default_scope_for(association).merge(AssociationScope.create.scope(association))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def default_scope_for(association)
|
|
60
|
+
association.klass.all
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ActiveRecord constructs an Association from a Reflection and an
|
|
64
|
+
# Owner. It expects Owner to be an instance of an ActiveRecord object
|
|
65
|
+
# and uses `[]` to access specific values for fields on the record.
|
|
66
|
+
#
|
|
67
|
+
# e.g. WHERE books.author_id = 7
|
|
68
|
+
#
|
|
69
|
+
# We want to create a subquery that will reference those fields
|
|
70
|
+
# but not their specific values.
|
|
71
|
+
#
|
|
72
|
+
# e.g. WHERE books.author_id = authors.id
|
|
73
|
+
#
|
|
74
|
+
# So we create an object that serves the purpose of Owner but returns
|
|
75
|
+
# appropriate selectors.
|
|
76
|
+
#
|
|
77
|
+
AbstractOwner = Struct.new(:reflection) do
|
|
78
|
+
def class
|
|
79
|
+
reflection.active_record
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def [](value)
|
|
83
|
+
self.class.arel_table[value]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "pluck_map/structured_attribute"
|
|
2
|
+
|
|
3
|
+
module PluckMap
|
|
4
|
+
module Relationships
|
|
5
|
+
class Base < StructuredAttribute
|
|
6
|
+
attr_reader :scope
|
|
7
|
+
|
|
8
|
+
def initialize(attribute_name, scope, block, options)
|
|
9
|
+
@scope = scope
|
|
10
|
+
@scope = @scope.instance_exec(&options[:scope_block]) if options[:scope_block]
|
|
11
|
+
super(attribute_name, scope.klass, block, options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def build_select
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_map
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "active_record/version"
|
|
2
|
+
require "pluck_map/attribute"
|
|
3
|
+
|
|
4
|
+
module PluckMap
|
|
5
|
+
module Relationships
|
|
6
|
+
class Many < Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def build_select
|
|
10
|
+
node = PluckMap::JsonSubqueryAggregate.new(scope, attributes.to_json_array)
|
|
11
|
+
|
|
12
|
+
# On Rails 4.2, `pluck` can't accept Arel nodes
|
|
13
|
+
if ActiveRecord.version.segments.take(2) == [4,2]
|
|
14
|
+
Arel.sql(scope.connection.visitor.compile(node))
|
|
15
|
+
else
|
|
16
|
+
node
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_map
|
|
21
|
+
lambda do |results|
|
|
22
|
+
return [] if results.nil?
|
|
23
|
+
results = JSON.parse(results) if results.is_a?(String)
|
|
24
|
+
results.map do |values|
|
|
25
|
+
attributes.each_with_object({}) do |attribute, hash|
|
|
26
|
+
hash[attribute.name] = attribute.exec(values)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "pluck_map/attribute"
|
|
2
|
+
|
|
3
|
+
module PluckMap
|
|
4
|
+
module Relationships
|
|
5
|
+
class One < Base
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def build_select
|
|
9
|
+
Arel.sql("(#{scope.select(attributes.to_json_array).to_sql})")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_map
|
|
13
|
+
lambda do |values|
|
|
14
|
+
return nil if values.nil?
|
|
15
|
+
values = JSON.parse(values) if values.is_a?(String)
|
|
16
|
+
attributes.each_with_object({}) do |attribute, hash|
|
|
17
|
+
hash[attribute.name] = attribute.exec(values)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "pluck_map/attribute"
|
|
2
|
+
|
|
3
|
+
module PluckMap
|
|
4
|
+
module Relationships
|
|
5
|
+
class PolymorphicOne < Attribute
|
|
6
|
+
|
|
7
|
+
def initialize(attribute_name, reflection, block, options)
|
|
8
|
+
@reflection = reflection
|
|
9
|
+
@attributes_block = block
|
|
10
|
+
@scope_block = options[:scope_block]
|
|
11
|
+
|
|
12
|
+
options = options.slice(:as).merge(
|
|
13
|
+
select: [ reflection.foreign_type.to_sym, reflection.foreign_key.to_sym ],
|
|
14
|
+
map: ->(*args) { @preloads[args] })
|
|
15
|
+
|
|
16
|
+
super(attribute_name, reflection.active_record, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def nested?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def preload!(results)
|
|
24
|
+
ids_by_type = Hash.new { |hash, key| hash[key] = [] }
|
|
25
|
+
|
|
26
|
+
results.each do |values|
|
|
27
|
+
type, id = values.values_at(*indexes)
|
|
28
|
+
ids_by_type[type].push(id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@preloads = Hash.new
|
|
32
|
+
ids_by_type.each do |type, ids|
|
|
33
|
+
klass = type.constantize
|
|
34
|
+
scope = klass.where(id: ids)
|
|
35
|
+
scope = scope.instance_exec(&@scope_block) if @scope_block
|
|
36
|
+
|
|
37
|
+
presenter = PluckMap[klass].define do |q|
|
|
38
|
+
q.__id select: klass.primary_key.to_sym
|
|
39
|
+
|
|
40
|
+
if @attributes_block.arity == 1
|
|
41
|
+
@attributes_block.call(q)
|
|
42
|
+
else
|
|
43
|
+
q.instance_eval(&@attributes_block)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
presenter.to_h(scope).each do |h|
|
|
48
|
+
id = h.delete(:__id)
|
|
49
|
+
@preloads[[type, id]] = h
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "pluck_map/attribute"
|
|
2
|
+
|
|
3
|
+
module PluckMap
|
|
4
|
+
class StructuredAttribute < Attribute
|
|
5
|
+
attr_reader :attributes
|
|
6
|
+
|
|
7
|
+
def initialize(attribute_name, model, block, options={})
|
|
8
|
+
@attributes = AttributeBuilder.build(model: model, &block)
|
|
9
|
+
options = options.slice(:as).merge(select: build_select, map: build_map)
|
|
10
|
+
super(attribute_name, model, options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def will_map?
|
|
14
|
+
attributes.any?(&:will_map?)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def nested?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
def build_select
|
|
24
|
+
attributes.selects
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_map
|
|
28
|
+
lambda do |*values|
|
|
29
|
+
return nil if values.none?
|
|
30
|
+
attributes.each_with_object({}) do |attribute, hash|
|
|
31
|
+
hash[attribute.name] = attribute.exec(values)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/pluck_map/version.rb
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "arel/visitors/mysql"
|
|
2
|
+
|
|
3
|
+
module Arel
|
|
4
|
+
module Visitors
|
|
5
|
+
class MySQL
|
|
6
|
+
def visit_PluckMap_BuildJsonObject(o, collector)
|
|
7
|
+
collector << "json_object("
|
|
8
|
+
visit o.args, collector
|
|
9
|
+
collector << ")"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_PluckMap_BuildJsonArray(o, collector)
|
|
13
|
+
collector << "json_array("
|
|
14
|
+
visit o.args, collector
|
|
15
|
+
collector << ")"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_PluckMap_JsonArrayAggregate(o, collector)
|
|
19
|
+
collector << "json_arrayagg("
|
|
20
|
+
visit o.arg, collector
|
|
21
|
+
collector << ")"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_PluckMap_JsonSubqueryAggregate(o, collector)
|
|
25
|
+
interior = compile(o.select)
|
|
26
|
+
if o.scope.order_values.present?
|
|
27
|
+
interior = "#{interior} ORDER BY #{compile(o.scope.order_values)}"
|
|
28
|
+
end
|
|
29
|
+
interior = "CAST(CONCAT('[',GROUP_CONCAT(#{interior}),']') AS JSON)"
|
|
30
|
+
sql = o.scope.reorder(nil).select(Arel.sql(interior)).to_sql
|
|
31
|
+
|
|
32
|
+
collector << "COALESCE((#{sql}), json_array())"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "arel/visitors/postgresql"
|
|
2
|
+
|
|
3
|
+
module Arel
|
|
4
|
+
module Visitors
|
|
5
|
+
class PostgreSQL
|
|
6
|
+
def visit_PluckMap_BuildJsonObject(o, collector)
|
|
7
|
+
collector << "json_build_object("
|
|
8
|
+
visit o.args, collector
|
|
9
|
+
collector << ")"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_PluckMap_BuildJsonArray(o, collector)
|
|
13
|
+
collector << "json_build_array("
|
|
14
|
+
visit o.args, collector
|
|
15
|
+
collector << ")"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_PluckMap_JsonArrayAggregate(o, collector)
|
|
19
|
+
collector << "json_agg("
|
|
20
|
+
visit o.arg, collector
|
|
21
|
+
collector << ")"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_PluckMap_JsonSubqueryAggregate(o, collector)
|
|
25
|
+
sql = o.scope.select(o.select.as("object")).to_sql
|
|
26
|
+
collector << "COALESCE((SELECT json_agg(d.object) FROM (#{sql}) AS d), json_build_array())"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "arel/visitors/sqlite"
|
|
2
|
+
|
|
3
|
+
module Arel
|
|
4
|
+
module Visitors
|
|
5
|
+
class SQLite
|
|
6
|
+
def visit_PluckMap_BuildJsonObject(o, collector)
|
|
7
|
+
collector << "json_object("
|
|
8
|
+
visit o.args, collector
|
|
9
|
+
collector << ")"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_PluckMap_BuildJsonArray(o, collector)
|
|
13
|
+
collector << "json_array("
|
|
14
|
+
visit o.args, collector
|
|
15
|
+
collector << ")"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_PluckMap_JsonArrayAggregate(o, collector)
|
|
19
|
+
collector << "json_group_array(json("
|
|
20
|
+
visit o.arg, collector
|
|
21
|
+
collector << "))"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_PluckMap_JsonSubqueryAggregate(o, collector)
|
|
25
|
+
sql = o.scope.select(o.select.as("object")).to_sql
|
|
26
|
+
collector << "COALESCE((SELECT json_group_array(json(d.object)) FROM (#{sql}) AS d), json_array())"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/pluck_map.gemspec
CHANGED
|
@@ -17,11 +17,14 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
18
18
|
spec.require_paths = ["lib"]
|
|
19
19
|
|
|
20
|
+
# uses `keyword_init` argument of `Struct.new`
|
|
21
|
+
spec.required_ruby_version = '>= 2.5.0'
|
|
22
|
+
|
|
20
23
|
spec.add_dependency "activerecord", ">= 4.2"
|
|
21
24
|
|
|
22
25
|
spec.add_development_dependency "appraisal"
|
|
23
26
|
spec.add_development_dependency "bundler"
|
|
24
|
-
spec.add_development_dependency "rake"
|
|
27
|
+
spec.add_development_dependency "rake"
|
|
25
28
|
spec.add_development_dependency "minitest-reporters"
|
|
26
29
|
spec.add_development_dependency "minitest-reporters-turn_reporter"
|
|
27
30
|
spec.add_development_dependency "sqlite3"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pluck_map
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bob Lail
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -56,16 +56,16 @@ dependencies:
|
|
|
56
56
|
name: rake
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
61
|
+
version: '0'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- - "
|
|
66
|
+
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
68
|
+
version: '0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: minitest-reporters
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -255,19 +255,34 @@ files:
|
|
|
255
255
|
- gemfiles/rails_5.0.gemfile
|
|
256
256
|
- gemfiles/rails_5.1.gemfile
|
|
257
257
|
- gemfiles/rails_5.2.gemfile
|
|
258
|
+
- gemfiles/rails_6.0.gemfile
|
|
258
259
|
- gemfiles/rails_edge.gemfile
|
|
259
260
|
- lib/pluck_map.rb
|
|
261
|
+
- lib/pluck_map/association_scope.rb
|
|
260
262
|
- lib/pluck_map/attribute.rb
|
|
261
263
|
- lib/pluck_map/attribute_builder.rb
|
|
262
264
|
- lib/pluck_map/attributes.rb
|
|
265
|
+
- lib/pluck_map/errors.rb
|
|
263
266
|
- lib/pluck_map/model_context.rb
|
|
267
|
+
- lib/pluck_map/nodes.rb
|
|
264
268
|
- lib/pluck_map/null_logger.rb
|
|
265
269
|
- lib/pluck_map/presenter.rb
|
|
266
270
|
- lib/pluck_map/presenters.rb
|
|
267
271
|
- lib/pluck_map/presenters/to_csv.rb
|
|
268
272
|
- lib/pluck_map/presenters/to_h.rb
|
|
269
273
|
- lib/pluck_map/presenters/to_json.rb
|
|
274
|
+
- lib/pluck_map/relationships.rb
|
|
275
|
+
- lib/pluck_map/relationships/base.rb
|
|
276
|
+
- lib/pluck_map/relationships/many.rb
|
|
277
|
+
- lib/pluck_map/relationships/one.rb
|
|
278
|
+
- lib/pluck_map/relationships/polymorphic_one.rb
|
|
279
|
+
- lib/pluck_map/struct.rb
|
|
280
|
+
- lib/pluck_map/structured_attribute.rb
|
|
270
281
|
- lib/pluck_map/version.rb
|
|
282
|
+
- lib/pluck_map/visitors.rb
|
|
283
|
+
- lib/pluck_map/visitors/mysql.rb
|
|
284
|
+
- lib/pluck_map/visitors/postgresql.rb
|
|
285
|
+
- lib/pluck_map/visitors/sqlite.rb
|
|
271
286
|
- pluck_map.gemspec
|
|
272
287
|
homepage: https://github.com/boblail/pluck_map
|
|
273
288
|
licenses: []
|
|
@@ -280,15 +295,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
280
295
|
requirements:
|
|
281
296
|
- - ">="
|
|
282
297
|
- !ruby/object:Gem::Version
|
|
283
|
-
version:
|
|
298
|
+
version: 2.5.0
|
|
284
299
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
285
300
|
requirements:
|
|
286
301
|
- - ">="
|
|
287
302
|
- !ruby/object:Gem::Version
|
|
288
303
|
version: '0'
|
|
289
304
|
requirements: []
|
|
290
|
-
|
|
291
|
-
rubygems_version: 2.7.6
|
|
305
|
+
rubygems_version: 3.1.4
|
|
292
306
|
signing_key:
|
|
293
307
|
specification_version: 4
|
|
294
308
|
summary: A DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord
|