jsonapi-resources 0.9.3 → 0.10.5

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