jsonapi-resources 0.9.12 → 0.10.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +303 -0
  8. data/lib/jsonapi/active_relation_resource.rb +884 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -106
  10. data/lib/jsonapi/basic_resource.rb +1162 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +127 -0
  12. data/lib/jsonapi/compiled_json.rb +11 -1
  13. data/lib/jsonapi/configuration.rb +57 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +63 -40
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -75
  19. data/lib/jsonapi/link_builder.rb +18 -25
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +73 -15
  22. data/lib/jsonapi/paginator.rb +17 -0
  23. data/lib/jsonapi/path.rb +43 -0
  24. data/lib/jsonapi/path_segment.rb +76 -0
  25. data/lib/jsonapi/processor.rb +246 -111
  26. data/lib/jsonapi/relationship.rb +117 -24
  27. data/lib/jsonapi/request_parser.rb +383 -396
  28. data/lib/jsonapi/resource.rb +3 -1376
  29. data/lib/jsonapi/resource_controller_metal.rb +3 -0
  30. data/lib/jsonapi/resource_fragment.rb +47 -0
  31. data/lib/jsonapi/resource_id_tree.rb +112 -0
  32. data/lib/jsonapi/resource_identity.rb +42 -0
  33. data/lib/jsonapi/resource_serializer.rb +124 -286
  34. data/lib/jsonapi/resource_set.rb +176 -0
  35. data/lib/jsonapi/resources/railtie.rb +9 -0
  36. data/lib/jsonapi/resources/version.rb +1 -1
  37. data/lib/jsonapi/response_document.rb +104 -87
  38. data/lib/jsonapi/routing_ext.rb +19 -21
  39. data/lib/jsonapi-resources.rb +20 -4
  40. data/lib/tasks/check_upgrade.rake +52 -0
  41. metadata +34 -31
  42. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  43. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  44. data/lib/jsonapi/operation_results.rb +0 -35
  45. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -1,1378 +1,5 @@
1
- require 'jsonapi/callbacks'
2
- require 'jsonapi/relationship_builder'
3
-
4
1
  module JSONAPI
5
- class Resource
6
- include Callbacks
7
-
8
- DEFAULT_ATTRIBUTE_OPTIONS = { format: :default }.freeze
9
- MODULE_PATH_REGEXP = /::[^:]+\Z/.freeze
10
-
11
- attr_reader :context
12
-
13
- define_jsonapi_resources_callbacks :create,
14
- :update,
15
- :remove,
16
- :save,
17
- :create_to_many_link,
18
- :replace_to_many_links,
19
- :create_to_one_link,
20
- :replace_to_one_link,
21
- :replace_polymorphic_to_one_link,
22
- :remove_to_many_link,
23
- :remove_to_one_link,
24
- :replace_fields
25
-
26
- def initialize(model, context)
27
- @model = model
28
- @context = context
29
- @reload_needed = false
30
- @changing = false
31
- @save_needed = false
32
- end
33
-
34
- def _model
35
- @model
36
- end
37
-
38
- def id
39
- _model.public_send(self.class._primary_key)
40
- end
41
-
42
- def cache_id
43
- [id, _model.public_send(self.class._cache_field)]
44
- end
45
-
46
- def is_new?
47
- id.nil?
48
- end
49
-
50
- def change(callback)
51
- completed = false
52
-
53
- if @changing
54
- run_callbacks callback do
55
- completed = (yield == :completed)
56
- end
57
- else
58
- run_callbacks is_new? ? :create : :update do
59
- @changing = true
60
- run_callbacks callback do
61
- completed = (yield == :completed)
62
- end
63
-
64
- completed = (save == :completed) if @save_needed || is_new?
65
- end
66
- end
67
-
68
- return completed ? :completed : :accepted
69
- end
70
-
71
- def remove
72
- run_callbacks :remove do
73
- _remove
74
- end
75
- end
76
-
77
- def create_to_many_links(relationship_type, relationship_key_values, options = {})
78
- change :create_to_many_link do
79
- _create_to_many_links(relationship_type, relationship_key_values, options)
80
- end
81
- end
82
-
83
- def replace_to_many_links(relationship_type, relationship_key_values, options = {})
84
- change :replace_to_many_links do
85
- _replace_to_many_links(relationship_type, relationship_key_values, options)
86
- end
87
- end
88
-
89
- def replace_to_one_link(relationship_type, relationship_key_value, options = {})
90
- change :replace_to_one_link do
91
- _replace_to_one_link(relationship_type, relationship_key_value, options)
92
- end
93
- end
94
-
95
- def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {})
96
- change :replace_polymorphic_to_one_link do
97
- _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options)
98
- end
99
- end
100
-
101
- def remove_to_many_link(relationship_type, key, options = {})
102
- change :remove_to_many_link do
103
- _remove_to_many_link(relationship_type, key, options)
104
- end
105
- end
106
-
107
- def remove_to_one_link(relationship_type, options = {})
108
- change :remove_to_one_link do
109
- _remove_to_one_link(relationship_type, options)
110
- end
111
- end
112
-
113
- def replace_fields(field_data)
114
- change :replace_fields do
115
- _replace_fields(field_data)
116
- end
117
- end
118
-
119
- # Override this on a resource instance to override the fetchable keys
120
- def fetchable_fields
121
- self.class.fields
122
- end
123
-
124
- # Override this on a resource to customize how the associated records
125
- # are fetched for a model. Particularly helpful for authorization.
126
- def records_for(relation_name)
127
- _model.public_send relation_name
128
- end
129
-
130
- def model_error_messages
131
- _model.errors.messages
132
- end
133
-
134
- # Add metadata to validation error objects.
135
- #
136
- # Suppose `model_error_messages` returned the following error messages
137
- # hash:
138
- #
139
- # {password: ["too_short", "format"]}
140
- #
141
- # Then to add data to the validation error `validation_error_metadata`
142
- # could return:
143
- #
144
- # {
145
- # password: {
146
- # "too_short": {"minimum_length" => 6},
147
- # "format": {"requirement" => "must contain letters and numbers"}
148
- # }
149
- # }
150
- #
151
- # The specified metadata is then be merged into the validation error
152
- # object.
153
- def validation_error_metadata
154
- {}
155
- end
156
-
157
- # Override this to return resource level meta data
158
- # must return a hash, and if the hash is empty the meta section will not be serialized with the resource
159
- # meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
160
- # serializer's format_key and format_value methods if desired
161
- # the _options hash will contain the serializer and the serialization_options
162
- def meta(_options)
163
- {}
164
- end
165
-
166
- # Override this to return custom links
167
- # must return a hash, which will be merged with the default { self: 'self-url' } links hash
168
- # links keys will be not be formatted with the key formatter for the serializer by default.
169
- # They can however use the serializer's format_key and format_value methods if desired
170
- # the _options hash will contain the serializer and the serialization_options
171
- def custom_links(_options)
172
- {}
173
- end
174
-
175
- def preloaded_fragments
176
- # A hash of hashes
177
- @preloaded_fragments ||= Hash.new
178
- end
179
-
180
- private
181
-
182
- def save
183
- run_callbacks :save do
184
- _save
185
- end
186
- end
187
-
188
- # Override this on a resource to return a different result code. Any
189
- # value other than :completed will result in operations returning
190
- # `:accepted`
191
- #
192
- # For example to return `:accepted` if your model does not immediately
193
- # save resources to the database you could override `_save` as follows:
194
- #
195
- # ```
196
- # def _save
197
- # super
198
- # return :accepted
199
- # end
200
- # ```
201
- def _save(validation_context = nil)
202
- unless @model.valid?(validation_context)
203
- fail JSONAPI::Exceptions::ValidationErrors.new(self)
204
- end
205
-
206
- if defined? @model.save
207
- saved = @model.save(validate: false)
208
-
209
- unless saved
210
- if @model.errors.present?
211
- fail JSONAPI::Exceptions::ValidationErrors.new(self)
212
- else
213
- fail JSONAPI::Exceptions::SaveFailed.new
214
- end
215
- end
216
- else
217
- saved = true
218
- end
219
- @model.reload if @reload_needed
220
- @reload_needed = false
221
-
222
- @save_needed = !saved
223
-
224
- :completed
225
- end
226
-
227
- def _remove
228
- unless @model.destroy
229
- fail JSONAPI::Exceptions::ValidationErrors.new(self)
230
- end
231
- :completed
232
-
233
- rescue ActiveRecord::DeleteRestrictionError => e
234
- fail JSONAPI::Exceptions::RecordLocked.new(e.message)
235
- end
236
-
237
- def reflect_relationship?(relationship, options)
238
- return false if !relationship.reflect ||
239
- (!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source])
240
-
241
- inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship]
242
- if inverse_relationship.nil?
243
- warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled."
244
- return false
245
- end
246
- true
247
- end
248
-
249
- def _create_to_many_links(relationship_type, relationship_key_values, options)
250
- relationship = self.class._relationships[relationship_type]
251
-
252
- # check if relationship_key_values are already members of this relationship
253
- relation_name = relationship.relation_name(context: @context)
254
- existing_relations = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_values)
255
- if existing_relations.count > 0
256
- # todo: obscure id so not to leak info
257
- fail JSONAPI::Exceptions::HasManyRelationExists.new(existing_relations.first.id)
258
- end
259
-
260
- if options[:reflected_source]
261
- @model.public_send(relation_name) << options[:reflected_source]._model
262
- return :completed
263
- end
264
-
265
- # load requested related resources
266
- # make sure they all exist (also based on context) and add them to relationship
267
-
268
- related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context)
269
-
270
- if related_resources.count != relationship_key_values.count
271
- # todo: obscure id so not to leak info
272
- fail JSONAPI::Exceptions::RecordNotFound.new('unspecified')
273
- end
274
-
275
- reflect = reflect_relationship?(relationship, options)
276
-
277
- related_resources.each do |related_resource|
278
- if reflect
279
- if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
280
- related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self)
281
- else
282
- related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self)
283
- end
284
- @reload_needed = true
285
- else
286
- @model.public_send(relation_name) << related_resource._model
287
- end
288
- end
289
-
290
- :completed
291
- end
292
-
293
- def _replace_to_many_links(relationship_type, relationship_key_values, options)
294
- relationship = self.class._relationships[relationship_type]
295
-
296
- reflect = reflect_relationship?(relationship, options)
297
-
298
- if reflect
299
- existing = send("#{relationship.foreign_key}")
300
- to_delete = existing - (relationship_key_values & existing)
301
- to_delete.each do |key|
302
- _remove_to_many_link(relationship_type, key, reflected_source: self)
303
- end
304
-
305
- to_add = relationship_key_values - (relationship_key_values & existing)
306
- _create_to_many_links(relationship_type, to_add, {})
307
-
308
- @reload_needed = true
309
- elsif relationship.polymorphic?
310
- relationship_key_values.each do |relationship_key_value|
311
- relationship_resource_klass = self.class.resource_for(relationship_key_value[:type])
312
- ids = relationship_key_value[:ids]
313
-
314
- related_records = relationship_resource_klass
315
- .records(options)
316
- .where({relationship_resource_klass._primary_key => ids})
317
-
318
- missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
319
-
320
- if missed_ids.present?
321
- fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
322
- end
323
-
324
- relation_name = relationship.relation_name(context: @context)
325
- @model.send("#{relation_name}") << related_records
326
- end
327
-
328
- @reload_needed = true
329
- else
330
- send("#{relationship.foreign_key}=", relationship_key_values)
331
- @save_needed = true
332
- end
333
-
334
- :completed
335
- end
336
-
337
- def _replace_to_one_link(relationship_type, relationship_key_value, options)
338
- relationship = self.class._relationships[relationship_type]
339
-
340
- send("#{relationship.foreign_key}=", relationship_key_value)
341
- @save_needed = true
342
-
343
- :completed
344
- end
345
-
346
- def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, options)
347
- relationship = self.class._relationships[relationship_type.to_sym]
348
-
349
- _model.public_send("#{relationship.foreign_key}=", key_value)
350
- _model.public_send("#{relationship.polymorphic_type}=", _model_class_name(key_type))
351
-
352
- @save_needed = true
353
-
354
- :completed
355
- end
356
-
357
- def _remove_to_many_link(relationship_type, key, options)
358
- relationship = self.class._relationships[relationship_type]
359
-
360
- reflect = reflect_relationship?(relationship, options)
361
-
362
- if reflect
363
-
364
- related_resource = relationship.resource_klass.find_by_key(key, context: @context)
365
-
366
- if related_resource.nil?
367
- fail JSONAPI::Exceptions::RecordNotFound.new(key)
368
- else
369
- if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
370
- related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self)
371
- else
372
- related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self)
373
- end
374
- end
375
-
376
- @reload_needed = true
377
- else
378
- @model.public_send(relationship.relation_name(context: @context)).delete(key)
379
- end
380
-
381
- :completed
382
-
383
- rescue ActiveRecord::DeleteRestrictionError => e
384
- fail JSONAPI::Exceptions::RecordLocked.new(e.message)
385
- rescue ActiveRecord::RecordNotFound
386
- fail JSONAPI::Exceptions::RecordNotFound.new(key)
387
- end
388
-
389
- def _remove_to_one_link(relationship_type, options)
390
- relationship = self.class._relationships[relationship_type]
391
-
392
- send("#{relationship.foreign_key}=", nil)
393
- @save_needed = true
394
-
395
- :completed
396
- end
397
-
398
- def _replace_fields(field_data)
399
- field_data[:attributes].each do |attribute, value|
400
- begin
401
- send "#{attribute}=", value
402
- @save_needed = true
403
- rescue ArgumentError
404
- # :nocov: Will be thrown if an enum value isn't allowed for an enum. Currently not tested as enums are a rails 4.1 and higher feature
405
- raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value)
406
- # :nocov:
407
- end
408
- end
409
-
410
- field_data[:to_one].each do |relationship_type, value|
411
- if value.nil?
412
- remove_to_one_link(relationship_type)
413
- else
414
- case value
415
- when Hash
416
- replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
417
- else
418
- replace_to_one_link(relationship_type, value)
419
- end
420
- end
421
- end if field_data[:to_one]
422
-
423
- field_data[:to_many].each do |relationship_type, values|
424
- replace_to_many_links(relationship_type, values)
425
- end if field_data[:to_many]
426
-
427
- :completed
428
- end
429
-
430
- def _model_class_name(key_type)
431
- type_class_name = key_type.to_s.classify
432
- resource = self.class.resource_for(type_class_name)
433
- resource ? resource._model_name.to_s : type_class_name
434
- end
435
-
436
- class << self
437
- def inherited(subclass)
438
- subclass.abstract(false)
439
- subclass.immutable(false)
440
- subclass.caching(false)
441
- subclass.singleton(singleton?, (_singleton_options.dup || {}))
442
- subclass.exclude_links(_exclude_links)
443
- subclass._attributes = (_attributes || {}).dup
444
-
445
- subclass._model_hints = (_model_hints || {}).dup
446
-
447
- unless _model_name.empty?
448
- subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
449
- end
450
-
451
- subclass.rebuild_relationships(_relationships || {})
452
-
453
- subclass._allowed_filters = (_allowed_filters || Set.new).dup
454
-
455
- type = subclass.name.demodulize.sub(/Resource$/, '').underscore
456
- subclass._type = type.pluralize.to_sym
457
-
458
- unless subclass._attributes[:id]
459
- subclass.attribute :id, format: :id
460
- end
461
-
462
- check_reserved_resource_name(subclass._type, subclass.name)
463
-
464
- subclass._routed = false
465
- subclass._warned_missing_route = false
466
- end
467
-
468
- def rebuild_relationships(relationships)
469
- original_relationships = relationships.deep_dup
470
-
471
- @_relationships = {}
472
-
473
- if original_relationships.is_a?(Hash)
474
- original_relationships.each_value do |relationship|
475
- options = relationship.options.dup
476
- options[:parent_resource] = self
477
- _add_relationship(relationship.class, relationship.name, options)
478
- end
479
- end
480
- end
481
-
482
- def resource_for(type)
483
- type = type.underscore
484
- type_with_module = type.start_with?(module_path) ? type : module_path + type
485
-
486
- resource_name = _resource_name_from_type(type_with_module)
487
- resource = resource_name.safe_constantize if resource_name
488
- if resource.nil?
489
- fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
490
- end
491
- resource
492
- end
493
-
494
- def resource_for_model(model)
495
- resource_for(resource_type_for(model))
496
- end
497
-
498
- def _resource_name_from_type(type)
499
- "#{type.to_s.underscore.singularize}_resource".camelize
500
- end
501
-
502
- def resource_type_for(model)
503
- model_name = model.class.to_s.underscore
504
- if _model_hints[model_name]
505
- _model_hints[model_name]
506
- else
507
- model_name.rpartition('/').last
508
- end
509
- end
510
-
511
- attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
512
- attr_writer :_allowed_filters, :_paginator
513
-
514
- def create(context)
515
- new(create_model, context)
516
- end
517
-
518
- def create_model
519
- _model_class.new
520
- end
521
-
522
- def routing_options(options)
523
- @_routing_resource_options = options
524
- end
525
-
526
- def routing_resource_options
527
- @_routing_resource_options ||= {}
528
- end
529
-
530
- # Methods used in defining a resource class
531
- def attributes(*attrs)
532
- options = attrs.extract_options!.dup
533
- attrs.each do |attr|
534
- attribute(attr, options)
535
- end
536
- end
537
-
538
- def attribute(attribute_name, options = {})
539
- attr = attribute_name.to_sym
540
-
541
- check_reserved_attribute_name(attr)
542
-
543
- if (attr == :id) && (options[:format].nil?)
544
- ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
545
- end
546
-
547
- check_duplicate_attribute_name(attr) if options[:format].nil?
548
-
549
- @_attributes ||= {}
550
- @_attributes[attr] = options
551
- define_method attr do
552
- @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
553
- end unless method_defined?(attr)
554
-
555
- define_method "#{attr}=" do |value|
556
- @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
557
- end unless method_defined?("#{attr}=")
558
- end
559
-
560
- def default_attribute_options
561
- DEFAULT_ATTRIBUTE_OPTIONS
562
- end
563
-
564
- def relationship(*attrs)
565
- options = attrs.extract_options!
566
- klass = case options[:to]
567
- when :one
568
- Relationship::ToOne
569
- when :many
570
- Relationship::ToMany
571
- else
572
- #:nocov:#
573
- fail ArgumentError.new('to: must be either :one or :many')
574
- #:nocov:#
575
- end
576
- _add_relationship(klass, *attrs, options.except(:to))
577
- end
578
-
579
- def has_one(*attrs)
580
- _add_relationship(Relationship::ToOne, *attrs)
581
- end
582
-
583
- def belongs_to(*attrs)
584
- ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
585
- " using the `belongs_to` class method. We think `has_one`" \
586
- " is more appropriate. If you know what you're doing," \
587
- " and don't want to see this warning again, override the" \
588
- " `belongs_to` class method on your resource."
589
- _add_relationship(Relationship::ToOne, *attrs)
590
- end
591
-
592
- def has_many(*attrs)
593
- _add_relationship(Relationship::ToMany, *attrs)
594
- end
595
-
596
- # @model_class is inherited from superclass, and this causes some issues:
597
- # ```
598
- # CarResource._model_class #=> Vehicle # it should be Car
599
- # ```
600
- # so in order to invoke the right class from subclasses,
601
- # we should call this method to override it.
602
- def model_name(model, options = {})
603
- @model_class = nil
604
- @_model_name = model.to_sym
605
-
606
- model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
607
-
608
- rebuild_relationships(_relationships)
609
- end
610
-
611
- def model_hint(model: _model_name, resource: _type)
612
- resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
613
-
614
- _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
615
- end
616
-
617
- def singleton(*attrs)
618
- @_singleton = (!!attrs[0] == attrs[0]) ? attrs[0] : true
619
- @_singleton_options = attrs.extract_options!
620
- end
621
-
622
- def _singleton_options
623
- @_singleton_options ||= {}
624
- end
625
-
626
- def singleton?
627
- @_singleton ||= false
628
- end
629
-
630
- def filters(*attrs)
631
- @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
632
- end
633
-
634
- def filter(attr, *args)
635
- @_allowed_filters[attr.to_sym] = args.extract_options!
636
- end
637
-
638
- def primary_key(key)
639
- @_primary_key = key.to_sym
640
- end
641
-
642
- def cache_field(field)
643
- @_cache_field = field.to_sym
644
- end
645
-
646
- # Override in your resource to filter the updatable keys
647
- def updatable_fields(_context = nil)
648
- _updatable_relationships | _attributes.keys - [:id]
649
- end
650
-
651
- # Override in your resource to filter the creatable keys
652
- def creatable_fields(_context = nil)
653
- _updatable_relationships | _attributes.keys - [:id]
654
- end
655
-
656
- # Override in your resource to filter the sortable keys
657
- def sortable_fields(_context = nil)
658
- _attributes.keys
659
- end
660
-
661
- def fields
662
- _relationships.keys | _attributes.keys
663
- end
664
-
665
- def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
666
- case model_includes
667
- when Array
668
- return model_includes.map do |value|
669
- resolve_relationship_names_to_relations(resource_klass, value, options)
670
- end
671
- when Hash
672
- model_includes.keys.each do |key|
673
- relationship = resource_klass._relationships[key]
674
- value = model_includes[key]
675
- model_includes.delete(key)
676
- model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
677
- end
678
- return model_includes
679
- when Symbol
680
- relationship = resource_klass._relationships[model_includes]
681
- return relationship.relation_name(options)
682
- end
683
- end
684
-
685
- def apply_includes(records, options = {})
686
- include_directives = options[:include_directives]
687
- if include_directives
688
- model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
689
- records = records.includes(model_includes) if model_includes.present?
690
- end
691
-
692
- records
693
- end
694
-
695
- def apply_pagination(records, paginator, order_options)
696
- records = paginator.apply(records, order_options) if paginator
697
- records
698
- end
699
-
700
- def apply_sort(records, order_options, _context = {})
701
- if order_options.any?
702
- order_options.each_pair do |field, direction|
703
- if field.to_s.include?(".")
704
- *model_names, column_name = field.split(".")
705
-
706
- associations = _lookup_association_chain([records.model.to_s, *model_names])
707
- joins_query = _build_joins([records.model, *associations])
708
-
709
- # _sorting is appended to avoid name clashes with manual joins eg. overridden filters
710
- order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
711
- records = records.joins(joins_query).order(order_by_query)
712
- else
713
- records = records.order(field => direction)
714
- end
715
- end
716
- end
717
-
718
- records
719
- end
720
-
721
- def _lookup_association_chain(model_names)
722
- associations = []
723
- model_names.inject do |prev, current|
724
- association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
725
- assoc.name.to_s.downcase == current.downcase
726
- end
727
- associations << association
728
- association.class_name
729
- end
730
-
731
- associations
732
- end
733
-
734
- def _build_joins(associations)
735
- joins = []
736
-
737
- associations.inject do |prev, current|
738
- joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
739
- current
740
- end
741
- joins.join("\n")
742
- end
743
-
744
- def apply_filter(records, filter, value, options = {})
745
- strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
746
-
747
- if strategy
748
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
749
- send(strategy, records, value, options)
750
- else
751
- strategy.call(records, value, options)
752
- end
753
- else
754
- records.where(filter => value)
755
- end
756
- end
757
-
758
- def apply_filters(records, filters, options = {})
759
- required_includes = []
760
-
761
- if filters
762
- filters.each do |filter, value|
763
- if _relationships.include?(filter)
764
- if _relationships[filter].belongs_to?
765
- records = apply_filter(records, _relationships[filter].foreign_key, value, options)
766
- else
767
- required_includes.push(filter.to_s)
768
- records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
769
- end
770
- else
771
- records = apply_filter(records, filter, value, options)
772
- end
773
- end
774
- end
775
-
776
- if required_includes.any?
777
- records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
778
- end
779
-
780
- records
781
- end
782
-
783
- def apply_included_resources_filters(records, options = {})
784
- include_directives = options[:include_directives]
785
- return records unless include_directives
786
- related_directives = include_directives.include_directives.fetch(:include_related)
787
- related_directives.reduce(records) do |memo, (relationship_name, config)|
788
- relationship = _relationship(relationship_name)
789
- next memo unless relationship && relationship.is_a?(JSONAPI::Relationship::ToMany)
790
- filtering_resource = relationship.resource_klass
791
-
792
- # Don't try to merge where clauses when relation isn't already being joined to query.
793
- next memo unless config[:include_in_join]
794
-
795
- filters = config[:include_filters]
796
- next memo unless filters
797
-
798
- rel_records = filtering_resource.apply_filters(filtering_resource.records(options), filters, options).references(relationship_name)
799
- memo.merge(rel_records)
800
- end
801
- end
802
-
803
- def filter_records(filters, options, records = records(options))
804
- records = apply_filters(records, filters, options)
805
- records = apply_includes(records, options)
806
- apply_included_resources_filters(records, options)
807
- end
808
-
809
- def sort_records(records, order_options, context = {})
810
- apply_sort(records, order_options, context)
811
- end
812
-
813
- # Assumes ActiveRecord's counting. Override if you need a different counting method
814
- def count_records(records)
815
- records.count(:all)
816
- end
817
-
818
- def find_count(filters, options = {})
819
- count_records(filter_records(filters, options))
820
- end
821
-
822
- def find(filters, options = {})
823
- resources_for(find_records(filters, options), options[:context])
824
- end
825
-
826
- def resources_for(records, context)
827
- records.collect do |model|
828
- resource_class = self.resource_for_model(model)
829
- resource_class.new(model, context)
830
- end
831
- end
832
-
833
- def find_by_keys(keys, options = {})
834
- context = options[:context]
835
- records = records(options)
836
- records = apply_includes(records, options)
837
- models = records.where({_primary_key => keys})
838
- models.collect do |model|
839
- self.resource_for_model(model).new(model, context)
840
- end
841
- end
842
-
843
- def find_serialized_with_caching(filters_or_source, serializer, options = {})
844
- if filters_or_source.is_a?(ActiveRecord::Relation)
845
- records = filters_or_source
846
- elsif _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
847
- records = find_records(filters_or_source, options.except(:include_directives))
848
- else
849
- records = find(filters_or_source, options)
850
- end
851
- cached_resources_for(records, serializer, options)
852
- end
853
-
854
- def find_by_key(key, options = {})
855
- context = options[:context]
856
- records = find_records({_primary_key => key}, options.except(:paginator, :sort_criteria))
857
- model = records.first
858
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
859
- self.resource_for_model(model).new(model, context)
860
- end
861
-
862
- def find_by_key_serialized_with_caching(key, serializer, options = {})
863
- if _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
864
- results = find_serialized_with_caching({_primary_key => key}, serializer, options)
865
- result = results.first
866
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil?
867
- return result
868
- else
869
- resource = find_by_key(key, options)
870
- return cached_resources_for([resource], serializer, options).first
871
- end
872
- end
873
-
874
- # Override this method if you want to customize the relation for
875
- # finder methods (find, find_by_key, find_serialized_with_caching)
876
- def records(_options = {})
877
- _model_class.all
878
- end
879
-
880
- def verify_filters(filters, context = nil)
881
- verified_filters = {}
882
-
883
- return verified_filters if filters.nil?
884
-
885
- filters.each do |filter, raw_value|
886
- verified_filter = verify_filter(filter, raw_value, context)
887
- verified_filters[verified_filter[0]] = verified_filter[1]
888
- end
889
- verified_filters
890
- end
891
-
892
- def is_filter_relationship?(filter)
893
- filter == _type || _relationships.include?(filter)
894
- end
895
-
896
- def verify_filter(filter, raw, context = nil)
897
- filter_values = []
898
- if raw.present?
899
- begin
900
- filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
901
- rescue CSV::MalformedCSVError
902
- filter_values << raw
903
- end
904
- end
905
-
906
- strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
907
-
908
- if strategy
909
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
910
- values = send(strategy, filter_values, context)
911
- else
912
- values = strategy.call(filter_values, context)
913
- end
914
- [filter, values]
915
- else
916
- if is_filter_relationship?(filter)
917
- verify_relationship_filter(filter, filter_values, context)
918
- else
919
- verify_custom_filter(filter, filter_values, context)
920
- end
921
- end
922
- end
923
-
924
- def key_type(key_type)
925
- @_resource_key_type = key_type
926
- end
927
-
928
- def resource_key_type
929
- @_resource_key_type ||= JSONAPI.configuration.resource_key_type
930
- end
931
-
932
- # override to all resolution of masked ids to actual ids. Because singleton routes do not specify the id this
933
- # will be needed to allow lookup of singleton resources. Alternately singleton resources can override
934
- # `verify_key`
935
- def singleton_key(context)
936
- if @_singleton_options && @_singleton_options[:singleton_key]
937
- strategy = @_singleton_options[:singleton_key]
938
- case strategy
939
- when Proc
940
- key = strategy.call(context)
941
- when Symbol, String
942
- key = send(strategy, context)
943
- else
944
- raise "singleton_key must be a proc or function name"
945
- end
946
- end
947
- key
948
- end
949
-
950
- def verify_key(key, context = nil)
951
- key_type = resource_key_type
952
-
953
- case key_type
954
- when :integer
955
- return if key.nil?
956
- Integer(key)
957
- when :string
958
- return if key.nil?
959
- if key.to_s.include?(',')
960
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
961
- else
962
- key
963
- end
964
- when :uuid
965
- return if key.nil?
966
- if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
967
- key
968
- else
969
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
970
- end
971
- else
972
- key_type.call(key, context)
973
- end
974
- rescue
975
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
976
- end
977
-
978
- # override to allow for key processing and checking
979
- def verify_keys(keys, context = nil)
980
- return keys.collect do |key|
981
- verify_key(key, context)
982
- end
983
- end
984
-
985
- # Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
986
- def verify_custom_filter(filter, value, _context = nil)
987
- [filter, value]
988
- end
989
-
990
- # Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
991
- # relationship logic, such as uuids, multiple keys or permission checks on keys
992
- def verify_relationship_filter(filter, raw, _context = nil)
993
- [filter, raw]
994
- end
995
-
996
- # quasi private class methods
997
- def _attribute_options(attr)
998
- default_attribute_options.merge(@_attributes[attr])
999
- end
1000
-
1001
- def _has_attribute?(attr)
1002
- @_attributes.keys.include?(attr.to_sym)
1003
- end
1004
-
1005
- def _updatable_relationships
1006
- @_relationships.map { |key, _relationship| key }
1007
- end
1008
-
1009
- def _relationship(type)
1010
- type = type.to_sym
1011
- @_relationships[type]
1012
- end
1013
-
1014
- def _model_name
1015
- if _abstract
1016
- return ''
1017
- else
1018
- return @_model_name.to_s if defined?(@_model_name)
1019
- class_name = self.name
1020
- return '' if class_name.nil?
1021
- @_model_name = class_name.demodulize.sub(/Resource$/, '')
1022
- return @_model_name.to_s
1023
- end
1024
- end
1025
-
1026
- def _primary_key
1027
- @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
1028
- end
1029
-
1030
- def _cache_field
1031
- @_cache_field ||= JSONAPI.configuration.default_resource_cache_field
1032
- end
1033
-
1034
- def _table_name
1035
- @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
1036
- end
1037
-
1038
- def _as_parent_key
1039
- @_as_parent_key ||= "#{_type.to_s.singularize}_id"
1040
- end
1041
-
1042
- def _allowed_filters
1043
- defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
1044
- end
1045
-
1046
- def _paginator
1047
- @_paginator ||= JSONAPI.configuration.default_paginator
1048
- end
1049
-
1050
- def paginator(paginator)
1051
- @_paginator = paginator
1052
- end
1053
-
1054
- def abstract(val = true)
1055
- @abstract = val
1056
- end
1057
-
1058
- def _abstract
1059
- @abstract
1060
- end
1061
-
1062
- def immutable(val = true)
1063
- @immutable = val
1064
- end
1065
-
1066
- def _immutable
1067
- @immutable
1068
- end
1069
-
1070
- def mutable?
1071
- !@immutable
1072
- end
1073
-
1074
- def exclude_links(exclude)
1075
- _resolve_exclude_links(exclude)
1076
- end
1077
-
1078
- def _exclude_links
1079
- @_exclude_links ||= _resolve_exclude_links(JSONAPI.configuration.default_exclude_links)
1080
- end
1081
-
1082
- def exclude_link?(link)
1083
- _exclude_links.include?(link.to_sym)
1084
- end
1085
-
1086
- def _resolve_exclude_links(exclude)
1087
- case exclude
1088
- when :default, "default"
1089
- @_exclude_links = [:self]
1090
- when :none, "none"
1091
- @_exclude_links = []
1092
- when Array
1093
- @_exclude_links = exclude.collect {|link| link.to_sym}
1094
- else
1095
- fail "Invalid exclude_links"
1096
- end
1097
- end
1098
-
1099
- def caching(val = true)
1100
- @caching = val
1101
- end
1102
-
1103
- def _caching
1104
- @caching
1105
- end
1106
-
1107
- def caching?
1108
- @caching && !JSONAPI.configuration.resource_cache.nil?
1109
- end
1110
-
1111
- def attribute_caching_context(context)
1112
- nil
1113
- end
1114
-
1115
- def _model_class
1116
- return nil if _abstract
1117
-
1118
- return @model_class if @model_class
1119
-
1120
- model_name = _model_name
1121
- return nil if model_name.to_s.blank?
1122
-
1123
- @model_class = model_name.to_s.safe_constantize
1124
- if @model_class.nil?
1125
- warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
1126
- end
1127
-
1128
- @model_class
1129
- end
1130
-
1131
- def _allowed_filter?(filter)
1132
- !_allowed_filters[filter].nil?
1133
- end
1134
-
1135
- def module_path
1136
- if name == 'JSONAPI::Resource'
1137
- ''
1138
- else
1139
- name =~ MODULE_PATH_REGEXP ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1140
- end
1141
- end
1142
-
1143
- def default_sort
1144
- [{field: 'id', direction: :asc}]
1145
- end
1146
-
1147
- def construct_order_options(sort_params)
1148
- sort_params ||= default_sort
1149
-
1150
- return {} unless sort_params
1151
-
1152
- sort_params.each_with_object({}) do |sort, order_hash|
1153
- field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
1154
- order_hash[field] = sort[:direction]
1155
- end
1156
- end
1157
-
1158
- def _add_relationship(klass, *attrs)
1159
- options = attrs.extract_options!
1160
- options[:parent_resource] = self
1161
-
1162
- attrs.each do |name|
1163
- relationship_name = name.to_sym
1164
- check_reserved_relationship_name(relationship_name)
1165
- check_duplicate_relationship_name(relationship_name)
1166
-
1167
- JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
1168
- .define_relationship_methods(relationship_name.to_sym)
1169
- end
1170
- end
1171
-
1172
- # Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
1173
- def inject_method_definition(name, body)
1174
- define_method(name, body)
1175
- end
1176
-
1177
- def register_relationship(name, relationship_object)
1178
- @_relationships[name] = relationship_object
1179
- end
1180
-
1181
- private
1182
-
1183
- def cached_resources_for(records, serializer, options)
1184
- if records.is_a?(Array) && records.all?{|rec| rec.is_a?(JSONAPI::Resource)}
1185
- resources = records.map{|r| [r.id, r] }.to_h
1186
- elsif self.caching?
1187
- t = _model_class.arel_table
1188
- cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
1189
- resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
1190
- else
1191
- resources = resources_for(records, options[:context]).map{|r| [r.id, r] }.to_h
1192
- end
1193
-
1194
- preload_included_fragments(resources, records, serializer, options)
1195
-
1196
- resources.values
1197
- end
1198
-
1199
- def find_records(filters, options = {})
1200
- context = options[:context]
1201
-
1202
- records = filter_records(filters, options)
1203
-
1204
- sort_criteria = options.fetch(:sort_criteria) { [] }
1205
- order_options = construct_order_options(sort_criteria)
1206
- records = sort_records(records, order_options, context)
1207
-
1208
- records = apply_pagination(records, options[:paginator], order_options)
1209
-
1210
- records
1211
- end
1212
-
1213
- def check_reserved_resource_name(type, name)
1214
- if [:ids, :types, :hrefs, :links].include?(type)
1215
- warn "[NAME COLLISION] `#{name}` is a reserved resource name."
1216
- return
1217
- end
1218
- end
1219
-
1220
- def check_reserved_attribute_name(name)
1221
- # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
1222
- # an attribute method won't be created for it.
1223
- if [:type].include?(name.to_sym)
1224
- warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
1225
- end
1226
- end
1227
-
1228
- def check_reserved_relationship_name(name)
1229
- if [:id, :ids, :type, :types].include?(name.to_sym)
1230
- warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
1231
- end
1232
- end
1233
-
1234
- def check_duplicate_relationship_name(name)
1235
- if _relationships.include?(name.to_sym)
1236
- warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1237
- end
1238
- end
1239
-
1240
- def check_duplicate_attribute_name(name)
1241
- if _attributes.include?(name.to_sym)
1242
- warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1243
- end
1244
- end
1245
-
1246
- def preload_included_fragments(resources, records, serializer, options)
1247
- return if resources.empty?
1248
- res_ids = resources.keys
1249
-
1250
- include_directives = options[:include_directives]
1251
- return unless include_directives
1252
-
1253
- context = options[:context]
1254
-
1255
- # For each association, including indirect associations, find the target record ids.
1256
- # Even if a target class doesn't have caching enabled, we still have to look up
1257
- # and match the target ids here, because we can't use ActiveRecord#includes.
1258
- #
1259
- # Note that `paths` returns partial paths before complete paths, so e.g. the partial
1260
- # fragments for posts.comments will exist before we start working with posts.comments.author
1261
- target_resources = {}
1262
- include_directives.paths.each do |path|
1263
- # If path is [:posts, :comments, :author], then...
1264
- pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
1265
- pluck_attrs << self._model_class.arel_table[self._primary_key]
1266
-
1267
- relation = records
1268
- .except(:limit, :offset, :order)
1269
- .where({_primary_key => res_ids})
1270
-
1271
- # These are updated as we iterate through the association path; afterwards they will
1272
- # refer to the final resource on the path, i.e. the actual resource to find in the cache.
1273
- # So e.g. if path is [:posts, :comments, :author], then after iteration...
1274
- parent_klass = nil # Comment
1275
- klass = self # Person
1276
- relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
1277
- table = nil # people
1278
- assocs_path = [] # [ :posts, :approved_comments, :author ]
1279
- ar_hash = nil # { :posts => { :approved_comments => :author } }
1280
-
1281
- # For each step on the path, figure out what the actual table name/alias in the join
1282
- # will be, and include the primary key of that table in our list of fields to select
1283
- non_polymorphic = true
1284
- path.each do |elem|
1285
- relationship = klass._relationships[elem]
1286
- if relationship.polymorphic
1287
- # Can't preload through a polymorphic belongs_to association, ResourceSerializer
1288
- # will just have to bypass the cache and load the real Resource.
1289
- non_polymorphic = false
1290
- break
1291
- end
1292
- assocs_path << relationship.relation_name(options).to_sym
1293
- # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
1294
- ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
1295
- # We can't just look up the table name from the resource class, because Arel could
1296
- # have used a table alias if the relation includes a self-reference.
1297
- join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node|
1298
- arel_node.is_a?(Arel::Nodes::InnerJoin)
1299
- end
1300
- table = join_source.left
1301
- parent_klass = klass
1302
- klass = relationship.resource_klass
1303
- pluck_attrs << table[klass._primary_key]
1304
- end
1305
- next unless non_polymorphic
1306
-
1307
- # Pre-fill empty hashes for each resource up to the end of the path.
1308
- # This allows us to later distinguish between a preload that returned nothing
1309
- # vs. a preload that never ran.
1310
- prefilling_resources = resources.values
1311
- path.each do |rel_name|
1312
- rel_name = serializer.key_formatter.format(rel_name)
1313
- prefilling_resources.map! do |res|
1314
- res.preloaded_fragments[rel_name] ||= {}
1315
- res.preloaded_fragments[rel_name].values
1316
- end
1317
- prefilling_resources.flatten!(1)
1318
- end
1319
-
1320
- pluck_attrs << table[klass._cache_field] if klass.caching?
1321
- relation = relation.joins(ar_hash)
1322
- if relationship.is_a?(JSONAPI::Relationship::ToMany)
1323
- # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
1324
- # FIXME Should find a better way to reflect on relationship ordering. :-(
1325
- relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders)
1326
- end
1327
-
1328
- # [[post id, comment id, author id, author updated_at], ...]
1329
- id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs)
1330
-
1331
- target_resources[klass.name] ||= {}
1332
-
1333
- if klass.caching?
1334
- sub_cache_ids = id_rows
1335
- .map{|row| row.last(2) }
1336
- .reject{|row| target_resources[klass.name].has_key?(row.first) }
1337
- .uniq
1338
- target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments(
1339
- klass, serializer, context, sub_cache_ids
1340
- )
1341
- else
1342
- sub_res_ids = id_rows
1343
- .map(&:last)
1344
- .reject{|id| target_resources[klass.name].has_key?(id) }
1345
- .uniq
1346
- found = klass.find({klass._primary_key => sub_res_ids}, context: options[:context])
1347
- target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
1348
- end
1349
-
1350
- id_rows.each do |row|
1351
- res = resources[row.first]
1352
- path.each_with_index do |rel_name, index|
1353
- rel_name = serializer.key_formatter.format(rel_name)
1354
- rel_id = row[index+1]
1355
- assoc_rels = res.preloaded_fragments[rel_name]
1356
- if index == path.length - 1
1357
- association_res = target_resources[klass.name].fetch(rel_id, nil)
1358
- assoc_rels[rel_id] = association_res if association_res
1359
- else
1360
- res = assoc_rels[rel_id]
1361
- end
1362
- end
1363
- end
1364
- end
1365
- end
1366
-
1367
- def pluck_arel_attributes(relation, *attrs)
1368
- conn = relation.connection
1369
- quoted_attrs = attrs.map do |attr|
1370
- quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
1371
- quoted_column = conn.quote_column_name(attr.name)
1372
- Arel.sql("#{quoted_table}.#{quoted_column}")
1373
- end
1374
- relation.pluck(*quoted_attrs)
1375
- end
1376
- end
2
+ class Resource < ActiveRelationResource
3
+ root_resource
1377
4
  end
1378
- end
5
+ end