jsonapi-resources 0.9.12 → 0.10.0.beta1

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 (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