jsonapi-resources 0.9.12 → 0.10.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) 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-resources.rb +8 -3
  7. data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
  8. data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
  10. data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
  11. data/lib/jsonapi/compiled_json.rb +11 -1
  12. data/lib/jsonapi/configuration.rb +44 -18
  13. data/lib/jsonapi/error.rb +27 -0
  14. data/lib/jsonapi/exceptions.rb +43 -40
  15. data/lib/jsonapi/formatter.rb +3 -3
  16. data/lib/jsonapi/include_directives.rb +2 -45
  17. data/lib/jsonapi/link_builder.rb +87 -80
  18. data/lib/jsonapi/operation.rb +16 -5
  19. data/lib/jsonapi/operation_result.rb +74 -16
  20. data/lib/jsonapi/processor.rb +233 -112
  21. data/lib/jsonapi/relationship.rb +77 -53
  22. data/lib/jsonapi/request_parser.rb +378 -423
  23. data/lib/jsonapi/resource.rb +224 -524
  24. data/lib/jsonapi/resource_controller_metal.rb +2 -2
  25. data/lib/jsonapi/resource_fragment.rb +47 -0
  26. data/lib/jsonapi/resource_id_tree.rb +112 -0
  27. data/lib/jsonapi/resource_identity.rb +42 -0
  28. data/lib/jsonapi/resource_serializer.rb +133 -301
  29. data/lib/jsonapi/resource_set.rb +108 -0
  30. data/lib/jsonapi/resources/version.rb +1 -1
  31. data/lib/jsonapi/response_document.rb +100 -88
  32. data/lib/jsonapi/routing_ext.rb +21 -43
  33. metadata +29 -45
  34. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  35. data/lib/jsonapi/operation_results.rb +0 -35
  36. data/lib/jsonapi/relationship_builder.rb +0 -167
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: bfb7891ac7686be582e0831afd808f08504e492998968e704de2086c1656e9af
4
- data.tar.gz: edeb27a90bec0a6fc79f67e0311cdf7e588fdcf347426489c21d94bcd40a3f28
2
+ SHA1:
3
+ metadata.gz: 1da47a9966931af4ff9a5bab22e41097bf9075bf
4
+ data.tar.gz: 7ed9248ebb089d4c3f76b03d9e1eb47c8810be75
5
5
  SHA512:
6
- metadata.gz: 645534a3b1811e939fb6e36098e7624072793b59a39eecc99b05743ff77e91289ca6b35441c614220833049b689f8bcf7b6b6fb9fb1f09190b239b706b3d2da7
7
- data.tar.gz: 7b562179f5f6ab17179c71208838fccaa1703d6c88eb99356075daa255a57242f59972e5ba0118740d4d80f7bc5706f2f87b4e99b0840cca32d00b19056f5712
6
+ metadata.gz: 7f8a2a940460ca620cf2495856523c2cab00e72ee169ecc9c7bb84cb3499d96e4fe9a39b39f439b116bef24417e766cd93f8b2bf2be98e397cfe4994806abcc8
7
+ data.tar.gz: c54161c677a2b7dcf1db569bf1c05685ce8d5b84349157f7ab6f3b927c0a357322b57f762b2feeb8225ca8f9e04aeb63aa8313467d77e1f0e1d7d44616ff3357
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Larry Gebhardt
1
+ Copyright (c) 2014-2017 Cerebris Corporation
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # JSONAPI::Resources [![Gem Version](https://badge.fury.io/rb/jsonapi-resources.svg)](https://badge.fury.io/rb/jsonapi-resources) [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.svg?branch=beta)](http://travis-ci.org/cerebris/jsonapi-resources) [![Code Climate](https://codeclimate.com/github/cerebris/jsonapi-resources/badges/gpa.svg)](https://codeclimate.com/github/cerebris/jsonapi-resources)
1
+ # JSONAPI::Resources [![Gem Version](https://badge.fury.io/rb/jsonapi-resources.svg)](https://badge.fury.io/rb/jsonapi-resources) [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.svg?branch=master)](http://travis-ci.org/cerebris/jsonapi-resources) [![Code Climate](https://codeclimate.com/github/cerebris/jsonapi-resources/badges/gpa.svg)](https://codeclimate.com/github/cerebris/jsonapi-resources)
2
2
 
3
3
  [![Join the chat at https://gitter.im/cerebris/jsonapi-resources](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cerebris/jsonapi-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
4
 
@@ -13,7 +13,7 @@ backed by ActiveRecord models or by custom objects.
13
13
 
14
14
  ## Documentation
15
15
 
16
- Full documentation can be found at [http://jsonapi-resources.com](http://jsonapi-resources.com), including the [v0.9 beta Guide](http://jsonapi-resources.com/v0.9/guide/) specific to this version.
16
+ Full documentation can be found at [http://jsonapi-resources.com](http://jsonapi-resources.com), including the [v0.10 alpha Guide](http://jsonapi-resources.com/v0.10/guide/) specific to this version.
17
17
 
18
18
  ## Demo App
19
19
 
@@ -28,26 +28,49 @@ which *should* be compatible with JSON:API compliant server implementations such
28
28
 
29
29
  Add JR to your application's `Gemfile`:
30
30
 
31
- gem 'jsonapi-resources'
31
+ ```
32
+ gem 'jsonapi-resources'
33
+ ```
32
34
 
33
35
  And then execute:
34
36
 
35
- $ bundle
37
+ ```bash
38
+ bundle
39
+ ```
36
40
 
37
41
  Or install it yourself as:
38
42
 
39
- $ gem install jsonapi-resources
43
+ ```bash
44
+ gem install jsonapi-resources
45
+ ```
40
46
 
41
- **For further usage see the [v0.9 beta Guide](http://jsonapi-resources.com/v0.9/guide/)**
47
+ **For further usage see the [v0.10 alpha Guide](http://jsonapi-resources.com/v0.10/guide/)**
42
48
 
43
49
  ## Contributing
44
50
 
51
+ 1. Submit an issue describing any new features you wish it add or the bug you intend to fix
45
52
  1. Fork it ( http://github.com/cerebris/jsonapi-resources/fork )
46
- 2. Create your feature branch (`git checkout -b my-new-feature`)
47
- 3. Commit your changes (`git commit -am 'Add some feature'`)
48
- 4. Push to the branch (`git push origin my-new-feature`)
49
- 5. Create a new Pull Request
53
+ 1. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 1. Run the full test suite (`rake test`)
55
+ 1. Fix any failing tests
56
+ 1. Commit your changes (`git commit -am 'Add some feature'`)
57
+ 1. Push to the branch (`git push origin my-new-feature`)
58
+ 1. Create a new Pull Request
59
+
60
+ ## Did you find a bug?
61
+
62
+ * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/cerebris/jsonapi-resources/issues).
63
+
64
+ * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/cerebris/jsonapi-resources/issues/new).
65
+ Be sure to include a **title and clear description**, as much relevant information as possible,
66
+ and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
67
+
68
+ * If possible, use the relevant bug report templates to create the issue.
69
+ Simply copy the content of the appropriate template into a .rb file, make the necessary changes to demonstrate the issue,
70
+ and **paste the content into the issue description or attach as a file**:
71
+ * [**Rails 5** issues](https://github.com/cerebris/jsonapi-resources/blob/master/lib/bug_report_templates/rails_5_master.rb)
72
+
50
73
 
51
74
  ## License
52
75
 
53
- Copyright 2014-2016 Cerebris Corporation. MIT License (see LICENSE for details).
76
+ Copyright 2014-2017 Cerebris Corporation. MIT License (see LICENSE for details).
@@ -0,0 +1,125 @@
1
+ begin
2
+ require 'bundler/inline'
3
+ rescue LoadError => e
4
+ STDERR.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
5
+ raise e
6
+ end
7
+
8
+ gemfile(true) do
9
+ source 'https://rubygems.org'
10
+
11
+ gem 'rails', require: false
12
+ gem 'sqlite3', platform: :mri
13
+
14
+ gem 'activerecord-jdbcsqlite3-adapter',
15
+ git: 'https://github.com/jruby/activerecord-jdbc-adapter',
16
+ branch: 'rails-5',
17
+ platform: :jruby
18
+
19
+ gem 'jsonapi-resources', require: false
20
+ end
21
+
22
+ # prepare active_record database
23
+ require 'active_record'
24
+
25
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
26
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
27
+
28
+ ActiveRecord::Schema.define do
29
+ # Add your schema here
30
+ create_table :your_models, force: true do |t|
31
+ t.string :name
32
+ end
33
+ end
34
+
35
+ # create models
36
+ class YourModel < ActiveRecord::Base
37
+ end
38
+
39
+ # prepare rails app
40
+ require 'action_controller/railtie'
41
+ # require 'action_view/railtie'
42
+ require 'jsonapi-resources'
43
+
44
+ class ApplicationController < ActionController::Base
45
+ end
46
+
47
+ # prepare jsonapi resources and controllers
48
+ class YourModelsController < ApplicationController
49
+ include JSONAPI::ActsAsResourceController
50
+ end
51
+
52
+ class YourModelResource < JSONAPI::Resource
53
+ attribute :name
54
+ filter :name
55
+ end
56
+
57
+ class TestApp < Rails::Application
58
+ config.root = File.dirname(__FILE__)
59
+ config.logger = Logger.new(STDOUT)
60
+ Rails.logger = config.logger
61
+
62
+ secrets.secret_token = 'secret_token'
63
+ secrets.secret_key_base = 'secret_key_base'
64
+
65
+ config.eager_load = false
66
+ end
67
+
68
+ # initialize app
69
+ Rails.application.initialize!
70
+
71
+ JSONAPI.configure do |config|
72
+ config.json_key_format = :underscored_key
73
+ config.route_format = :underscored_key
74
+ end
75
+
76
+ # draw routes
77
+ Rails.application.routes.draw do
78
+ jsonapi_resources :your_models, only: [:index, :create]
79
+ end
80
+
81
+ # prepare tests
82
+ require 'minitest/autorun'
83
+ require 'rack/test'
84
+
85
+ # Replace this with the code necessary to make your test fail.
86
+ class BugTest < Minitest::Test
87
+ include Rack::Test::Methods
88
+
89
+ def json_api_headers
90
+ {'Accept' => JSONAPI::MEDIA_TYPE, 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE}
91
+ end
92
+
93
+ def test_index_your_models
94
+ record = YourModel.create! name: 'John Doe'
95
+ get '/your_models', nil, json_api_headers
96
+ assert last_response.ok?
97
+ json_response = JSON.parse(last_response.body)
98
+ refute_nil json_response['data']
99
+ refute_empty json_response['data']
100
+ refute_empty json_response['data'].first
101
+ assert record.id.to_s, json_response['data'].first['id']
102
+ assert 'your_models', json_response['data'].first['type']
103
+ assert({'name' => 'John Doe'}, json_response['data'].first['attributes'])
104
+ end
105
+
106
+ def test_create_your_models
107
+ json_request = {
108
+ 'data' => {
109
+ type: 'your_models',
110
+ attributes: {
111
+ name: 'Jane Doe'
112
+ }
113
+ }
114
+ }
115
+ post '/your_models', json_request.to_json, json_api_headers
116
+ assert last_response.created?
117
+ refute_nil YourModel.find_by(name: 'Jane Doe')
118
+ end
119
+
120
+ private
121
+
122
+ def app
123
+ Rails.application
124
+ end
125
+ end
@@ -0,0 +1,140 @@
1
+ begin
2
+ require 'bundler/inline'
3
+ require 'bundler'
4
+ rescue LoadError => e
5
+ STDERR.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
6
+ raise e
7
+ end
8
+
9
+ gemfile(true, ui: ENV['SILENT'] ? Bundler::UI::Silent.new : Bundler::UI::Shell.new) do
10
+ source 'https://rubygems.org'
11
+
12
+ gem 'rails', require: false
13
+ gem 'sqlite3', platform: :mri
14
+
15
+ gem 'activerecord-jdbcsqlite3-adapter',
16
+ git: 'https://github.com/jruby/activerecord-jdbc-adapter',
17
+ branch: 'rails-5',
18
+ platform: :jruby
19
+
20
+ if ENV['JSONAPI_RESOURCES_PATH']
21
+ gem 'jsonapi-resources', path: ENV['JSONAPI_RESOURCES_PATH'], require: false
22
+ else
23
+ gem 'jsonapi-resources', git: 'https://github.com/cerebris/jsonapi-resources', require: false
24
+ end
25
+
26
+ end
27
+
28
+ # prepare active_record database
29
+ require 'active_record'
30
+
31
+ class NullLogger < Logger
32
+ def initialize(*_args)
33
+ end
34
+
35
+ def add(*_args, &_block)
36
+ end
37
+ end
38
+
39
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
40
+ ActiveRecord::Base.logger = ENV['SILENT'] ? NullLogger.new : Logger.new(STDOUT)
41
+ ActiveRecord::Migration.verbose = !ENV['SILENT']
42
+
43
+ ActiveRecord::Schema.define do
44
+ # Add your schema here
45
+ create_table :your_models, force: true do |t|
46
+ t.string :name
47
+ end
48
+ end
49
+
50
+ # create models
51
+ class YourModel < ActiveRecord::Base
52
+ end
53
+
54
+ # prepare rails app
55
+ require 'action_controller/railtie'
56
+ # require 'action_view/railtie'
57
+ require 'jsonapi-resources'
58
+
59
+ class ApplicationController < ActionController::Base
60
+ end
61
+
62
+ # prepare jsonapi resources and controllers
63
+ class YourModelsController < ApplicationController
64
+ include JSONAPI::ActsAsResourceController
65
+ end
66
+
67
+ class YourModelResource < JSONAPI::Resource
68
+ attribute :name
69
+ filter :name
70
+ end
71
+
72
+ class TestApp < Rails::Application
73
+ config.root = File.dirname(__FILE__)
74
+ config.logger = ENV['SILENT'] ? NullLogger.new : Logger.new(STDOUT)
75
+ Rails.logger = config.logger
76
+
77
+ secrets.secret_token = 'secret_token'
78
+ secrets.secret_key_base = 'secret_key_base'
79
+
80
+ config.eager_load = false
81
+ end
82
+
83
+ # initialize app
84
+ Rails.application.initialize!
85
+
86
+ JSONAPI.configure do |config|
87
+ config.json_key_format = :underscored_key
88
+ config.route_format = :underscored_key
89
+ end
90
+
91
+ # draw routes
92
+ Rails.application.routes.draw do
93
+ jsonapi_resources :your_models, only: [:index, :create]
94
+ end
95
+
96
+ # prepare tests
97
+ require 'minitest/autorun'
98
+ require 'rack/test'
99
+
100
+ # Replace this with the code necessary to make your test fail.
101
+ class BugTest < Minitest::Test
102
+ include Rack::Test::Methods
103
+
104
+ def json_api_headers
105
+ {'Accept' => JSONAPI::MEDIA_TYPE, 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE}
106
+ end
107
+
108
+ def test_index_your_models
109
+ record = YourModel.create! name: 'John Doe'
110
+ get '/your_models', nil, json_api_headers
111
+ assert last_response.ok?
112
+ json_response = JSON.parse(last_response.body)
113
+ refute_nil json_response['data']
114
+ refute_empty json_response['data']
115
+ refute_empty json_response['data'].first
116
+ assert record.id.to_s, json_response['data'].first['id']
117
+ assert 'your_models', json_response['data'].first['type']
118
+ assert({'name' => 'John Doe'}, json_response['data'].first['attributes'])
119
+ end
120
+
121
+ def test_create_your_models
122
+ json_request = {
123
+ 'data' => {
124
+ type: 'your_models',
125
+ attributes: {
126
+ name: 'Jane Doe'
127
+ }
128
+ }
129
+ }
130
+ post '/your_models', json_request.to_json, json_api_headers
131
+ assert last_response.created?
132
+ refute_nil YourModel.find_by(name: 'Jane Doe')
133
+ end
134
+
135
+ private
136
+
137
+ def app
138
+ Rails.application
139
+ end
140
+ end
@@ -1,7 +1,7 @@
1
1
  require 'jsonapi/naive_cache'
2
2
  require 'jsonapi/compiled_json'
3
3
  require 'jsonapi/resource'
4
- require 'jsonapi/cached_resource_fragment'
4
+ require 'jsonapi/cached_response_fragment'
5
5
  require 'jsonapi/response_document'
6
6
  require 'jsonapi/acts_as_resource_controller'
7
7
  require 'jsonapi/resource_controller'
@@ -17,11 +17,16 @@ require 'jsonapi/exceptions'
17
17
  require 'jsonapi/error'
18
18
  require 'jsonapi/error_codes'
19
19
  require 'jsonapi/request_parser'
20
- require 'jsonapi/operation_dispatcher'
21
20
  require 'jsonapi/processor'
22
21
  require 'jsonapi/relationship'
23
22
  require 'jsonapi/include_directives'
23
+ require 'jsonapi/operation'
24
24
  require 'jsonapi/operation_result'
25
- require 'jsonapi/operation_results'
26
25
  require 'jsonapi/callbacks'
27
26
  require 'jsonapi/link_builder'
27
+ require 'jsonapi/active_relation_resource_finder'
28
+ require 'jsonapi/active_relation_resource_finder/join_tree'
29
+ require 'jsonapi/resource_identity'
30
+ require 'jsonapi/resource_fragment'
31
+ require 'jsonapi/resource_id_tree'
32
+ require 'jsonapi/resource_set'
@@ -0,0 +1,640 @@
1
+ module JSONAPI
2
+ module ActiveRelationResourceFinder
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ # Finds Resources using the `filters`. Pagination and sort options are used when provided
10
+ #
11
+ # @param filters [Hash] the filters hash
12
+ # @option options [Hash] :context The context of the request, set in the controller
13
+ # @option options [Hash] :sort_criteria The `sort criteria`
14
+ # @option options [Hash] :include_directives The `include_directives`
15
+ #
16
+ # @return [Array<Resource>] the Resource instances matching the filters, sorting and pagination rules.
17
+ def find(filters, options = {})
18
+ records = find_records(filters, options)
19
+ resources_for(records, options[:context])
20
+ end
21
+
22
+ # Counts Resources found using the `filters`
23
+ #
24
+ # @param filters [Hash] the filters hash
25
+ # @option options [Hash] :context The context of the request, set in the controller
26
+ #
27
+ # @return [Integer] the count
28
+ def count(filters, options = {})
29
+ count_records(filter_records(records(options), filters, options))
30
+ end
31
+
32
+ # Returns the single Resource identified by `key`
33
+ #
34
+ # @param key the primary key of the resource to find
35
+ # @option options [Hash] :context The context of the request, set in the controller
36
+ def find_by_key(key, options = {})
37
+ record = find_record_by_key(key, options)
38
+ resource_for(record, options[:context])
39
+ end
40
+
41
+ # Returns an array of Resources identified by the `keys` array
42
+ #
43
+ # @param keys [Array<key>] Array of primary keys to find resources for
44
+ # @option options [Hash] :context The context of the request, set in the controller
45
+ def find_by_keys(keys, options = {})
46
+ records = find_records_by_keys(keys, options)
47
+ resources_for(records, options[:context])
48
+ end
49
+
50
+ # Finds Resource fragments using the `filters`. Pagination and sort options are used when provided.
51
+ # Retrieving the ResourceIdentities and attributes does not instantiate a model instance.
52
+ #
53
+ # @param filters [Hash] the filters hash
54
+ # @option options [Hash] :context The context of the request, set in the controller
55
+ # @option options [Hash] :sort_criteria The `sort criteria`
56
+ # @option options [Hash] :include_directives The `include_directives`
57
+ # @option options [Hash] :attributes Additional fields to be retrieved.
58
+ # @option options [Boolean] :cache Return the resources' cache field
59
+ #
60
+ # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, attributes: => {name => value}}}]
61
+ # the ResourceInstances matching the filters, sorting, and pagination rules along with any request
62
+ # additional_field values
63
+ def find_fragments(filters, options = {})
64
+ records = find_records(filters, options)
65
+
66
+ table_name = _model_class.table_name
67
+ pluck_fields = [Arel.sql("#{concat_table_field(table_name, _primary_key)} AS #{table_name}_#{_primary_key}")]
68
+
69
+ cache_field = attribute_to_model_field(:_cache_field) if options[:cache]
70
+ if cache_field
71
+ pluck_fields << Arel.sql("#{concat_table_field(table_name, cache_field[:name])} AS #{table_name}_#{cache_field[:name]}")
72
+ end
73
+
74
+ model_fields = {}
75
+ attributes = options[:attributes]
76
+ attributes.try(:each) do |attribute|
77
+ model_field = attribute_to_model_field(attribute)
78
+ model_fields[attribute] = model_field
79
+ pluck_fields << Arel.sql("#{concat_table_field(table_name, model_field[:name])} AS #{table_name}_#{model_field[:name]}")
80
+ end
81
+
82
+ fragments = {}
83
+ records.pluck(*pluck_fields).collect do |row|
84
+ rid = JSONAPI::ResourceIdentity.new(self, pluck_fields.length == 1 ? row : row[0])
85
+
86
+ fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
87
+ attributes_offset = 1
88
+
89
+ if cache_field
90
+ fragments[rid].cache = cast_to_attribute_type(row[1], cache_field[:type])
91
+ attributes_offset+= 1
92
+ end
93
+
94
+ model_fields.each_with_index do |k, idx|
95
+ fragments[rid].attributes[k[0]]= cast_to_attribute_type(row[idx + attributes_offset], k[1][:type])
96
+ end
97
+ end
98
+
99
+ fragments
100
+ end
101
+
102
+ # Finds Resource Fragments related to the source resources through the specified relationship
103
+ #
104
+ # @param source_rids [Array<ResourceIdentity>] The resources to find related ResourcesIdentities for
105
+ # @param relationship_name [String | Symbol] The name of the relationship
106
+ # @option options [Hash] :context The context of the request, set in the controller
107
+ # @option options [Hash] :attributes Additional fields to be retrieved.
108
+ # @option options [Boolean] :cache Return the resources' cache field
109
+ #
110
+ # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, attributes: => {name => value}, related: {relationship_name: [] }}}]
111
+ # the ResourceInstances matching the filters, sorting, and pagination rules along with any request
112
+ # additional_field values
113
+ def find_related_fragments(source_rids, relationship_name, options = {})
114
+ relationship = _relationship(relationship_name)
115
+
116
+ if relationship.polymorphic? && relationship.foreign_key_on == :self
117
+ find_related_polymorphic_fragments(source_rids, relationship, options, false)
118
+ else
119
+ find_related_monomorphic_fragments(source_rids, relationship, options, false)
120
+ end
121
+ end
122
+
123
+ def find_included_fragments(source_rids, relationship_name, options = {})
124
+ relationship = _relationship(relationship_name)
125
+
126
+ if relationship.polymorphic? && relationship.foreign_key_on == :self
127
+ find_related_polymorphic_fragments(source_rids, relationship, options, true)
128
+ else
129
+ find_related_monomorphic_fragments(source_rids, relationship, options, true)
130
+ end
131
+ end
132
+
133
+ # Counts Resources related to the source resource through the specified relationship
134
+ #
135
+ # @param source_rid [ResourceIdentity] Source resource identifier
136
+ # @param relationship_name [String | Symbol] The name of the relationship
137
+ # @option options [Hash] :context The context of the request, set in the controller
138
+ #
139
+ # @return [Integer] the count
140
+ def count_related(source_rid, relationship_name, options = {})
141
+ opts = options.dup
142
+
143
+ relationship = _relationship(relationship_name)
144
+ related_klass = relationship.resource_klass
145
+
146
+ context = opts[:context]
147
+
148
+ primary_key_field = "#{_table_name}.#{_primary_key}"
149
+
150
+ records = records(context: context).where(primary_key_field => source_rid.id)
151
+
152
+ # join in related to the source records
153
+ records, related_alias = get_join_alias(records) { |records| records.joins(relationship.relation_name(opts)) }
154
+
155
+ join_tree = JoinTree.new(resource_klass: related_klass,
156
+ source_relationship: relationship,
157
+ filters: filters,
158
+ options: opts)
159
+
160
+ records, joins = apply_joins(records, join_tree, opts)
161
+
162
+ # Options for filtering
163
+ opts[:joins] = joins
164
+ opts[:related_alias] = related_alias
165
+
166
+ filters = opts.fetch(:filters, {})
167
+ records = related_klass.filter_records(records, filters, opts)
168
+
169
+ records.count(:all)
170
+ end
171
+
172
+ def parse_relationship_path(path)
173
+ relationships = []
174
+ relationship_names = []
175
+ field = nil
176
+
177
+ current_path = path
178
+ current_resource_klass = self
179
+ loop do
180
+ parts = current_path.to_s.partition('.')
181
+ relationship = current_resource_klass._relationship(parts[0])
182
+ if relationship
183
+ relationships << relationship
184
+ relationship_names << relationship.name
185
+ else
186
+ if parts[2].blank?
187
+ field = parts[0]
188
+ break
189
+ else
190
+ # :nocov:
191
+ warn "Unknown relationship #{parts[0]}"
192
+ # :nocov:
193
+ end
194
+ end
195
+
196
+ current_resource_klass = relationship.resource_klass
197
+
198
+ if parts[2].include?('.')
199
+ current_path = parts[2]
200
+ else
201
+ relationship = current_resource_klass._relationship(parts[2])
202
+ if relationship
203
+ relationships << relationship
204
+ relationship_names << relationship.name
205
+ else
206
+ field = parts[2]
207
+ end
208
+ break
209
+ end
210
+ end
211
+
212
+ return relationships, relationship_names.join('.'), field
213
+ end
214
+
215
+ protected
216
+
217
+ def find_record_by_key(key, options = {})
218
+ records = find_records({ _primary_key => key }, options.except(:paginator, :sort_criteria))
219
+ record = records.first
220
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if record.nil?
221
+ record
222
+ end
223
+
224
+ def find_records_by_keys(keys, options = {})
225
+ records(options).where({ _primary_key => keys })
226
+ end
227
+
228
+ def find_related_monomorphic_fragments(source_rids, relationship, options, connect_source_identity)
229
+ opts = options.dup
230
+
231
+ source_ids = source_rids.collect {|rid| rid.id}
232
+
233
+ context = opts[:context]
234
+
235
+ related_klass = relationship.resource_klass
236
+
237
+ primary_key_field = "#{_table_name}.#{_primary_key}"
238
+
239
+ records = records(context: context).where(primary_key_field => source_ids)
240
+
241
+ # join in related to the source records
242
+ records, related_alias = get_join_alias(records) { |records| records.joins(relationship.relation_name(opts)) }
243
+
244
+ sort_criteria = []
245
+ opts[:sort_criteria].try(:each) do |sort|
246
+ field = sort[:field].to_s == 'id' ? related_klass._primary_key : sort[:field]
247
+ sort_criteria << { field: field, direction: sort[:direction] }
248
+ end
249
+
250
+ paginator = opts[:paginator]
251
+
252
+ filters = opts.fetch(:filters, {})
253
+
254
+ # Joins in this case are related to the related_klass
255
+ join_tree = JoinTree.new(resource_klass: related_klass,
256
+ source_relationship: relationship,
257
+ filters: filters,
258
+ sort_criteria: sort_criteria,
259
+ options: opts)
260
+
261
+ records, joins = apply_joins(records, join_tree, opts)
262
+
263
+ # Options for filtering
264
+ opts[:joins] = joins
265
+ opts[:related_alias] = related_alias
266
+
267
+ records = related_klass.filter_records(records, filters, opts)
268
+
269
+ order_options = related_klass.construct_order_options(sort_criteria)
270
+
271
+ # ToDO: Remove count check. Currently pagination isn't working with multiple source_rids (i.e. it only works
272
+ # for show relationships, not related includes).
273
+ if paginator && source_rids.count == 1
274
+ records = related_klass.apply_pagination(records, paginator, order_options)
275
+ end
276
+
277
+ records = sort_records(records, order_options, opts)
278
+
279
+ pluck_fields = [
280
+ Arel.sql(primary_key_field),
281
+ Arel.sql("#{concat_table_field(related_alias, related_klass._primary_key)} AS #{related_alias}_#{related_klass._primary_key}")
282
+ ]
283
+
284
+ cache_field = related_klass.attribute_to_model_field(:_cache_field) if opts[:cache]
285
+ if cache_field
286
+ pluck_fields << Arel.sql("#{concat_table_field(related_alias, cache_field[:name])} AS #{related_alias}_#{cache_field[:name]}")
287
+ end
288
+
289
+ model_fields = {}
290
+ attributes = opts[:attributes]
291
+ attributes.try(:each) do |attribute|
292
+ model_field = related_klass.attribute_to_model_field(attribute)
293
+ model_fields[attribute] = model_field
294
+ pluck_fields << Arel.sql("#{concat_table_field(related_alias, model_field[:name])} AS #{related_alias}_#{model_field[:name]}")
295
+ end
296
+
297
+ rows = records.pluck(*pluck_fields)
298
+
299
+ related_fragments = {}
300
+
301
+ rows.each do |row|
302
+ unless row[1].nil?
303
+ rid = JSONAPI::ResourceIdentity.new(related_klass, row[1])
304
+
305
+ related_fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
306
+
307
+ attributes_offset = 2
308
+
309
+ if cache_field
310
+ related_fragments[rid].cache = cast_to_attribute_type(row[attributes_offset], cache_field[:type])
311
+ attributes_offset+= 1
312
+ end
313
+
314
+ model_fields.each_with_index do |k, idx|
315
+ related_fragments[rid].attributes[k[0]] = cast_to_attribute_type(row[idx + attributes_offset], k[1][:type])
316
+ end
317
+
318
+ source_rid = JSONAPI::ResourceIdentity.new(self, row[0])
319
+
320
+ related_fragments[rid].add_related_from(source_rid)
321
+
322
+ if connect_source_identity
323
+ related_relationship = related_klass._relationships[relationship.inverse_relationship]
324
+ if related_relationship
325
+ related_fragments[rid].add_related_identity(related_relationship.name, source_rid)
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ related_fragments
332
+ end
333
+
334
+ # Gets resource identities where the related resource is polymorphic and the resource type and id
335
+ # are stored on the primary resources. Cache fields will always be on the related resources.
336
+ def find_related_polymorphic_fragments(source_rids, relationship, options, connect_source_identity)
337
+ source_ids = source_rids.collect {|rid| rid.id}
338
+
339
+ context = options[:context]
340
+
341
+ records = records(context: context)
342
+
343
+ primary_key = concat_table_field(_table_name, _primary_key)
344
+ related_key = concat_table_field(_table_name, relationship.foreign_key)
345
+ related_type = concat_table_field(_table_name, relationship.polymorphic_type)
346
+
347
+ pluck_fields = [
348
+ Arel.sql("#{primary_key} AS #{_table_name}_#{_primary_key}"),
349
+ Arel.sql("#{related_key} AS #{_table_name}_#{relationship.foreign_key}"),
350
+ Arel.sql("#{related_type} AS #{_table_name}_#{relationship.polymorphic_type}")
351
+ ]
352
+
353
+ relations = relationship.polymorphic_relations
354
+
355
+ # Get the additional fields from each relation. There's a limitation that the fields must exist in each relation
356
+
357
+ relation_positions = {}
358
+ relation_index = 3
359
+
360
+ attributes = options.fetch(:attributes, [])
361
+
362
+ if relations.nil? || relations.length == 0
363
+ # :nocov:
364
+ warn "No relations found for polymorphic relationship."
365
+ # :nocov:
366
+ else
367
+ relations.try(:each) do |relation|
368
+ related_klass = resource_klass_for(relation.to_s)
369
+
370
+ cache_field = related_klass.attribute_to_model_field(:_cache_field) if options[:cache]
371
+
372
+ # We only need to join the relations if we are getting additional fields
373
+ if cache_field || attributes.length > 0
374
+ records, table_alias = get_join_alias(records) { |records| records.left_joins(relation.to_sym) }
375
+
376
+ if cache_field
377
+ pluck_fields << concat_table_field(table_alias, cache_field[:name])
378
+ end
379
+
380
+ model_fields = {}
381
+ attributes.try(:each) do |attribute|
382
+ model_field = related_klass.attribute_to_model_field(attribute)
383
+ model_fields[attribute] = model_field
384
+ end
385
+
386
+ model_fields.each do |_k, v|
387
+ pluck_fields << concat_table_field(table_alias, v[:name])
388
+ end
389
+
390
+ end
391
+
392
+ related = related_klass._model_class.name
393
+ relation_positions[related] = { relation_klass: related_klass,
394
+ cache_field: cache_field,
395
+ model_fields: model_fields,
396
+ field_offset: relation_index}
397
+
398
+ relation_index+= 1 if cache_field
399
+ relation_index+= attributes.length if attributes.length > 0
400
+ end
401
+ end
402
+
403
+ primary_resource_filters = options[:filters]
404
+ primary_resource_filters ||= {}
405
+
406
+ primary_resource_filters[_primary_key] = source_ids
407
+
408
+ records = apply_filters(records, primary_resource_filters, options)
409
+
410
+ rows = records.pluck(*pluck_fields)
411
+
412
+ related_fragments = {}
413
+
414
+ rows.each do |row|
415
+ unless row[1].nil? || row[2].nil?
416
+ related_klass = resource_klass_for(row[2])
417
+
418
+ rid = JSONAPI::ResourceIdentity.new(related_klass, row[1])
419
+ related_fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
420
+
421
+ source_rid = JSONAPI::ResourceIdentity.new(self, row[0])
422
+ related_fragments[rid].add_related_from(source_rid)
423
+
424
+ if connect_source_identity
425
+ related_relationship = related_klass._relationships[relationship.inverse_relationship]
426
+ if related_relationship
427
+ related_fragments[rid].add_related_identity(related_relationship.name, source_rid)
428
+ end
429
+ end
430
+
431
+ relation_position = relation_positions[row[2]]
432
+ model_fields = relation_position[:model_fields]
433
+ cache_field = relation_position[:cache_field]
434
+ field_offset = relation_position[:field_offset]
435
+
436
+ attributes_offset = 0
437
+
438
+ if cache_field
439
+ related_fragments[rid].cache = cast_to_attribute_type(row[field_offset], cache_field[:type])
440
+ attributes_offset+= 1
441
+ end
442
+
443
+ if attributes.length > 0
444
+ model_fields.each_with_index do |k, idx|
445
+ related_fragments[rid].add_attribute(k[0], cast_to_attribute_type(row[idx + field_offset + attributes_offset], k[1][:type]))
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ related_fragments
452
+ end
453
+
454
+ def find_records(filters, options = {})
455
+ opts = options.dup
456
+
457
+ sort_criteria = opts.fetch(:sort_criteria) { [] }
458
+
459
+ join_tree = JoinTree.new(resource_klass: self,
460
+ filters: filters,
461
+ sort_criteria: sort_criteria,
462
+ options: opts)
463
+
464
+ records, joins = apply_joins(records(opts), join_tree, opts)
465
+
466
+ opts[:joins] = joins
467
+
468
+ records = filter_records(records, filters, opts)
469
+
470
+ order_options = construct_order_options(sort_criteria)
471
+ records = sort_records(records, order_options, opts)
472
+
473
+ records = apply_pagination(records, opts[:paginator], order_options)
474
+
475
+ records.distinct
476
+ end
477
+
478
+ def get_join_alias(records, &block)
479
+ init_join_sources = records.arel.join_sources
480
+ init_join_sources_length = init_join_sources.length
481
+
482
+ records = yield(records)
483
+
484
+ join_sources = records.arel.join_sources
485
+ if join_sources.length > init_join_sources_length
486
+ last_join = (join_sources - init_join_sources).last
487
+ join_alias =
488
+ case last_join.left
489
+ when Arel::Table
490
+ last_join.left.name
491
+ when Arel::Nodes::TableAlias
492
+ last_join.left.right
493
+ when Arel::Nodes::StringJoin
494
+ # :nocov:
495
+ warn "get_join_alias: Unsupported join type - use custom filtering and sorting"
496
+ nil
497
+ # :nocov:
498
+ end
499
+ else
500
+ # :nocov:
501
+ warn "get_join_alias: No join added"
502
+ join_alias = nil
503
+ # :nocov:
504
+ end
505
+
506
+ return records, join_alias
507
+ end
508
+
509
+ def apply_joins(records, join_tree, _options)
510
+ joins = join_tree.get_joins
511
+
512
+ joins.each do |key, join_details|
513
+ case join_details[:join_type]
514
+ when :inner
515
+ records, join_alias = get_join_alias(records) { |records| records.joins(join_details[:relation_join_hash]) }
516
+ when :left
517
+ records, join_alias = get_join_alias(records) { |records| records.left_joins(join_details[:relation_join_hash]) }
518
+ end
519
+
520
+ joins[key][:alias] = join_alias
521
+ end
522
+
523
+ return records, joins
524
+ end
525
+
526
+ def apply_pagination(records, paginator, order_options)
527
+ records = paginator.apply(records, order_options) if paginator
528
+ records
529
+ end
530
+
531
+ def apply_sort(records, order_options, options)
532
+ if order_options.any?
533
+ order_options.each_pair do |field, direction|
534
+ records = apply_single_sort(records, field, direction, options)
535
+ end
536
+ end
537
+
538
+ records
539
+ end
540
+
541
+ def apply_single_sort(records, field, direction, options)
542
+ context = options[:context]
543
+
544
+ strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]
545
+
546
+ if strategy
547
+ call_method_or_proc(strategy, records, direction, context)
548
+ else
549
+ joins = options[:joins] || {}
550
+
551
+ records.order("#{get_aliased_field(field, joins, options[:related_alias])} #{direction}")
552
+ end
553
+ end
554
+
555
+ # Assumes ActiveRecord's counting. Override if you need a different counting method
556
+ def count_records(records)
557
+ records.count(:all)
558
+ end
559
+
560
+ def filter_records(records, filters, options)
561
+ apply_filters(records, filters, options)
562
+ end
563
+
564
+ def sort_records(records, order_options, options)
565
+ apply_sort(records, order_options, options)
566
+ end
567
+
568
+ def concat_table_field(table, field, quoted = false)
569
+ if table.blank? || field.to_s.include?('.')
570
+ # :nocov:
571
+ if quoted
572
+ "\"#{field.to_s}\""
573
+ else
574
+ field.to_s
575
+ end
576
+ # :nocov:
577
+ else
578
+ if quoted
579
+ # :nocov:
580
+ "\"#{table.to_s}\".\"#{field.to_s}\""
581
+ # :nocov:
582
+ else
583
+ "#{table.to_s}.#{field.to_s}"
584
+ end
585
+ end
586
+ end
587
+
588
+ def apply_filters(records, filters, options = {})
589
+ if filters
590
+ filters.each do |filter, value|
591
+ records = apply_filter(records, filter, value, options)
592
+ end
593
+ end
594
+
595
+ records
596
+ end
597
+
598
+ def get_aliased_field(path_with_field, joins, related_alias)
599
+ relationships, relationship_path, field = parse_relationship_path(path_with_field)
600
+ relationship = relationships.last
601
+
602
+ resource_klass = relationship ? relationship.resource_klass : self
603
+
604
+ if field.empty?
605
+ field_name = resource_klass._primary_key
606
+ else
607
+ field_name = resource_klass._attribute_delegated_name(field)
608
+ end
609
+
610
+ if relationship
611
+ join_name = relationship_path
612
+
613
+ join = joins.try(:[], join_name)
614
+
615
+ table_alias = join.try(:[], :alias)
616
+ else
617
+ table_alias = related_alias
618
+ end
619
+
620
+ table_alias ||= resource_klass._table_name
621
+
622
+ concat_table_field(table_alias, field_name)
623
+ end
624
+
625
+ def apply_filter(records, filter, value, options = {})
626
+ strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
627
+
628
+ if strategy
629
+ records = call_method_or_proc(strategy, records, value, options)
630
+ else
631
+ joins = options[:joins] || {}
632
+ related_alias = options[:related_alias]
633
+ records = records.where(get_aliased_field(filter, joins, related_alias) => value)
634
+ end
635
+
636
+ records
637
+ end
638
+ end
639
+ end
640
+ end