jsonapi-resources 0.9.0 → 0.10.6

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 +303 -0
  8. data/lib/jsonapi/active_relation_resource.rb +884 -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 +71 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +80 -50
  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 +239 -111
  25. data/lib/jsonapi/relationship.rb +153 -15
  26. data/lib/jsonapi/request_parser.rb +430 -367
  27. data/lib/jsonapi/resource.rb +3 -1253
  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 +50 -20
  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,1255 +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)).delete(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.include?('/') ? 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(attr, options = {})
511
- check_reserved_attribute_name(attr)
512
-
513
- if (attr.to_sym == :id) && (options[:format].nil?)
514
- ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
515
- end
516
-
517
- check_duplicate_attribute_name(attr) if options[:format].nil?
518
-
519
- @_attributes ||= {}
520
- @_attributes[attr] = options
521
- define_method attr do
522
- @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
523
- end unless method_defined?(attr)
524
-
525
- define_method "#{attr}=" do |value|
526
- @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
527
- end unless method_defined?("#{attr}=")
528
- end
529
-
530
- def default_attribute_options
531
- { format: :default }
532
- end
533
-
534
- def relationship(*attrs)
535
- options = attrs.extract_options!
536
- klass = case options[:to]
537
- when :one
538
- Relationship::ToOne
539
- when :many
540
- Relationship::ToMany
541
- else
542
- #:nocov:#
543
- fail ArgumentError.new('to: must be either :one or :many')
544
- #:nocov:#
545
- end
546
- _add_relationship(klass, *attrs, options.except(:to))
547
- end
548
-
549
- def has_one(*attrs)
550
- _add_relationship(Relationship::ToOne, *attrs)
551
- end
552
-
553
- def belongs_to(*attrs)
554
- ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
555
- " using the `belongs_to` class method. We think `has_one`" \
556
- " is more appropriate. If you know what you're doing," \
557
- " and don't want to see this warning again, override the" \
558
- " `belongs_to` class method on your resource."
559
- _add_relationship(Relationship::ToOne, *attrs)
560
- end
561
-
562
- def has_many(*attrs)
563
- _add_relationship(Relationship::ToMany, *attrs)
564
- end
565
-
566
- def model_name(model, options = {})
567
- @_model_name = model.to_sym
568
-
569
- model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
570
-
571
- rebuild_relationships(_relationships)
572
- end
573
-
574
- def model_hint(model: _model_name, resource: _type)
575
- resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
576
-
577
- _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
578
- end
579
-
580
- def filters(*attrs)
581
- @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
582
- end
583
-
584
- def filter(attr, *args)
585
- @_allowed_filters[attr.to_sym] = args.extract_options!
586
- end
587
-
588
- def primary_key(key)
589
- @_primary_key = key.to_sym
590
- end
591
-
592
- def cache_field(field)
593
- @_cache_field = field.to_sym
594
- end
595
-
596
- # Override in your resource to filter the updatable keys
597
- def updatable_fields(_context = nil)
598
- _updatable_relationships | _attributes.keys - [:id]
599
- end
600
-
601
- # Override in your resource to filter the creatable keys
602
- def creatable_fields(_context = nil)
603
- _updatable_relationships | _attributes.keys - [:id]
604
- end
605
-
606
- # Override in your resource to filter the sortable keys
607
- def sortable_fields(_context = nil)
608
- _attributes.keys
609
- end
610
-
611
- def fields
612
- _relationships.keys | _attributes.keys
613
- end
614
-
615
- def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
616
- case model_includes
617
- when Array
618
- return model_includes.map do |value|
619
- resolve_relationship_names_to_relations(resource_klass, value, options)
620
- end
621
- when Hash
622
- model_includes.keys.each do |key|
623
- relationship = resource_klass._relationships[key]
624
- value = model_includes[key]
625
- model_includes.delete(key)
626
- model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
627
- end
628
- return model_includes
629
- when Symbol
630
- relationship = resource_klass._relationships[model_includes]
631
- return relationship.relation_name(options)
632
- end
633
- end
634
-
635
- def apply_includes(records, options = {})
636
- include_directives = options[:include_directives]
637
- if include_directives
638
- model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
639
- records = records.includes(model_includes)
640
- end
641
-
642
- records
643
- end
644
-
645
- def apply_pagination(records, paginator, order_options)
646
- records = paginator.apply(records, order_options) if paginator
647
- records
648
- end
649
-
650
- def apply_sort(records, order_options, _context = {})
651
- if order_options.any?
652
- order_options.each_pair do |field, direction|
653
- if field.to_s.include?(".")
654
- *model_names, column_name = field.split(".")
655
-
656
- associations = _lookup_association_chain([records.model.to_s, *model_names])
657
- joins_query = _build_joins([records.model, *associations])
658
-
659
- # _sorting is appended to avoid name clashes with manual joins eg. overridden filters
660
- order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
661
- records = records.joins(joins_query).order(order_by_query)
662
- else
663
- records = records.order(field => direction)
664
- end
665
- end
666
- end
667
-
668
- records
669
- end
670
-
671
- def _lookup_association_chain(model_names)
672
- associations = []
673
- model_names.inject do |prev, current|
674
- association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
675
- assoc.name.to_s.downcase == current.downcase
676
- end
677
- associations << association
678
- association.class_name
679
- end
680
-
681
- associations
682
- end
683
-
684
- def _build_joins(associations)
685
- joins = []
686
-
687
- associations.inject do |prev, current|
688
- joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
689
- current
690
- end
691
- joins.join("\n")
692
- end
693
-
694
- def apply_filter(records, filter, value, options = {})
695
- strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
696
-
697
- if strategy
698
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
699
- send(strategy, records, value, options)
700
- else
701
- strategy.call(records, value, options)
702
- end
703
- else
704
- records.where(filter => value)
705
- end
706
- end
707
-
708
- def apply_filters(records, filters, options = {})
709
- required_includes = []
710
-
711
- if filters
712
- filters.each do |filter, value|
713
- if _relationships.include?(filter)
714
- if _relationships[filter].belongs_to?
715
- records = apply_filter(records, _relationships[filter].foreign_key, value, options)
716
- else
717
- required_includes.push(filter.to_s)
718
- records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
719
- end
720
- else
721
- records = apply_filter(records, filter, value, options)
722
- end
723
- end
724
- end
725
-
726
- if required_includes.any?
727
- records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
728
- end
729
-
730
- records
731
- end
732
-
733
- def filter_records(filters, options, records = records(options))
734
- records = apply_filters(records, filters, options)
735
- apply_includes(records, options)
736
- end
737
-
738
- def sort_records(records, order_options, context = {})
739
- apply_sort(records, order_options, context)
740
- end
741
-
742
- # Assumes ActiveRecord's counting. Override if you need a different counting method
743
- def count_records(records)
744
- records.count(:all)
745
- end
746
-
747
- def find_count(filters, options = {})
748
- count_records(filter_records(filters, options))
749
- end
750
-
751
- def find(filters, options = {})
752
- resources_for(find_records(filters, options), options[:context])
753
- end
754
-
755
- def resources_for(records, context)
756
- records.collect do |model|
757
- resource_class = self.resource_for_model(model)
758
- resource_class.new(model, context)
759
- end
760
- end
761
-
762
- def find_by_keys(keys, options = {})
763
- context = options[:context]
764
- records = records(options)
765
- records = apply_includes(records, options)
766
- models = records.where({_primary_key => keys})
767
- models.collect do |model|
768
- self.resource_for_model(model).new(model, context)
769
- end
770
- end
771
-
772
- def find_serialized_with_caching(filters_or_source, serializer, options = {})
773
- if filters_or_source.is_a?(ActiveRecord::Relation)
774
- records = filters_or_source
775
- elsif _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
776
- records = find_records(filters_or_source, options.except(:include_directives))
777
- else
778
- records = find(filters_or_source, options)
779
- end
780
- cached_resources_for(records, serializer, options)
781
- end
782
-
783
- def find_by_key(key, options = {})
784
- context = options[:context]
785
- records = find_records({_primary_key => key}, options.except(:paginator, :sort_criteria))
786
- model = records.first
787
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
788
- self.resource_for_model(model).new(model, context)
789
- end
790
-
791
- def find_by_key_serialized_with_caching(key, serializer, options = {})
792
- if _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
793
- results = find_serialized_with_caching({_primary_key => key}, serializer, options)
794
- result = results.first
795
- fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil?
796
- return result
797
- else
798
- resource = find_by_key(key, options)
799
- return cached_resources_for([resource], serializer, options).first
800
- end
801
- end
802
-
803
- # Override this method if you want to customize the relation for
804
- # finder methods (find, find_by_key, find_serialized_with_caching)
805
- def records(_options = {})
806
- _model_class.all
807
- end
808
-
809
- def verify_filters(filters, context = nil)
810
- verified_filters = {}
811
- filters.each do |filter, raw_value|
812
- verified_filter = verify_filter(filter, raw_value, context)
813
- verified_filters[verified_filter[0]] = verified_filter[1]
814
- end
815
- verified_filters
816
- end
817
-
818
- def is_filter_relationship?(filter)
819
- filter == _type || _relationships.include?(filter)
820
- end
821
-
822
- def verify_filter(filter, raw, context = nil)
823
- filter_values = []
824
- if raw.present?
825
- begin
826
- filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
827
- rescue CSV::MalformedCSVError
828
- filter_values << raw
829
- end
830
- end
831
-
832
- strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
833
-
834
- if strategy
835
- if strategy.is_a?(Symbol) || strategy.is_a?(String)
836
- values = send(strategy, filter_values, context)
837
- else
838
- values = strategy.call(filter_values, context)
839
- end
840
- [filter, values]
841
- else
842
- if is_filter_relationship?(filter)
843
- verify_relationship_filter(filter, filter_values, context)
844
- else
845
- verify_custom_filter(filter, filter_values, context)
846
- end
847
- end
848
- end
849
-
850
- def key_type(key_type)
851
- @_resource_key_type = key_type
852
- end
853
-
854
- def resource_key_type
855
- @_resource_key_type ||= JSONAPI.configuration.resource_key_type
856
- end
857
-
858
- def verify_key(key, context = nil)
859
- key_type = resource_key_type
860
-
861
- case key_type
862
- when :integer
863
- return if key.nil?
864
- Integer(key)
865
- when :string
866
- return if key.nil?
867
- if key.to_s.include?(',')
868
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
869
- else
870
- key
871
- end
872
- when :uuid
873
- return if key.nil?
874
- 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}$/)
875
- key
876
- else
877
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
878
- end
879
- else
880
- key_type.call(key, context)
881
- end
882
- rescue
883
- raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
884
- end
885
-
886
- # override to allow for key processing and checking
887
- def verify_keys(keys, context = nil)
888
- return keys.collect do |key|
889
- verify_key(key, context)
890
- end
891
- end
892
-
893
- # Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
894
- def verify_custom_filter(filter, value, _context = nil)
895
- [filter, value]
896
- end
897
-
898
- # Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
899
- # relationship logic, such as uuids, multiple keys or permission checks on keys
900
- def verify_relationship_filter(filter, raw, _context = nil)
901
- [filter, raw]
902
- end
903
-
904
- # quasi private class methods
905
- def _attribute_options(attr)
906
- default_attribute_options.merge(@_attributes[attr])
907
- end
908
-
909
- def _updatable_relationships
910
- @_relationships.map { |key, _relationship| key }
911
- end
912
-
913
- def _relationship(type)
914
- type = type.to_sym
915
- @_relationships[type]
916
- end
917
-
918
- def _model_name
919
- if _abstract
920
- return ''
921
- else
922
- return @_model_name.to_s if defined?(@_model_name)
923
- class_name = self.name
924
- return '' if class_name.nil?
925
- @_model_name = class_name.demodulize.sub(/Resource$/, '')
926
- return @_model_name.to_s
927
- end
928
- end
929
-
930
- def _primary_key
931
- @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
932
- end
933
-
934
- def _cache_field
935
- @_cache_field ||= JSONAPI.configuration.default_resource_cache_field
936
- end
937
-
938
- def _table_name
939
- @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
940
- end
941
-
942
- def _as_parent_key
943
- @_as_parent_key ||= "#{_type.to_s.singularize}_id"
944
- end
945
-
946
- def _allowed_filters
947
- defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
948
- end
949
-
950
- def _paginator
951
- @_paginator ||= JSONAPI.configuration.default_paginator
952
- end
953
-
954
- def paginator(paginator)
955
- @_paginator = paginator
956
- end
957
-
958
- def abstract(val = true)
959
- @abstract = val
960
- end
961
-
962
- def _abstract
963
- @abstract
964
- end
965
-
966
- def immutable(val = true)
967
- @immutable = val
968
- end
969
-
970
- def _immutable
971
- @immutable
972
- end
973
-
974
- def mutable?
975
- !@immutable
976
- end
977
-
978
- def caching(val = true)
979
- @caching = val
980
- end
981
-
982
- def _caching
983
- @caching
984
- end
985
-
986
- def caching?
987
- @caching && !JSONAPI.configuration.resource_cache.nil?
988
- end
989
-
990
- def attribute_caching_context(context)
991
- nil
992
- end
993
-
994
- def _model_class
995
- return nil if _abstract
996
-
997
- return @model_class if @model_class
998
-
999
- model_name = _model_name
1000
- return nil if model_name.to_s.blank?
1001
-
1002
- @model_class = model_name.to_s.safe_constantize
1003
- if @model_class.nil?
1004
- warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
1005
- end
1006
-
1007
- @model_class
1008
- end
1009
-
1010
- def _allowed_filter?(filter)
1011
- !_allowed_filters[filter].nil?
1012
- end
1013
-
1014
- def module_path
1015
- if name == 'JSONAPI::Resource'
1016
- ''
1017
- else
1018
- name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1019
- end
1020
- end
1021
-
1022
- def default_sort
1023
- [{field: 'id', direction: :asc}]
1024
- end
1025
-
1026
- def construct_order_options(sort_params)
1027
- sort_params ||= default_sort
1028
-
1029
- return {} unless sort_params
1030
-
1031
- sort_params.each_with_object({}) do |sort, order_hash|
1032
- field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
1033
- order_hash[field] = sort[:direction]
1034
- end
1035
- end
1036
-
1037
- def _add_relationship(klass, *attrs)
1038
- options = attrs.extract_options!
1039
- options[:parent_resource] = self
1040
-
1041
- attrs.each do |relationship_name|
1042
- check_reserved_relationship_name(relationship_name)
1043
- check_duplicate_relationship_name(relationship_name)
1044
-
1045
- JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
1046
- .define_relationship_methods(relationship_name.to_sym)
1047
- end
1048
- end
1049
-
1050
- # Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
1051
- def inject_method_definition(name, body)
1052
- define_method(name, body)
1053
- end
1054
-
1055
- def register_relationship(name, relationship_object)
1056
- @_relationships[name] = relationship_object
1057
- end
1058
-
1059
- private
1060
-
1061
- def cached_resources_for(records, serializer, options)
1062
- if records.is_a?(Array) && records.all?{|rec| rec.is_a?(JSONAPI::Resource)}
1063
- resources = records.map{|r| [r.id, r] }.to_h
1064
- elsif self.caching?
1065
- t = _model_class.arel_table
1066
- cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
1067
- resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
1068
- else
1069
- resources = resources_for(records, options[:context]).map{|r| [r.id, r] }.to_h
1070
- end
1071
-
1072
- preload_included_fragments(resources, records, serializer, options)
1073
-
1074
- resources.values
1075
- end
1076
-
1077
- def find_records(filters, options = {})
1078
- context = options[:context]
1079
-
1080
- records = filter_records(filters, options)
1081
-
1082
- sort_criteria = options.fetch(:sort_criteria) { [] }
1083
- order_options = construct_order_options(sort_criteria)
1084
- records = sort_records(records, order_options, context)
1085
-
1086
- records = apply_pagination(records, options[:paginator], order_options)
1087
-
1088
- records
1089
- end
1090
-
1091
- def check_reserved_resource_name(type, name)
1092
- if [:ids, :types, :hrefs, :links].include?(type)
1093
- warn "[NAME COLLISION] `#{name}` is a reserved resource name."
1094
- return
1095
- end
1096
- end
1097
-
1098
- def check_reserved_attribute_name(name)
1099
- # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
1100
- # an attribute method won't be created for it.
1101
- if [:type].include?(name.to_sym)
1102
- warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
1103
- end
1104
- end
1105
-
1106
- def check_reserved_relationship_name(name)
1107
- if [:id, :ids, :type, :types].include?(name.to_sym)
1108
- warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
1109
- end
1110
- end
1111
-
1112
- def check_duplicate_relationship_name(name)
1113
- if _relationships.include?(name.to_sym)
1114
- warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1115
- end
1116
- end
1117
-
1118
- def check_duplicate_attribute_name(name)
1119
- if _attributes.include?(name.to_sym)
1120
- warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1121
- end
1122
- end
1123
-
1124
- def preload_included_fragments(resources, records, serializer, options)
1125
- return if resources.empty?
1126
- res_ids = resources.keys
1127
-
1128
- include_directives = options[:include_directives]
1129
- return unless include_directives
1130
-
1131
- context = options[:context]
1132
-
1133
- # For each association, including indirect associations, find the target record ids.
1134
- # Even if a target class doesn't have caching enabled, we still have to look up
1135
- # and match the target ids here, because we can't use ActiveRecord#includes.
1136
- #
1137
- # Note that `paths` returns partial paths before complete paths, so e.g. the partial
1138
- # fragments for posts.comments will exist before we start working with posts.comments.author
1139
- target_resources = {}
1140
- include_directives.paths.each do |path|
1141
- # If path is [:posts, :comments, :author], then...
1142
- pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
1143
- pluck_attrs << self._model_class.arel_table[self._primary_key]
1144
-
1145
- relation = records
1146
- .except(:limit, :offset, :order)
1147
- .where({_primary_key => res_ids})
1148
-
1149
- # These are updated as we iterate through the association path; afterwards they will
1150
- # refer to the final resource on the path, i.e. the actual resource to find in the cache.
1151
- # So e.g. if path is [:posts, :comments, :author], then after iteration...
1152
- parent_klass = nil # Comment
1153
- klass = self # Person
1154
- relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
1155
- table = nil # people
1156
- assocs_path = [] # [ :posts, :approved_comments, :author ]
1157
- ar_hash = nil # { :posts => { :approved_comments => :author } }
1158
-
1159
- # For each step on the path, figure out what the actual table name/alias in the join
1160
- # will be, and include the primary key of that table in our list of fields to select
1161
- non_polymorphic = true
1162
- path.each do |elem|
1163
- relationship = klass._relationships[elem]
1164
- if relationship.polymorphic
1165
- # Can't preload through a polymorphic belongs_to association, ResourceSerializer
1166
- # will just have to bypass the cache and load the real Resource.
1167
- non_polymorphic = false
1168
- break
1169
- end
1170
- assocs_path << relationship.relation_name(options).to_sym
1171
- # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
1172
- ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
1173
- # We can't just look up the table name from the resource class, because Arel could
1174
- # have used a table alias if the relation includes a self-reference.
1175
- join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node|
1176
- arel_node.is_a?(Arel::Nodes::InnerJoin)
1177
- end
1178
- table = join_source.left
1179
- parent_klass = klass
1180
- klass = relationship.resource_klass
1181
- pluck_attrs << table[klass._primary_key]
1182
- end
1183
- next unless non_polymorphic
1184
-
1185
- # Pre-fill empty hashes for each resource up to the end of the path.
1186
- # This allows us to later distinguish between a preload that returned nothing
1187
- # vs. a preload that never ran.
1188
- prefilling_resources = resources.values
1189
- path.each do |rel_name|
1190
- rel_name = serializer.key_formatter.format(rel_name)
1191
- prefilling_resources.map! do |res|
1192
- res.preloaded_fragments[rel_name] ||= {}
1193
- res.preloaded_fragments[rel_name].values
1194
- end
1195
- prefilling_resources.flatten!(1)
1196
- end
1197
-
1198
- pluck_attrs << table[klass._cache_field] if klass.caching?
1199
- relation = relation.joins(ar_hash)
1200
- if relationship.is_a?(JSONAPI::Relationship::ToMany)
1201
- # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
1202
- # FIXME Should find a better way to reflect on relationship ordering. :-(
1203
- relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders)
1204
- end
1205
-
1206
- # [[post id, comment id, author id, author updated_at], ...]
1207
- id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs)
1208
-
1209
- target_resources[klass.name] ||= {}
1210
-
1211
- if klass.caching?
1212
- sub_cache_ids = id_rows
1213
- .map{|row| row.last(2) }
1214
- .reject{|row| target_resources[klass.name].has_key?(row.first) }
1215
- .uniq
1216
- target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments(
1217
- klass, serializer, context, sub_cache_ids
1218
- )
1219
- else
1220
- sub_res_ids = id_rows
1221
- .map(&:last)
1222
- .reject{|id| target_resources[klass.name].has_key?(id) }
1223
- .uniq
1224
- found = klass.find({klass._primary_key => sub_res_ids}, context: options[:context])
1225
- target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
1226
- end
1227
-
1228
- id_rows.each do |row|
1229
- res = resources[row.first]
1230
- path.each_with_index do |rel_name, index|
1231
- rel_name = serializer.key_formatter.format(rel_name)
1232
- rel_id = row[index+1]
1233
- assoc_rels = res.preloaded_fragments[rel_name]
1234
- if index == path.length - 1
1235
- assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id)
1236
- else
1237
- res = assoc_rels[rel_id]
1238
- end
1239
- end
1240
- end
1241
- end
1242
- end
1243
-
1244
- def pluck_arel_attributes(relation, *attrs)
1245
- conn = relation.connection
1246
- quoted_attrs = attrs.map do |attr|
1247
- quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
1248
- quoted_column = conn.quote_column_name(attr.name)
1249
- "#{quoted_table}.#{quoted_column}"
1250
- end
1251
- relation.pluck(*quoted_attrs)
1252
- end
1253
- end
2
+ class Resource < ActiveRelationResource
3
+ root_resource
1254
4
  end
1255
- end
5
+ end