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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a367b0f58fd1570cdc45f28570f440ab2b68c1c7
4
- data.tar.gz: 48fdfb4691776dea1491b237c7f9fc1eca401072
2
+ SHA256:
3
+ metadata.gz: '091836afd689ed975ab1ad2cee54a467a0771cb5ea5b0bfa2055e25531809f74'
4
+ data.tar.gz: 96d46c67f8c88fa3e6d298eac85dc3862bef6feacbe8eddde1515b80e7c8e5a8
5
5
  SHA512:
6
- metadata.gz: a1664c0f610a4baaa22c80642575cdb177910476d0d3154bf259902e24b27261ee26b2f385820a360bb50a229a5e74c04ed55fd69f6507ab3596db6cf5324cc9
7
- data.tar.gz: b372447e19a732fadcdb1154894f963460187a2b92e5481df411b798b27347808f4d79f90c51f49b6198a95cd7c1e2d05901ca5a42463064ee89340c80ffa4ad
6
+ metadata.gz: f4dfc6fd61f4a7d27a291626b00c6e5c13cad32be0b73b66e239f254800d73557168470191923cd5bcf29869bfdb1e93a11c4a8b6e3eca25411b1ac8dbc4e24d
7
+ data.tar.gz: e6c699748872330b22ae151082595e48bed10665e9c61c3ba902f585747090e19ab9319776d189bc06fe9ca7797f00467fb08a3e863a4ce7e3b740fd85bc2fe8
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
@@ -0,0 +1,27 @@
1
+ module JSONAPI
2
+ module ActiveRelation
3
+ module Adapters
4
+ module JoinLeftActiveRecordAdapter
5
+
6
+ # Extends left_joins functionality to rails 4, and uses the same logic for rails 5.0.x and 5.1.x
7
+ # The default left_joins logic of rails 5.2.x is used. This results in and extra join in some cases. For
8
+ # example Post.joins(:comments).joins_left(comments: :author) will join the comments table twice,
9
+ # once inner and once left in 5.2, but only as inner in earlier versions.
10
+ def joins_left(*columns)
11
+ if Rails::VERSION::MAJOR >= 6 || (Rails::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2)
12
+ left_joins(columns)
13
+ else
14
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(self, columns, [])
15
+ joins(join_dependency)
16
+ end
17
+ end
18
+
19
+ alias_method :join_left, :joins_left
20
+ end
21
+
22
+ if defined?(ActiveRecord)
23
+ ActiveRecord::Base.extend JoinLeftActiveRecordAdapter
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,303 @@
1
+ module JSONAPI
2
+ module ActiveRelation
3
+
4
+ # Stores relationship paths starting from the resource_klass, consolidating duplicate paths from
5
+ # relationships, filters and sorts. When joins are made the table aliases are tracked in join_details
6
+ class JoinManager
7
+ attr_reader :resource_klass,
8
+ :source_relationship,
9
+ :resource_join_tree,
10
+ :join_details
11
+
12
+ def initialize(resource_klass:,
13
+ source_relationship: nil,
14
+ relationships: nil,
15
+ filters: nil,
16
+ sort_criteria: nil)
17
+
18
+ @resource_klass = resource_klass
19
+ @join_details = nil
20
+ @collected_aliases = Set.new
21
+
22
+ @resource_join_tree = {
23
+ root: {
24
+ join_type: :root,
25
+ resource_klasses: {
26
+ resource_klass => {
27
+ relationships: {}
28
+ }
29
+ }
30
+ }
31
+ }
32
+ add_source_relationship(source_relationship)
33
+ add_sort_criteria(sort_criteria)
34
+ add_filters(filters)
35
+ add_relationships(relationships)
36
+ end
37
+
38
+ def join(records, options)
39
+ fail "can't be joined again" if @join_details
40
+ @join_details = {}
41
+ perform_joins(records, options)
42
+ end
43
+
44
+ # source details will only be on a relationship if the source_relationship is set
45
+ # this method gets the join details whether they are on a relationship or are just pseudo details for the base
46
+ # resource. Specify the resource type for polymorphic relationships
47
+ #
48
+ def source_join_details(type=nil)
49
+ if source_relationship
50
+ related_resource_klass = type ? resource_klass.resource_klass_for(type) : source_relationship.resource_klass
51
+ segment = PathSegment::Relationship.new(relationship: source_relationship, resource_klass: related_resource_klass)
52
+ details = @join_details[segment]
53
+ else
54
+ if type
55
+ details = @join_details["##{type}"]
56
+ else
57
+ details = @join_details['']
58
+ end
59
+ end
60
+ details
61
+ end
62
+
63
+ def join_details_by_polymorphic_relationship(relationship, type)
64
+ segment = PathSegment::Relationship.new(relationship: relationship, resource_klass: resource_klass.resource_klass_for(type))
65
+ @join_details[segment]
66
+ end
67
+
68
+ def join_details_by_relationship(relationship)
69
+ segment = PathSegment::Relationship.new(relationship: relationship, resource_klass: relationship.resource_klass)
70
+ @join_details[segment]
71
+ end
72
+
73
+ def self.get_join_arel_node(records, options = {})
74
+ init_join_sources = records.arel.join_sources
75
+ init_join_sources_length = init_join_sources.length
76
+
77
+ records = yield(records, options)
78
+
79
+ join_sources = records.arel.join_sources
80
+ if join_sources.length > init_join_sources_length
81
+ last_join = (join_sources - init_join_sources).last
82
+ else
83
+ # :nocov:
84
+ warn "get_join_arel_node: No join added"
85
+ last_join = nil
86
+ # :nocov:
87
+ end
88
+
89
+ return records, last_join
90
+ end
91
+
92
+ def self.alias_from_arel_node(node)
93
+ case node.left
94
+ when Arel::Table
95
+ node.left.name
96
+ when Arel::Nodes::TableAlias
97
+ node.left.right
98
+ when Arel::Nodes::StringJoin
99
+ # :nocov:
100
+ warn "alias_from_arel_node: Unsupported join type - use custom filtering and sorting"
101
+ nil
102
+ # :nocov:
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def flatten_join_tree_by_depth(join_array = [], node = @resource_join_tree, level = 0)
109
+ join_array[level] = [] unless join_array[level]
110
+
111
+ node.each do |relationship, relationship_details|
112
+ relationship_details[:resource_klasses].each do |related_resource_klass, resource_details|
113
+ join_array[level] << { relationship: relationship,
114
+ relationship_details: relationship_details,
115
+ related_resource_klass: related_resource_klass}
116
+ flatten_join_tree_by_depth(join_array, resource_details[:relationships], level+1)
117
+ end
118
+ end
119
+ join_array
120
+ end
121
+
122
+ def add_join_details(join_key, details, check_for_duplicate_alias = true)
123
+ fail "details already set" if @join_details.has_key?(join_key)
124
+ @join_details[join_key] = details
125
+
126
+ # Joins are being tracked as they are added to the built up relation. If the same table is added to a
127
+ # relation more than once subsequent versions will be assigned an alias. Depending on the order the joins
128
+ # are made the computed aliases may change. The order this library performs the joins was chosen
129
+ # to prevent this. However if the relation is reordered it should result in reusing on of the earlier
130
+ # aliases (in this case a plain table name). The following check will catch this an raise an exception.
131
+ # An exception is appropriate because not using the correct alias could leak data due to filters and
132
+ # applied permissions being performed on the wrong data.
133
+ if check_for_duplicate_alias && @collected_aliases.include?(details[:alias])
134
+ fail "alias '#{details[:alias]}' has already been added. Possible relation reordering"
135
+ end
136
+
137
+ @collected_aliases << details[:alias]
138
+ end
139
+
140
+ def perform_joins(records, options)
141
+ join_array = flatten_join_tree_by_depth
142
+
143
+ join_array.each do |level_joins|
144
+ level_joins.each do |join_details|
145
+ relationship = join_details[:relationship]
146
+ relationship_details = join_details[:relationship_details]
147
+ related_resource_klass = join_details[:related_resource_klass]
148
+ join_type = relationship_details[:join_type]
149
+
150
+ join_options = {
151
+ relationship: relationship,
152
+ relationship_details: relationship_details,
153
+ related_resource_klass: related_resource_klass,
154
+ }
155
+
156
+ if relationship == :root
157
+ unless source_relationship
158
+ add_join_details('', {alias: resource_klass._table_name, join_type: :root, join_options: join_options})
159
+ end
160
+ next
161
+ end
162
+
163
+ records, join_node = self.class.get_join_arel_node(records, options) {|records, options|
164
+ related_resource_klass.join_relationship(
165
+ records: records,
166
+ resource_type: related_resource_klass._type,
167
+ join_type: join_type,
168
+ relationship: relationship,
169
+ options: options)
170
+ }
171
+
172
+ details = {alias: self.class.alias_from_arel_node(join_node), join_type: join_type, join_options: join_options}
173
+
174
+ if relationship == source_relationship
175
+ if relationship.polymorphic? && relationship.belongs_to?
176
+ add_join_details("##{related_resource_klass._type}", details)
177
+ else
178
+ add_join_details('', details)
179
+ end
180
+ end
181
+
182
+ # We're adding the source alias with two keys. We only want the check for duplicate aliases once.
183
+ # See the note in `add_join_details`.
184
+ check_for_duplicate_alias = !(relationship == source_relationship)
185
+ add_join_details(PathSegment::Relationship.new(relationship: relationship, resource_klass: related_resource_klass), details, check_for_duplicate_alias)
186
+ end
187
+ end
188
+ records
189
+ end
190
+
191
+ def add_join(path, default_type = :inner, default_polymorphic_join_type = :left)
192
+ if source_relationship
193
+ if source_relationship.polymorphic?
194
+ # Polymorphic paths will come it with the resource_type as the first segment (for example `#documents.comments`)
195
+ # We just need to prepend the relationship portion the
196
+ sourced_path = "#{source_relationship.name}#{path}"
197
+ else
198
+ sourced_path = "#{source_relationship.name}.#{path}"
199
+ end
200
+ else
201
+ sourced_path = path
202
+ end
203
+
204
+ join_manager, _field = parse_path_to_tree(sourced_path, resource_klass, default_type, default_polymorphic_join_type)
205
+
206
+ @resource_join_tree[:root].deep_merge!(join_manager) { |key, val, other_val|
207
+ if key == :join_type
208
+ if val == other_val
209
+ val
210
+ else
211
+ :inner
212
+ end
213
+ end
214
+ }
215
+ end
216
+
217
+ def process_path_to_tree(path_segments, resource_klass, default_join_type, default_polymorphic_join_type)
218
+ node = {
219
+ resource_klasses: {
220
+ resource_klass => {
221
+ relationships: {}
222
+ }
223
+ }
224
+ }
225
+
226
+ segment = path_segments.shift
227
+
228
+ if segment.is_a?(PathSegment::Relationship)
229
+ node[:resource_klasses][resource_klass][:relationships][segment.relationship] ||= {}
230
+
231
+ # join polymorphic as left joins
232
+ node[:resource_klasses][resource_klass][:relationships][segment.relationship][:join_type] ||=
233
+ segment.relationship.polymorphic? ? default_polymorphic_join_type : default_join_type
234
+
235
+ segment.relationship.resource_types.each do |related_resource_type|
236
+ related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
237
+
238
+ # If the resource type was specified in the path segment we want to only process the next segments for
239
+ # that resource type, otherwise process for all
240
+ process_all_types = !segment.path_specified_resource_klass?
241
+
242
+ if process_all_types || related_resource_klass == segment.resource_klass
243
+ related_resource_tree = process_path_to_tree(path_segments.dup, related_resource_klass, default_join_type, default_polymorphic_join_type)
244
+ node[:resource_klasses][resource_klass][:relationships][segment.relationship].deep_merge!(related_resource_tree)
245
+ end
246
+ end
247
+ end
248
+ node
249
+ end
250
+
251
+ def parse_path_to_tree(path_string, resource_klass, default_join_type = :inner, default_polymorphic_join_type = :left)
252
+ path = JSONAPI::Path.new(resource_klass: resource_klass, path_string: path_string)
253
+
254
+ field = path.segments[-1]
255
+ return process_path_to_tree(path.segments, resource_klass, default_join_type, default_polymorphic_join_type), field
256
+ end
257
+
258
+ def add_source_relationship(source_relationship)
259
+ @source_relationship = source_relationship
260
+
261
+ if @source_relationship
262
+ resource_klasses = {}
263
+ source_relationship.resource_types.each do |related_resource_type|
264
+ related_resource_klass = resource_klass.resource_klass_for(related_resource_type)
265
+ resource_klasses[related_resource_klass] = {relationships: {}}
266
+ end
267
+
268
+ join_type = source_relationship.polymorphic? ? :left : :inner
269
+
270
+ @resource_join_tree[:root][:resource_klasses][resource_klass][:relationships][@source_relationship] = {
271
+ source: true, resource_klasses: resource_klasses, join_type: join_type
272
+ }
273
+ end
274
+ end
275
+
276
+ def add_filters(filters)
277
+ return if filters.blank?
278
+ filters.each_key do |filter|
279
+ # Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
280
+ next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
281
+ !resource_klass._allowed_filters[filter].try(:[], :perform_joins)
282
+
283
+ add_join(filter, :left)
284
+ end
285
+ end
286
+
287
+ def add_sort_criteria(sort_criteria)
288
+ return if sort_criteria.blank?
289
+
290
+ sort_criteria.each do |sort|
291
+ add_join(sort[:field], :left)
292
+ end
293
+ end
294
+
295
+ def add_relationships(relationships)
296
+ return if relationships.blank?
297
+ relationships.each do |relationship|
298
+ add_join(relationship, :left)
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end