active_model_serializers_pg 0.0.1
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 +7 -0
- data/.gitignore +25 -0
- data/.rspec +1 -0
- data/.travis.yml +21 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +111 -0
- data/Rakefile +134 -0
- data/active_model_serializers_pg.gemspec +36 -0
- data/gemfiles/Gemfile.activerecord-5.0.x +5 -0
- data/gemfiles/Gemfile.activerecord-5.1.x +5 -0
- data/gemfiles/Gemfile.activerecord-5.2.x +5 -0
- data/lib/active_model_serializers/adapter/json_api_pg.rb +691 -0
- data/lib/active_model_serializers_pg/version.rb +3 -0
- data/lib/active_model_serializers_pg.rb +1 -0
- data/spec/serializer_spec.rb +579 -0
- data/spec/spec_helper.rb +181 -0
- metadata +201 -0
@@ -0,0 +1,691 @@
|
|
1
|
+
require 'active_model_serializers'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
class Serializer
|
5
|
+
class CollectionSerializer
|
6
|
+
def element_serializer
|
7
|
+
# TODO: This is probably not set every time
|
8
|
+
options[:serializer]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ActiveModelSerializers
|
15
|
+
module Adapter
|
16
|
+
class JsonApiPg < Base
|
17
|
+
|
18
|
+
def initialize(serializer, options={})
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_json(options={})
|
23
|
+
connection.select_value serializer_sql
|
24
|
+
end
|
25
|
+
|
26
|
+
def relation
|
27
|
+
@relation ||= _relation
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def connection
|
33
|
+
@connection ||= relation.connection
|
34
|
+
end
|
35
|
+
|
36
|
+
def _relation
|
37
|
+
o = serializer.object
|
38
|
+
case o
|
39
|
+
when ActiveRecord::Relation
|
40
|
+
o
|
41
|
+
when Array
|
42
|
+
# TODO: determine what class it is, even if the array is empty
|
43
|
+
o.first.class.where(id: o.map(&:id))
|
44
|
+
when ActiveRecord::Base
|
45
|
+
o.class.where(id: o.id)
|
46
|
+
else
|
47
|
+
raise "not sure what to do with #{o.class}: #{o}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def serializer_sql
|
52
|
+
# TODO: There should be a better way....
|
53
|
+
opts = serializer.instance_variable_get("@options") || {}
|
54
|
+
sql = JsonApiPgSql.new(serializer, relation, instance_options, opts)
|
55
|
+
sql = sql.to_sql
|
56
|
+
sql
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Each JsonThing is a struct
|
64
|
+
# collecting all the stuff we need to know
|
65
|
+
# about a model you want in the JSONAPI output.
|
66
|
+
#
|
67
|
+
# It has the ActiveRecord class,
|
68
|
+
# the name of the thing,
|
69
|
+
# and how to reach it from its parent.
|
70
|
+
#
|
71
|
+
# The full_name param should be a dotted path
|
72
|
+
# like you'd pass to the `includes` option of ActiveModelSerializers,
|
73
|
+
# except it should *also* start with the name of the top-level entity.
|
74
|
+
#
|
75
|
+
# The reflection should be from the perspective of the parent,
|
76
|
+
# i.e. how you got here, not how you'd leave:
|
77
|
+
# "Reflection" seems to be the internal ActiveRecord lingo
|
78
|
+
# for a belongs_to or has_many relationship.
|
79
|
+
# (The public documentation calls these "associations",
|
80
|
+
# I think think older versions of Rails even used that internally,
|
81
|
+
# but nowadays the method names use "reflection".)
|
82
|
+
class JsonThing
|
83
|
+
attr_reader :ar_class, :full_name, :name, :serializer, :serializer_options, :json_key, :json_type, :reflection, :parent, :cte_name
|
84
|
+
delegate :table_name, :primary_key, to: :ar_class
|
85
|
+
delegate :foreign_key, :belongs_to?, :has_many?, :has_one?, to: :reflection
|
86
|
+
|
87
|
+
def initialize(ar_class, full_name, serializer=nil, serializer_options={}, reflection=nil, parent_json_thing=nil)
|
88
|
+
@ar_class = ar_class
|
89
|
+
@full_name = full_name
|
90
|
+
@name = full_name.split('.').last
|
91
|
+
@serializer = serializer || ActiveModel::Serializer.serializer_for(ar_class.new, {})
|
92
|
+
@serializer_options = serializer_options
|
93
|
+
|
94
|
+
# json_key and json_type might be the same thing, but not always.
|
95
|
+
# json_key is the name of the belongs_to/has_many association,
|
96
|
+
# and json_type is the name of the thing's class.
|
97
|
+
@json_key = JsonThing.json_key(name)
|
98
|
+
@json_type = JsonThing.json_key(ar_class.name.underscore.pluralize)
|
99
|
+
|
100
|
+
@reflection = reflection
|
101
|
+
@parent = parent_json_thing
|
102
|
+
|
103
|
+
@cte_name = _cte_name
|
104
|
+
@sql_methods = {}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Constructs another JsonThing with this one as the parent, via `reflection_name`.
|
108
|
+
# TODO: tests
|
109
|
+
def from_reflection(reflection_name)
|
110
|
+
refl = JsonApiReflection.new(reflection_name, ar_class, serializer)
|
111
|
+
JsonThing.new(refl.klass, "#{full_name}.#{reflection_name}", nil, serializer_options, refl, self)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Gets the attributes (i.e. scalar fields) on the AR class
|
115
|
+
# as a Set of symbols.
|
116
|
+
# TODO: tests
|
117
|
+
def declared_attributes
|
118
|
+
@declared_attributes ||= Set.new(@ar_class.attribute_types.keys.map(&:to_sym))
|
119
|
+
end
|
120
|
+
|
121
|
+
def enum?(field)
|
122
|
+
@ar_class.attribute_types[field.to_s].is_a? ActiveRecord::Enum::EnumType
|
123
|
+
end
|
124
|
+
|
125
|
+
# Gets the reflections (aka associations) of the AR class
|
126
|
+
# as a Hash from symbol to a subclass of ActiveRecord::Reflection.
|
127
|
+
# TODO: tests
|
128
|
+
def declared_reflections
|
129
|
+
@declared_reflections ||= Hash[
|
130
|
+
@ar_class.reflections.map{|k, v|
|
131
|
+
[k.to_sym, v]
|
132
|
+
}
|
133
|
+
]
|
134
|
+
end
|
135
|
+
|
136
|
+
# TODO: tests
|
137
|
+
def self.json_key(k)
|
138
|
+
# TODO: technically the serializer could have an option overriding the default:
|
139
|
+
case ActiveModelSerializers.config.key_transform
|
140
|
+
when :dash
|
141
|
+
k.to_s.gsub('_', '-')
|
142
|
+
else
|
143
|
+
k.to_s
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def has_sql_method?(field)
|
148
|
+
sql_method(field).present?
|
149
|
+
end
|
150
|
+
|
151
|
+
def sql_method(field)
|
152
|
+
(@sql_methods[field] ||= _sql_method(field))[0]
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# This needs to be globally unique within the SQL query,
|
158
|
+
# even if the same model class appears in different places
|
159
|
+
# (e.g. a Book has_many :authors and has_many :reviewers,
|
160
|
+
# but those are both of class User).
|
161
|
+
# So we use the full_name to prevent conflicts.
|
162
|
+
# But since Postgres table names have limited length,
|
163
|
+
# we also hash that name to guarantee something short
|
164
|
+
# (like how Rails migrations generate foreign key names).
|
165
|
+
# TODO: tests
|
166
|
+
def _cte_name
|
167
|
+
if parent.nil?
|
168
|
+
't'
|
169
|
+
else
|
170
|
+
"cte_#{Digest::SHA256.hexdigest(full_name).first(10)}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def _sql_method(field)
|
175
|
+
m = "#{field}__sql".to_sym
|
176
|
+
if ar_class.respond_to?(m)
|
177
|
+
# We return an array so our caller can cache a negative result too:
|
178
|
+
[ar_class.send(m)]
|
179
|
+
elsif serializer.instance_methods.include? m
|
180
|
+
ser = serializer.new(ar_class.new, serializer_options)
|
181
|
+
[ser.send(m)]
|
182
|
+
else
|
183
|
+
[nil]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
# Wraps what we know about a reflection.
|
190
|
+
# Includes the ActiveRecord::Reflection,
|
191
|
+
# the ActiveModel::Serializer::Reflection,
|
192
|
+
# and the JsonApiReflectionReceiver results
|
193
|
+
# (i.e. the contents of a has_many block from the serializer definition).
|
194
|
+
class JsonApiReflection
|
195
|
+
|
196
|
+
attr_reader :name, :original_name, :ar_reflection, :serializer_reflection, :include_data, :links,
|
197
|
+
:reflection_sql, :ar_class, :klass
|
198
|
+
delegate :foreign_key, to: :ar_reflection
|
199
|
+
|
200
|
+
# `ar_class` should be the *source* ActiveRecord class,
|
201
|
+
# so that `ar_class.name` is one or more things of `klass`.
|
202
|
+
def initialize(name, ar_class, serializer_class)
|
203
|
+
@name = name
|
204
|
+
@ar_class = ar_class
|
205
|
+
@original_name = @ar_class.instance_method(name).original_name
|
206
|
+
|
207
|
+
@serializer_reflection = serializer_class._reflections[name.to_sym]
|
208
|
+
|
209
|
+
@ar_reflection = ar_class.reflections[name.to_s]
|
210
|
+
@reflection_sql = nil
|
211
|
+
if @ar_reflection.nil?
|
212
|
+
# See if it's an alias:
|
213
|
+
@ar_reflection = ar_class.reflections[@original_name.to_s]
|
214
|
+
end
|
215
|
+
if @ar_reflection.nil?
|
216
|
+
m = "#{name}__sql".to_sym
|
217
|
+
if ar_class.respond_to? m
|
218
|
+
rel = ar_class.send(m)
|
219
|
+
# Must be an ActiveRecord::Relation (or ActiveModel::Base) so we can determine klass
|
220
|
+
@reflection_sql = rel
|
221
|
+
@klass = ActiveRecord::Relation === rel ? rel.klass : rel
|
222
|
+
else
|
223
|
+
raise "Can't find an association named #{name} for class #{ar_class.name}"
|
224
|
+
end
|
225
|
+
else
|
226
|
+
@klass = @ar_reflection.klass
|
227
|
+
end
|
228
|
+
@include_data = true
|
229
|
+
@links = {}
|
230
|
+
|
231
|
+
if serializer_reflection.try(:block).present?
|
232
|
+
x = JsonApiReflectionReceiver.new(serializer_class)
|
233
|
+
x.instance_eval &serializer_reflection.block
|
234
|
+
@include_data = x.result_include_data
|
235
|
+
@links = x.result_links
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def belongs_to?
|
240
|
+
ar_reflection.is_a? ActiveRecord::Reflection::BelongsToReflection
|
241
|
+
# TODO: fall back to AMS reflection
|
242
|
+
end
|
243
|
+
|
244
|
+
def has_many?
|
245
|
+
ar_reflection.try(:is_a?, ActiveRecord::Reflection::HasManyReflection) ||
|
246
|
+
serializer_reflection.is_a?(ActiveModel::Serializer::HasManyReflection)
|
247
|
+
end
|
248
|
+
|
249
|
+
def has_one?
|
250
|
+
ar_reflection.is_a? ActiveRecord::Reflection::HasOneReflection
|
251
|
+
# TODO: fall back to AMS reflection
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
# We use this when a serializer has a reflection with a block argument,
|
257
|
+
# like this:
|
258
|
+
#
|
259
|
+
# has_many :users do
|
260
|
+
# include_data false
|
261
|
+
# link(:related) { users_company_path(object) }
|
262
|
+
# end
|
263
|
+
#
|
264
|
+
# The only way to find out what options get set in that block is to run it,
|
265
|
+
# so this class does that and records what is there.
|
266
|
+
class JsonApiReflectionReceiver
|
267
|
+
include ActiveModelSerializers::SerializationContext::UrlHelpers
|
268
|
+
|
269
|
+
attr_reader :serializer, :result_include_data, :result_links
|
270
|
+
|
271
|
+
def initialize(serializer)
|
272
|
+
@serializer = serializer
|
273
|
+
@result_include_data = true
|
274
|
+
@result_links = {}
|
275
|
+
end
|
276
|
+
|
277
|
+
def include_data(val)
|
278
|
+
@result_include_data = val
|
279
|
+
end
|
280
|
+
|
281
|
+
# Users may pass either a string or a block,
|
282
|
+
# so we accept both.
|
283
|
+
def link(name, val=nil, &block)
|
284
|
+
if not val.nil?
|
285
|
+
@result_links[name] = val
|
286
|
+
else
|
287
|
+
lnk = ActiveModelSerializers::Adapter::JsonApi::Link.new(serializer.new(object), block)
|
288
|
+
# TODO: Super hacky here, and only supports one level of path resources:
|
289
|
+
template = lnk.as_json
|
290
|
+
@result_links[name] = template.split("PARAM").map{|p| "'#{p}'"}
|
291
|
+
# @result_links[name] = "CONCAT(template
|
292
|
+
# @result_links[name] = instance_eval(&block)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def object
|
297
|
+
# TODO: Could even be a singleton
|
298
|
+
JsonApiObjectProxy.new
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
class JsonApiObjectProxy
|
304
|
+
def to_param
|
305
|
+
"PARAM"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
class JsonApiPgSql
|
310
|
+
attr_reader :base_serializer, :base_relation
|
311
|
+
|
312
|
+
def initialize(base_serializer, base_relation, instance_options, options)
|
313
|
+
@base_relation = base_relation
|
314
|
+
@instance_options = instance_options
|
315
|
+
@options = options
|
316
|
+
|
317
|
+
# Make a JsonThing for everything,
|
318
|
+
# cached as the full_name:
|
319
|
+
|
320
|
+
# User.where is a Relation, but plain User is not:
|
321
|
+
ar_class = ActiveRecord::Relation === base_relation ? base_relation.klass : base_relation
|
322
|
+
|
323
|
+
case base_serializer
|
324
|
+
when ActiveModel::Serializer::CollectionSerializer
|
325
|
+
# base_serializer = base_serializer.to_a.first
|
326
|
+
base_serializer = base_serializer.element_serializer
|
327
|
+
@many = true
|
328
|
+
else
|
329
|
+
base_serializer = base_serializer.class
|
330
|
+
@many = false
|
331
|
+
end
|
332
|
+
base_serializer ||= ActiveModel::Serializer.serializer_for(ar_class.new, options)
|
333
|
+
@base_serializer = base_serializer
|
334
|
+
|
335
|
+
base_name = ar_class.name.underscore.pluralize
|
336
|
+
base_thing = JsonThing.new(ar_class, base_name, base_serializer, options)
|
337
|
+
@fields_for = {}
|
338
|
+
@attribute_fields_for = {}
|
339
|
+
@reflection_fields_for = {}
|
340
|
+
@json_things = {
|
341
|
+
base: base_thing, # `base` is a sym but every other key is a string
|
342
|
+
}
|
343
|
+
@json_things[base_name] = base_thing
|
344
|
+
# We don't need to add anything else to @json_things yet
|
345
|
+
# because we'll lazy-build it via get_json_thing.
|
346
|
+
# That lets us go as deep in the relationships as we need
|
347
|
+
# without loading anything extra.
|
348
|
+
end
|
349
|
+
|
350
|
+
def get_json_thing(resource, field)
|
351
|
+
refl_name = "#{resource.full_name}.#{field}"
|
352
|
+
@json_things[refl_name] ||= resource.from_reflection(field)
|
353
|
+
end
|
354
|
+
|
355
|
+
def many?
|
356
|
+
@many
|
357
|
+
end
|
358
|
+
|
359
|
+
def json_key(name)
|
360
|
+
JsonThing.json_key(name)
|
361
|
+
end
|
362
|
+
|
363
|
+
# Given a JsonThing and the fields you want,
|
364
|
+
# outputs the json column for a SQL SELECT clause.
|
365
|
+
def select_resource_attributes(resource)
|
366
|
+
fields = attribute_fields_for(resource)
|
367
|
+
<<~EOQ
|
368
|
+
jsonb_build_object(#{fields.map{|f| "'#{json_key(f)}', #{select_resource_attribute(resource, f)}"}.join(', ')})
|
369
|
+
EOQ
|
370
|
+
end
|
371
|
+
|
372
|
+
# Returns SQL for one JSON value for the resource's 'attributes' object.
|
373
|
+
# If a field is an enum then we convert it from an int to a string.
|
374
|
+
# If a field has a #{field}__sql method on the ActiveRecord class,
|
375
|
+
# we use that instead.
|
376
|
+
def select_resource_attribute(resource, field)
|
377
|
+
typ = resource.ar_class.attribute_types[field.to_s]
|
378
|
+
if typ.is_a? ActiveRecord::Enum::EnumType
|
379
|
+
<<~EOQ
|
380
|
+
CASE #{typ.as_json['mapping'].map{|str, int| %Q{WHEN "#{resource.table_name}"."#{field}" = #{int} THEN '#{str}'}}.join("\n ")} END
|
381
|
+
EOQ
|
382
|
+
elsif resource.has_sql_method?(field)
|
383
|
+
resource.sql_method(field)
|
384
|
+
else
|
385
|
+
%Q{"#{resource.table_name}"."#{field}"}
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def select_resource_relationship_links(resource, reflection)
|
390
|
+
reflection.links.map {|link_name, link_parts|
|
391
|
+
<<~EOQ
|
392
|
+
'#{link_name}', CONCAT(#{link_parts.join(%Q{, "#{resource.parent.table_name}"."#{resource.parent.primary_key}", })})
|
393
|
+
EOQ
|
394
|
+
}.join(",\n")
|
395
|
+
end
|
396
|
+
|
397
|
+
def select_resource_relationship(resource)
|
398
|
+
if resource.belongs_to?
|
399
|
+
fk = %Q{"#{resource.parent.table_name}"."#{resource.foreign_key}"}
|
400
|
+
<<~EOQ
|
401
|
+
'#{resource.json_key}',
|
402
|
+
jsonb_build_object('data',
|
403
|
+
CASE WHEN #{fk} IS NULL THEN NULL
|
404
|
+
ELSE jsonb_build_object('id', #{fk}::text,
|
405
|
+
'type', '#{resource.json_type}') END)
|
406
|
+
EOQ
|
407
|
+
elsif resource.has_many? or resource.has_one?
|
408
|
+
refl = resource.reflection
|
409
|
+
<<~EOQ
|
410
|
+
'#{resource.json_key}',
|
411
|
+
jsonb_build_object(#{refl.include_data ? %Q{'data', rel_#{resource.cte_name}.j} : ''}
|
412
|
+
#{refl.include_data && refl.links.any? ? ',' : ''}
|
413
|
+
#{refl.links.any? ? %Q{'links', jsonb_build_object(#{select_resource_relationship_links(resource, refl)})} : ''})
|
414
|
+
EOQ
|
415
|
+
else
|
416
|
+
raise "Unknown kind of field reflection for #{resource.full_name}"
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def select_resource_relationships(resource)
|
421
|
+
fields = reflection_fields_for(resource)
|
422
|
+
children = fields.map{|f| get_json_thing(resource, f)}
|
423
|
+
if children.any?
|
424
|
+
<<~EOQ
|
425
|
+
jsonb_build_object(#{children.map{|ch| select_resource_relationship(ch)}.join(', ')})
|
426
|
+
EOQ
|
427
|
+
else
|
428
|
+
nil
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def join_resource_relationships(resource)
|
433
|
+
fields = reflection_fields_for(resource)
|
434
|
+
fields.map{|f|
|
435
|
+
child_resource = get_json_thing(resource, f)
|
436
|
+
refl = child_resource.reflection
|
437
|
+
if refl.has_many?
|
438
|
+
if refl.ar_reflection.present?
|
439
|
+
# Preserve ordering options, either from the AR association itself
|
440
|
+
# or from the class's default scope.
|
441
|
+
# TODO: preserve the whole custom relation, not just ordering
|
442
|
+
p = refl.ar_class.new
|
443
|
+
ordering = nil
|
444
|
+
ActiveSupport::Deprecation.silence do
|
445
|
+
# TODO: Calling `orders` prints deprecation warnings, so find another way:
|
446
|
+
ordering = p.send(refl.name).orders
|
447
|
+
ordering = child_resource.ar_class.default_scoped.orders if ordering.empty?
|
448
|
+
end
|
449
|
+
ordering = ordering.map{|o|
|
450
|
+
case o
|
451
|
+
# TODO: The gsub is pretty awful....
|
452
|
+
when Arel::Nodes::Ordering
|
453
|
+
o.to_sql.gsub("\"#{child_resource.table_name}\"", "rel")
|
454
|
+
when String
|
455
|
+
o
|
456
|
+
else
|
457
|
+
raise "Unknown type of ordering: #{o.inspect}"
|
458
|
+
end
|
459
|
+
}.join(', ').presence
|
460
|
+
ordering = "ORDER BY #{ordering}" if ordering
|
461
|
+
<<~EOQ
|
462
|
+
LEFT OUTER JOIN LATERAL (
|
463
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object('id', rel."#{child_resource.primary_key}"::text,
|
464
|
+
'type', '#{child_resource.json_type}') #{ordering}), '[]') AS j
|
465
|
+
FROM "#{child_resource.table_name}" rel
|
466
|
+
WHERE rel."#{child_resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"
|
467
|
+
) "rel_#{child_resource.cte_name}" ON true
|
468
|
+
EOQ
|
469
|
+
elsif not refl.reflection_sql.nil? # can't use .present? since that loads the Relation!
|
470
|
+
case refl.reflection_sql
|
471
|
+
when String
|
472
|
+
raise "TODO"
|
473
|
+
when ActiveRecord::Relation
|
474
|
+
rel = refl.reflection_sql
|
475
|
+
sql = rel.select(<<~EOQ).to_sql
|
476
|
+
coalesce(jsonb_agg(jsonb_build_object('id', "#{child_resource.table_name}"."#{child_resource.primary_key}"::text,
|
477
|
+
'type', '#{child_resource.json_type}')), '[]') AS j
|
478
|
+
EOQ
|
479
|
+
<<~EOQ
|
480
|
+
LEFT OUTER JOIN LATERAL (
|
481
|
+
#{sql}
|
482
|
+
) "rel_#{child_resource.cte_name}" ON true
|
483
|
+
EOQ
|
484
|
+
end
|
485
|
+
end
|
486
|
+
elsif refl.has_one?
|
487
|
+
<<~EOQ
|
488
|
+
LEFT OUTER JOIN LATERAL (
|
489
|
+
SELECT jsonb_build_object('id', rel."#{child_resource.primary_key}"::text,
|
490
|
+
'type', '#{child_resource.json_type}') AS j
|
491
|
+
FROM "#{child_resource.table_name}" rel
|
492
|
+
WHERE rel."#{child_resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"
|
493
|
+
) "rel_#{child_resource.cte_name}" ON true
|
494
|
+
EOQ
|
495
|
+
else
|
496
|
+
nil
|
497
|
+
end
|
498
|
+
}.compact.join("\n")
|
499
|
+
end
|
500
|
+
|
501
|
+
def include_selects
|
502
|
+
@include_selects ||= includes.map {|inc|
|
503
|
+
th = get_json_thing_from_base(inc)
|
504
|
+
# TODO: UNION ALL would be faster than UNION,
|
505
|
+
# but then we still need to de-dupe when we have two paths to the same table,
|
506
|
+
# e.g. buyer and seller for User.
|
507
|
+
# But we could group those and union just them, or even better do a DISTINCT ON (id).
|
508
|
+
# Since we don't get the id here that could be another CTE.
|
509
|
+
"UNION SELECT j FROM #{th.cte_name}"
|
510
|
+
}
|
511
|
+
end
|
512
|
+
|
513
|
+
def include_cte_join_condition(resource)
|
514
|
+
parent = resource.parent
|
515
|
+
if resource.belongs_to?
|
516
|
+
%Q{#{parent.cte_name}."#{resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"}
|
517
|
+
elsif resource.has_many? or resource.has_one?
|
518
|
+
%Q{#{parent.cte_name}."#{parent.primary_key}" = "#{resource.table_name}"."#{resource.foreign_key}"}
|
519
|
+
else
|
520
|
+
raise "not supported relationship: #{resource.full_name}"
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def include_cte(resource)
|
525
|
+
# Sometimes options[:fields] has plural keys and sometimes singular,
|
526
|
+
# so try both:
|
527
|
+
parent = resource.parent
|
528
|
+
<<~EOQ
|
529
|
+
SELECT DISTINCT ON ("#{resource.table_name}"."#{resource.primary_key}")
|
530
|
+
"#{resource.table_name}".*,
|
531
|
+
#{select_resource(resource)} AS j
|
532
|
+
FROM "#{resource.table_name}"
|
533
|
+
JOIN #{parent.cte_name}
|
534
|
+
ON #{include_cte_join_condition(resource)}
|
535
|
+
#{join_resource_relationships(resource)}
|
536
|
+
ORDER BY "#{resource.table_name}"."#{resource.primary_key}"
|
537
|
+
EOQ
|
538
|
+
end
|
539
|
+
|
540
|
+
def includes
|
541
|
+
@instance_options[:include] || []
|
542
|
+
end
|
543
|
+
|
544
|
+
# Takes a dotted field name (not including the base resource)
|
545
|
+
# like we might find in options[:include],
|
546
|
+
# and builds up all the JsonThings needed to get to the end.
|
547
|
+
def get_json_thing_from_base(field)
|
548
|
+
r = base_resource
|
549
|
+
field.split('.').each do |f|
|
550
|
+
r = get_json_thing(r, f)
|
551
|
+
end
|
552
|
+
r
|
553
|
+
end
|
554
|
+
|
555
|
+
def include_ctes
|
556
|
+
includes.map { |inc|
|
557
|
+
# Be careful: inc might have dots:
|
558
|
+
th = get_json_thing_from_base(inc)
|
559
|
+
<<~EOQ
|
560
|
+
#{th.cte_name} AS (
|
561
|
+
#{include_cte(th)}
|
562
|
+
),
|
563
|
+
EOQ
|
564
|
+
}.join("\n")
|
565
|
+
end
|
566
|
+
|
567
|
+
def base_resource
|
568
|
+
@json_things[:base]
|
569
|
+
end
|
570
|
+
|
571
|
+
def maybe_select_resource_relationships(resource)
|
572
|
+
rels_sql = select_resource_relationships(resource)
|
573
|
+
if rels_sql.nil?
|
574
|
+
''
|
575
|
+
else
|
576
|
+
%Q{, 'relationships', #{rels_sql}}
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
def select_resource(resource)
|
581
|
+
fields = fields_for(resource)
|
582
|
+
<<~EOQ
|
583
|
+
jsonb_build_object('id', "#{resource.table_name}"."#{resource.primary_key}"::text,
|
584
|
+
'type', '#{resource.json_type}',
|
585
|
+
'attributes', #{select_resource_attributes(resource)}
|
586
|
+
#{maybe_select_resource_relationships(resource)})
|
587
|
+
EOQ
|
588
|
+
end
|
589
|
+
|
590
|
+
# Returns all the attributes listed in the serializer,
|
591
|
+
# after checking `include_foo?` methods.
|
592
|
+
def serializer_attributes(resource)
|
593
|
+
ms = Set.new(resource.serializer.instance_methods)
|
594
|
+
resource.serializer._attributes.select{|f|
|
595
|
+
if ms.include? "include_#{f}?".to_sym
|
596
|
+
ser = resource.serializer.new(nil, @options)
|
597
|
+
ser.send("include_#{f}?".to_sym) # TODO: call the method
|
598
|
+
else
|
599
|
+
true
|
600
|
+
end
|
601
|
+
}
|
602
|
+
end
|
603
|
+
|
604
|
+
# Returns all the relationships listed in the serializer,
|
605
|
+
# after checking `include_foo?` methods.
|
606
|
+
def serializer_reflections(resource)
|
607
|
+
ms = Set.new(resource.serializer.instance_methods)
|
608
|
+
resource.serializer._reflections.keys.select{|f|
|
609
|
+
if ms.include? "include_#{f}?".to_sym
|
610
|
+
ser = resource.serializer.new(nil, @options)
|
611
|
+
ser.send("include_#{f}?".to_sym) # TODO: call the method
|
612
|
+
else
|
613
|
+
true
|
614
|
+
end
|
615
|
+
}
|
616
|
+
end
|
617
|
+
|
618
|
+
def fields_for(resource)
|
619
|
+
@fields_for[resource.full_name] ||= _fields_for(resource)
|
620
|
+
end
|
621
|
+
|
622
|
+
def _fields_for(resource)
|
623
|
+
# Sometimes options[:fields] has plural keys and sometimes singular,
|
624
|
+
# so try both:
|
625
|
+
resource_key = resource.json_type.to_sym
|
626
|
+
fields = @instance_options.dig :fields, resource_key
|
627
|
+
if fields.nil?
|
628
|
+
resource_key = resource.json_type.singularize.to_sym
|
629
|
+
fields = @instance_options.dig :fields, resource_key
|
630
|
+
end
|
631
|
+
if fields.nil?
|
632
|
+
# If the user didn't request specific fields, then give them all that appear in the serializer:
|
633
|
+
fields = serializer_attributes(resource).to_a + serializer_reflections(resource).to_a
|
634
|
+
end
|
635
|
+
fields
|
636
|
+
end
|
637
|
+
|
638
|
+
def attribute_fields_for(resource)
|
639
|
+
@attribute_fields_for[resource.full_name] ||= _attribute_fields_for(resource)
|
640
|
+
end
|
641
|
+
|
642
|
+
def _attribute_fields_for(resource)
|
643
|
+
attrs = Set.new(serializer_attributes(resource))
|
644
|
+
fields_for(resource).select { |f| attrs.include? f }.to_a
|
645
|
+
end
|
646
|
+
|
647
|
+
def reflection_fields_for(resource)
|
648
|
+
@reflection_fields_for[resource.full_name] ||= _reflection_fields_for(resource)
|
649
|
+
end
|
650
|
+
|
651
|
+
def _reflection_fields_for(resource)
|
652
|
+
refls = Set.new(serializer_reflections(resource))
|
653
|
+
fields_for(resource).select { |f| refls.include? f }.to_a
|
654
|
+
end
|
655
|
+
|
656
|
+
def to_sql
|
657
|
+
table_name = base_resource.table_name
|
658
|
+
maybe_included = if include_selects.any?
|
659
|
+
%Q{, 'included', inc.j}
|
660
|
+
else
|
661
|
+
''
|
662
|
+
end
|
663
|
+
return <<~EOQ
|
664
|
+
WITH
|
665
|
+
t AS (
|
666
|
+
#{base_relation.select(%Q{"#{base_resource.table_name}".*}).to_sql}
|
667
|
+
),
|
668
|
+
t2 AS (
|
669
|
+
#{many? ? "SELECT COALESCE(jsonb_agg(#{select_resource(base_resource)}), '[]') AS j"
|
670
|
+
: "SELECT #{select_resource(base_resource)} AS j"}
|
671
|
+
FROM t AS "#{base_resource.table_name}"
|
672
|
+
#{join_resource_relationships(base_resource)}
|
673
|
+
),
|
674
|
+
#{include_ctes}
|
675
|
+
all_ctes AS (
|
676
|
+
SELECT '{}'::jsonb AS j
|
677
|
+
WHERE 1=0
|
678
|
+
#{include_selects.join("\n")}
|
679
|
+
),
|
680
|
+
inc AS (
|
681
|
+
SELECT COALESCE(jsonb_agg(j), '[]') AS j
|
682
|
+
FROM all_ctes
|
683
|
+
)
|
684
|
+
SELECT jsonb_build_object('data', t2.j
|
685
|
+
#{maybe_included})
|
686
|
+
FROM t2
|
687
|
+
CROSS JOIN inc
|
688
|
+
EOQ
|
689
|
+
end
|
690
|
+
|
691
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_model_serializers/adapter/json_api_pg'
|