active_model_serializers_pg 0.0.3 → 0.0.4
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
|
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
|