active_model_serializers_pg 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63d2090a2d7e9e33d8565156023d5c3dc75ea74c77baade84c5c0727d3ab917f
|
4
|
+
data.tar.gz: d1abc37c7a6dade9d9bacb7584b5e630f81652a87d28849dacc6bcbb5291c7d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec68d79eacdcbb257753fa6636236b406e2a57012aa68aba4d8f1ff564bd072653cfc6da4daa07ebe17bce5be0913d6e93da7cadccf6c4d637467d4026cdc21c
|
7
|
+
data.tar.gz: 0a7726480564582f54afcc91b8f7666ceb8416a772a8b34623b252723bb36b545a19491894b07b06372cbbfc5313473e8b12f80899e4c7d6705bdaef10ac8235
|
@@ -8,7 +8,7 @@ module ActiveModel
|
|
8
8
|
class Serializer
|
9
9
|
class CollectionSerializer
|
10
10
|
def element_serializer
|
11
|
-
options[:serializer]
|
11
|
+
options && options[:serializer]
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -23,7 +23,16 @@ module ActiveModelSerializers
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def to_json(options={})
|
26
|
-
|
26
|
+
if relation.nil?
|
27
|
+
ret = { data: [] }
|
28
|
+
if includes.any?
|
29
|
+
# TODO: Can included ever be non-empty when the main data is empty?
|
30
|
+
ret[:included] = []
|
31
|
+
end
|
32
|
+
ret.to_json
|
33
|
+
else
|
34
|
+
connection.select_value serializer_sql
|
35
|
+
end
|
27
36
|
end
|
28
37
|
|
29
38
|
def relation
|
@@ -32,6 +41,10 @@ module ActiveModelSerializers
|
|
32
41
|
|
33
42
|
private
|
34
43
|
|
44
|
+
def includes
|
45
|
+
instance_options && instance_options[:include] || []
|
46
|
+
end
|
47
|
+
|
35
48
|
def connection
|
36
49
|
@connection ||= relation.connection
|
37
50
|
end
|
@@ -42,8 +55,12 @@ module ActiveModelSerializers
|
|
42
55
|
when ActiveRecord::Relation
|
43
56
|
o
|
44
57
|
when Array
|
45
|
-
|
46
|
-
|
58
|
+
o2 = o.first
|
59
|
+
if o2.nil?
|
60
|
+
nil
|
61
|
+
else
|
62
|
+
o2.class.where(id: o.map(&:id))
|
63
|
+
end
|
47
64
|
when ActiveRecord::Base
|
48
65
|
o.class.where(id: o.id)
|
49
66
|
else
|
@@ -92,7 +109,7 @@ end
|
|
92
109
|
# I think think older versions of Rails even used that internally,
|
93
110
|
# but nowadays the method names use "reflection".)
|
94
111
|
class JsonThing
|
95
|
-
attr_reader :ar_class, :full_name, :name, :serializer, :serializer_options, :json_key, :json_type, :reflection, :parent, :cte_name
|
112
|
+
attr_reader :ar_class, :full_name, :name, :serializer, :serializer_options, :json_key, :json_type, :reflection, :parent, :cte_name, :jbs_name
|
96
113
|
delegate :table_name, :primary_key, to: :ar_class
|
97
114
|
delegate :foreign_key, :belongs_to?, :has_many?, :has_one?, to: :reflection
|
98
115
|
|
@@ -113,6 +130,7 @@ class JsonThing
|
|
113
130
|
@parent = parent_json_thing
|
114
131
|
|
115
132
|
@cte_name = _cte_name
|
133
|
+
@jbs_name = _jbs_name
|
116
134
|
@sql_methods = {}
|
117
135
|
end
|
118
136
|
|
@@ -145,6 +163,15 @@ class JsonThing
|
|
145
163
|
]
|
146
164
|
end
|
147
165
|
|
166
|
+
# Checks for alias_attribute and gets to the real attribute name.
|
167
|
+
def unaliased(field_name)
|
168
|
+
ret = field_name
|
169
|
+
while field_name = @ar_class.attribute_aliases[field_name.to_s]
|
170
|
+
ret = field_name
|
171
|
+
end
|
172
|
+
ret
|
173
|
+
end
|
174
|
+
|
148
175
|
# TODO: tests
|
149
176
|
def self.json_key(k)
|
150
177
|
# TODO: technically the serializer could have an option overriding the default:
|
@@ -179,10 +206,38 @@ class JsonThing
|
|
179
206
|
if parent.nil?
|
180
207
|
't'
|
181
208
|
else
|
182
|
-
"cte_#{Digest::SHA256.hexdigest(full_name).first(10)}"
|
209
|
+
"cte_#{_cte_name_human_part}_#{Digest::SHA256.hexdigest(full_name).first(10)}"
|
183
210
|
end
|
184
211
|
end
|
185
212
|
|
213
|
+
# Each thing has bother a `cte_foo` CTE and a `jbs_foo` CTE.
|
214
|
+
# (jbs stands for "JSONBs" and is meant to take 3 chars like `cte`.)
|
215
|
+
# The former is just the relevant records,
|
216
|
+
# and the second builds the JSON object for each record.
|
217
|
+
# We need to split things into phases like this
|
218
|
+
# because of the JSON:API `relationships` item,
|
219
|
+
# which can contain references in *both directions*.
|
220
|
+
# In that case Postgres will object to our circular dependency.
|
221
|
+
# But with two phases, every jbs_* CTE only depends on cte_* CTEs.
|
222
|
+
def _jbs_name
|
223
|
+
if parent.nil?
|
224
|
+
't'
|
225
|
+
else
|
226
|
+
"jbs_#{_cte_name_human_part}_#{Digest::SHA256.hexdigest(full_name).first(10)}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Gets a more informative name for the CTE based on the include key.
|
231
|
+
# This makes reading the big SQL query easier, especially for debugging.
|
232
|
+
# Note that Postgres's max identifier length is 63 chars (unless you compile yourself),
|
233
|
+
# and we are spending 4+4+11=19 chars elsewhere on `rel_cte_XXX_1234567890`.
|
234
|
+
# So this method can't return more than 63-19=44 chars.
|
235
|
+
#
|
236
|
+
# Since we quote the CTE names, we don't actually need to remove dots in the name!
|
237
|
+
def _cte_name_human_part
|
238
|
+
@cte_name_human_part ||= full_name[0, 44]
|
239
|
+
end
|
240
|
+
|
186
241
|
def _sql_method(field)
|
187
242
|
m = "#{field}__sql".to_sym
|
188
243
|
if ar_class.respond_to?(m)
|
@@ -397,7 +452,7 @@ class JsonApiPgSql
|
|
397
452
|
elsif resource.has_sql_method?(field)
|
398
453
|
resource.sql_method(field)
|
399
454
|
else
|
400
|
-
%Q{"#{resource.table_name}"."#{field}"}
|
455
|
+
%Q{"#{resource.table_name}"."#{resource.unaliased(field)}"}
|
401
456
|
end
|
402
457
|
end
|
403
458
|
|
@@ -423,7 +478,7 @@ class JsonApiPgSql
|
|
423
478
|
refl = resource.reflection
|
424
479
|
<<~EOQ
|
425
480
|
'#{resource.json_key}',
|
426
|
-
jsonb_build_object(#{refl.include_data ? %Q{'data', rel_#{resource.cte_name}.j} : ''}
|
481
|
+
jsonb_build_object(#{refl.include_data ? %Q{'data', "rel_#{resource.cte_name}".j} : ''}
|
427
482
|
#{refl.include_data && refl.links.any? ? ',' : ''}
|
428
483
|
#{refl.links.any? ? %Q{'links', jsonb_build_object(#{select_resource_relationship_links(resource, refl)})} : ''})
|
429
484
|
EOQ
|
@@ -521,39 +576,51 @@ class JsonApiPgSql
|
|
521
576
|
# e.g. buyer and seller for User.
|
522
577
|
# But we could group those and union just them, or even better do a DISTINCT ON (id).
|
523
578
|
# Since we don't get the id here that could be another CTE.
|
524
|
-
|
579
|
+
%Q{UNION SELECT j FROM "#{th.jbs_name}"}
|
525
580
|
}
|
526
581
|
end
|
527
582
|
|
528
583
|
def include_cte_join_condition(resource)
|
529
584
|
parent = resource.parent
|
530
585
|
if resource.belongs_to?
|
531
|
-
%Q{#{parent.cte_name}."#{resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"}
|
586
|
+
%Q{"#{parent.cte_name}"."#{resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"}
|
532
587
|
elsif resource.has_many? or resource.has_one?
|
533
|
-
%Q{#{parent.cte_name}."#{parent.primary_key}" = "#{resource.table_name}"."#{resource.foreign_key}"}
|
588
|
+
%Q{"#{parent.cte_name}"."#{parent.primary_key}" = "#{resource.table_name}"."#{resource.foreign_key}"}
|
534
589
|
else
|
535
590
|
raise "not supported relationship: #{resource.full_name}"
|
536
591
|
end
|
537
592
|
end
|
538
593
|
|
594
|
+
# See note in _jbs_name method for why we split each thing into two CTEs.
|
539
595
|
def include_cte(resource)
|
540
596
|
# Sometimes options[:fields] has plural keys and sometimes singular,
|
541
597
|
# so try both:
|
542
598
|
parent = resource.parent
|
543
599
|
<<~EOQ
|
544
600
|
SELECT DISTINCT ON ("#{resource.table_name}"."#{resource.primary_key}")
|
545
|
-
"#{resource.table_name}"
|
546
|
-
#{select_resource(resource)} AS j
|
601
|
+
"#{resource.table_name}".*
|
547
602
|
FROM "#{resource.table_name}"
|
548
|
-
JOIN #{parent.cte_name}
|
603
|
+
JOIN "#{parent.cte_name}"
|
549
604
|
ON #{include_cte_join_condition(resource)}
|
550
|
-
#{join_resource_relationships(resource)}
|
551
605
|
ORDER BY "#{resource.table_name}"."#{resource.primary_key}"
|
552
606
|
EOQ
|
553
607
|
end
|
554
608
|
|
609
|
+
# See note in _jbs_name method for why we split each thing into two CTEs.
|
610
|
+
def include_jbs(resource)
|
611
|
+
<<~EOQ
|
612
|
+
SELECT "#{resource.table_name}".*,
|
613
|
+
#{select_resource(resource)} AS j
|
614
|
+
FROM "#{resource.cte_name}" AS "#{resource.table_name}"
|
615
|
+
#{join_resource_relationships(resource)}
|
616
|
+
EOQ
|
617
|
+
end
|
618
|
+
|
555
619
|
def includes
|
556
|
-
@instance_options[:include] || []
|
620
|
+
@includes ||= (@instance_options[:include] || []).sort_by do |inc|
|
621
|
+
# Sort these by length so we never have bad foreign references in the CTEs:
|
622
|
+
inc.size
|
623
|
+
end
|
557
624
|
end
|
558
625
|
|
559
626
|
# Takes a dotted field name (not including the base resource)
|
@@ -572,13 +639,25 @@ class JsonApiPgSql
|
|
572
639
|
# Be careful: inc might have dots:
|
573
640
|
th = get_json_thing_from_base(inc)
|
574
641
|
<<~EOQ
|
575
|
-
#{th.cte_name} AS (
|
642
|
+
"#{th.cte_name}" AS (
|
576
643
|
#{include_cte(th)}
|
577
644
|
),
|
578
645
|
EOQ
|
579
646
|
}.join("\n")
|
580
647
|
end
|
581
648
|
|
649
|
+
def include_jbsses
|
650
|
+
includes.map { |inc|
|
651
|
+
# Be careful: inc might have dots:
|
652
|
+
th = get_json_thing_from_base(inc)
|
653
|
+
<<~EOQ
|
654
|
+
"#{th.jbs_name}" AS (
|
655
|
+
#{include_jbs(th)}
|
656
|
+
),
|
657
|
+
EOQ
|
658
|
+
}.join("\n")
|
659
|
+
end
|
660
|
+
|
582
661
|
def base_resource
|
583
662
|
@json_things[:base]
|
584
663
|
end
|
@@ -687,14 +766,15 @@ class JsonApiPgSql
|
|
687
766
|
#{join_resource_relationships(base_resource)}
|
688
767
|
),
|
689
768
|
#{include_ctes}
|
690
|
-
|
769
|
+
#{include_jbsses}
|
770
|
+
all_jbsses AS (
|
691
771
|
SELECT '{}'::jsonb AS j
|
692
772
|
WHERE 1=0
|
693
773
|
#{include_selects.join("\n")}
|
694
774
|
),
|
695
775
|
inc AS (
|
696
776
|
SELECT COALESCE(jsonb_agg(j), '[]') AS j
|
697
|
-
FROM
|
777
|
+
FROM all_jbsses
|
698
778
|
)
|
699
779
|
SELECT jsonb_build_object('data', t2.j
|
700
780
|
#{maybe_included})
|
data/spec/serializer_spec.rb
CHANGED
@@ -113,6 +113,42 @@ describe 'ArraySerializer' do
|
|
113
113
|
end
|
114
114
|
end
|
115
115
|
|
116
|
+
context 'serialize an array instead of a relation' do
|
117
|
+
let(:relation) { [Note.where(name: 'Title').first] }
|
118
|
+
let(:controller) { NotesController.new }
|
119
|
+
let(:options) { }
|
120
|
+
|
121
|
+
before do
|
122
|
+
@note = Note.create content: 'Test', name: 'Title'
|
123
|
+
@tag = Tag.create name: 'My tag', note: @note, popular: true
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'generates the proper json output' do
|
127
|
+
json_expected = {
|
128
|
+
data: [
|
129
|
+
{
|
130
|
+
id: @note.id.to_s,
|
131
|
+
type: 'notes',
|
132
|
+
attributes: { name: 'Title', content: 'Test' },
|
133
|
+
relationships: { tags: { data: [{id: @tag.id.to_s, type: 'tags'}] } }
|
134
|
+
}
|
135
|
+
]
|
136
|
+
}.to_json
|
137
|
+
expect(json_data).to eq json_expected
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'serialize an empty array' do
|
142
|
+
let(:relation) { [] }
|
143
|
+
let(:controller) { NotesController.new }
|
144
|
+
let(:options) { }
|
145
|
+
|
146
|
+
it 'generates the proper json output' do
|
147
|
+
json_expected = { data: [] }.to_json
|
148
|
+
expect(json_data).to eq json_expected
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
116
152
|
context 'serialize singular record' do
|
117
153
|
let(:relation) { Note.where(name: 'Title').first }
|
118
154
|
let(:controller) { NotesController.new }
|
@@ -134,7 +170,6 @@ describe 'ArraySerializer' do
|
|
134
170
|
}.to_json
|
135
171
|
expect(json_data).to eq json_expected
|
136
172
|
end
|
137
|
-
|
138
173
|
end
|
139
174
|
|
140
175
|
context 'with dasherized keys and types' do
|
@@ -195,7 +230,29 @@ describe 'ArraySerializer' do
|
|
195
230
|
}.to_json
|
196
231
|
expect(json_data).to eq json_expected
|
197
232
|
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context 'with aliased attribute' do
|
236
|
+
let(:relation) { Tag.first }
|
237
|
+
let(:controller) { TagsController.new }
|
238
|
+
let(:options) { { serializer: TagWithAliasedNameSerializer } }
|
198
239
|
|
240
|
+
before do
|
241
|
+
@note = Note.create content: 'Test', name: 'Title'
|
242
|
+
@tag = Tag.create name: 'My tag', note: @note, popular: true
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'generates the proper json output' do
|
246
|
+
json_expected = {
|
247
|
+
data: {
|
248
|
+
id: @tag.id.to_s,
|
249
|
+
type: 'tags',
|
250
|
+
attributes: { 'aliased_name' => 'My tag' },
|
251
|
+
relationships: { note: { data: {id: @note.id.to_s, type: 'notes'} } }
|
252
|
+
}
|
253
|
+
}.to_json
|
254
|
+
expect(json_data).to eq json_expected
|
255
|
+
end
|
199
256
|
end
|
200
257
|
|
201
258
|
context 'serialize single record with custom serializer' do
|
data/spec/spec_helper.rb
CHANGED
@@ -126,6 +126,7 @@ end
|
|
126
126
|
class Tag < ActiveRecord::Base
|
127
127
|
belongs_to :note
|
128
128
|
alias :aliased_note :note
|
129
|
+
alias_attribute :aliased_name, :name
|
129
130
|
end
|
130
131
|
|
131
132
|
class SortedTag < Tag
|
@@ -164,6 +165,11 @@ class TagWithAliasedNoteSerializer < ActiveModel::Serializer
|
|
164
165
|
has_one :aliased_note
|
165
166
|
end
|
166
167
|
|
168
|
+
class TagWithAliasedNameSerializer < ActiveModel::Serializer
|
169
|
+
attributes :aliased_name
|
170
|
+
has_one :note
|
171
|
+
end
|
172
|
+
|
167
173
|
class User < ActiveRecord::Base
|
168
174
|
has_many :offers, foreign_key: :created_by_id, inverse_of: :created_by
|
169
175
|
has_many :reviewed_offers, foreign_key: :reviewed_by_id, inverse_of: :reviewed_by, class_name: 'Offer'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_model_serializers_pg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul A. Jungwirth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-10-
|
11
|
+
date: 2019-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: active_model_serializers
|