active_model_serializers_pg 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module ActiveModelSerializersPg
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1 @@
1
+ require 'active_model_serializers/adapter/json_api_pg'