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.
@@ -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'