sanger-jsonapi-resources 0.1.0

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +53 -0
  4. data/lib/generators/jsonapi/USAGE +13 -0
  5. data/lib/generators/jsonapi/controller_generator.rb +14 -0
  6. data/lib/generators/jsonapi/resource_generator.rb +14 -0
  7. data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
  8. data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
  10. data/lib/jsonapi/cached_resource_fragment.rb +127 -0
  11. data/lib/jsonapi/callbacks.rb +51 -0
  12. data/lib/jsonapi/compiled_json.rb +36 -0
  13. data/lib/jsonapi/configuration.rb +258 -0
  14. data/lib/jsonapi/error.rb +47 -0
  15. data/lib/jsonapi/error_codes.rb +60 -0
  16. data/lib/jsonapi/exceptions.rb +563 -0
  17. data/lib/jsonapi/formatter.rb +169 -0
  18. data/lib/jsonapi/include_directives.rb +100 -0
  19. data/lib/jsonapi/link_builder.rb +152 -0
  20. data/lib/jsonapi/mime_types.rb +41 -0
  21. data/lib/jsonapi/naive_cache.rb +30 -0
  22. data/lib/jsonapi/operation.rb +24 -0
  23. data/lib/jsonapi/operation_dispatcher.rb +88 -0
  24. data/lib/jsonapi/operation_result.rb +65 -0
  25. data/lib/jsonapi/operation_results.rb +35 -0
  26. data/lib/jsonapi/paginator.rb +209 -0
  27. data/lib/jsonapi/processor.rb +328 -0
  28. data/lib/jsonapi/relationship.rb +94 -0
  29. data/lib/jsonapi/relationship_builder.rb +167 -0
  30. data/lib/jsonapi/request_parser.rb +678 -0
  31. data/lib/jsonapi/resource.rb +1255 -0
  32. data/lib/jsonapi/resource_controller.rb +5 -0
  33. data/lib/jsonapi/resource_controller_metal.rb +16 -0
  34. data/lib/jsonapi/resource_serializer.rb +531 -0
  35. data/lib/jsonapi/resources/version.rb +5 -0
  36. data/lib/jsonapi/response_document.rb +135 -0
  37. data/lib/jsonapi/routing_ext.rb +262 -0
  38. data/lib/jsonapi-resources.rb +27 -0
  39. metadata +223 -0
@@ -0,0 +1,1255 @@
1
+ require 'jsonapi/callbacks'
2
+ require 'jsonapi/relationship_builder'
3
+
4
+ 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
1254
+ end
1255
+ end