jsonapi-resources 0.9.11 → 0.10.7

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.
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 +16 -19
  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 -18
  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 +5 -2
  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 +36 -33
  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