graphiti 2.0.0.beta.3 → 2.0.0.beta.5
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +30 -86
- data/.github/workflows/docs.yml +57 -0
- data/.github/workflows/release.yml +8 -8
- data/.gitignore +7 -0
- data/.npmrc +9 -0
- data/.standard.yml +4 -1
- data/Appraisals +33 -32
- data/CHANGELOG.md +48 -0
- data/README.md +13 -2
- data/UPGRADING.md +2 -58
- data/docs/concepts/backends-and-models.md +122 -0
- data/docs/concepts/endpoints.md +183 -0
- data/docs/concepts/links.md +212 -0
- data/docs/concepts/overview.md +80 -0
- data/docs/concepts/persisting.md +376 -0
- data/docs/concepts/relationships.md +527 -0
- data/docs/concepts/resources.md +677 -0
- data/docs/getting-started/first-api.md +289 -0
- data/docs/getting-started/installation.md +185 -0
- data/docs/intro.md +307 -0
- data/docs/js/authentication.md +63 -0
- data/docs/js/ddau.md +20 -0
- data/docs/js/extra-params.md +41 -0
- data/docs/js/index.md +112 -0
- data/docs/js/installation.md +120 -0
- data/docs/js/middleware.md +72 -0
- data/docs/js/models.md +202 -0
- data/docs/js/reads.md +494 -0
- data/docs/js/state-syncing.md +100 -0
- data/docs/js/writes.md +373 -0
- data/docs/reference/vandal.md +63 -0
- data/docs/reference/why.md +13 -0
- data/docs/topics/authorization.md +155 -0
- data/docs/topics/caching.md +55 -0
- data/docs/topics/customizing-sideloads.md +156 -0
- data/docs/topics/debugging.md +216 -0
- data/docs/topics/error-handling.md +210 -0
- data/docs/topics/etags.md +46 -0
- data/docs/topics/hopping-relationships.md +149 -0
- data/docs/topics/json-attributes.md +77 -0
- data/docs/topics/openstruct-models.md +50 -0
- data/docs/topics/remote-resources.md +291 -0
- data/docs/topics/testing.md +894 -0
- data/docs/topics/without-activerecord.md +324 -0
- data/docs/tutorial/index.md +58 -0
- data/docs/tutorial/step_0.md +107 -0
- data/docs/tutorial/step_1.md +199 -0
- data/docs/tutorial/step_2.md +312 -0
- data/docs/tutorial/step_3.md +142 -0
- data/docs/tutorial/step_4.md +135 -0
- data/docs/tutorial/step_5.md +69 -0
- data/docs/tutorial/step_6.md +82 -0
- data/docs/tutorial/step_7.md +205 -0
- data/docs/tutorial/step_8.md +128 -0
- data/docs/tutorial/step_9.md +171 -0
- data/docs/upgrading.md +260 -0
- data/gemfiles/rails_7_1.gemfile +4 -3
- data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
- data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
- data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
- data/graphiti.gemspec +7 -23
- data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
- data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
- data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
- data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +68 -7
- data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +1 -1
- data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
- data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
- data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
- data/lib/graphiti/configuration.rb +2 -2
- data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
- data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
- data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
- data/lib/graphiti/error_serializers/validation.rb +143 -0
- data/lib/graphiti/errors.rb +4 -4
- data/lib/graphiti/query.rb +1 -1
- data/lib/graphiti/rails/context.rb +33 -0
- data/lib/graphiti/rails/controller.rb +41 -0
- data/lib/graphiti/rails/debugging.rb +18 -0
- data/lib/graphiti/rails/exception_handlers.rb +77 -0
- data/lib/graphiti/rails/railtie.rb +139 -0
- data/lib/graphiti/rails/rake_helpers.rb +42 -0
- data/lib/graphiti/rails/responders.rb +21 -0
- data/lib/graphiti/rails/test_helpers.rb +22 -0
- data/lib/graphiti/rails.rb +47 -29
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/interface.rb +2 -2
- data/lib/graphiti/resource/remote.rb +2 -2
- data/lib/graphiti/resource/sideloading.rb +1 -1
- data/lib/graphiti/resource.rb +13 -1
- data/lib/graphiti/responders.rb +7 -20
- data/lib/graphiti/schema.rb +5 -1
- data/lib/graphiti/schema_diff.rb +4 -0
- data/lib/graphiti/scope.rb +45 -37
- data/lib/graphiti/serializer.rb +6 -0
- data/lib/graphiti/sideload/belongs_to.rb +38 -5
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
- data/lib/graphiti/sideload.rb +54 -35
- data/lib/graphiti/spec_helpers/errors.rb +73 -0
- data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
- data/lib/graphiti/spec_helpers/helpers.rb +107 -0
- data/lib/graphiti/spec_helpers/matchers.rb +196 -0
- data/lib/graphiti/spec_helpers/node.rb +88 -0
- data/lib/graphiti/spec_helpers/rspec.rb +148 -0
- data/lib/graphiti/spec_helpers.rb +54 -0
- data/lib/graphiti/util/include_params.rb +2 -2
- data/lib/graphiti/util/serializer_relationships.rb +41 -5
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti-rails.rb +11 -0
- data/lib/graphiti.rb +34 -10
- data/lib/graphiti_errors.rb +11 -0
- data/lib/graphiti_spec_helpers/rspec.rb +3 -0
- data/lib/graphiti_spec_helpers.rb +11 -0
- data/lib/tasks/graphiti.rake +29 -0
- data/package-lock.json +6199 -0
- data/package.json +5 -4
- data/website/.gitignore +20 -0
- data/website/README.md +43 -0
- data/website/docusaurus.config.js +197 -0
- data/website/package-lock.json +19474 -0
- data/website/package.json +46 -0
- data/website/sidebars.js +82 -0
- data/website/src/css/custom.css +85 -0
- data/website/src/pages/markdown-page.mdx +7 -0
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
- data/website/static/1.13/2019/10/14/tutorial.html +198 -0
- data/website/static/1.13/CNAME +1 -0
- data/website/static/1.13/README.md +16 -0
- data/website/static/1.13/assets/css/syntax.css +60 -0
- data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
- data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
- data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
- data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
- data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
- data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
- data/website/static/1.13/assets/favicons/favicon.ico +0 -0
- data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
- data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
- data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
- data/website/static/1.13/assets/img/backend.gif +0 -0
- data/website/static/1.13/assets/img/conformity.png +0 -0
- data/website/static/1.13/assets/img/error_payload.png +0 -0
- data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +0 -0
- data/website/static/1.13/assets/img/lifecycle.gif +0 -0
- data/website/static/1.13/assets/img/logo-500.png +0 -0
- data/website/static/1.13/assets/img/logo.png +0 -0
- data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/meta_total_count.png +0 -0
- data/website/static/1.13/assets/img/persist.jpg +0 -0
- data/website/static/1.13/assets/img/resource.gif +0 -0
- data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/rest1.gif +0 -0
- data/website/static/1.13/assets/img/rest2.gif +0 -0
- data/website/static/1.13/assets/img/rest3.gif +0 -0
- data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/sunrise.png +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +2 -0
- data/website/static/1.13/assets/main.css +15518 -0
- data/website/static/1.13/assets/main.css.map +1 -0
- data/website/static/1.13/bin/bundle +109 -0
- data/website/static/1.13/bin/jekyll +27 -0
- data/website/static/1.13/bin/kramdown +27 -0
- data/website/static/1.13/bin/listen +27 -0
- data/website/static/1.13/bin/rake +27 -0
- data/website/static/1.13/bin/rougify +27 -0
- data/website/static/1.13/bin/safe_yaml +27 -0
- data/website/static/1.13/bin/sass +27 -0
- data/website/static/1.13/bin/sass-convert +27 -0
- data/website/static/1.13/bin/scss +27 -0
- data/website/static/1.13/blog.html +259 -0
- data/website/static/1.13/cheatsheet.html +316 -0
- data/website/static/1.13/cookbooks/authorization.md +0 -0
- data/website/static/1.13/cookbooks/caching.md +0 -0
- data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
- data/website/static/1.13/cookbooks/json_attributes.md +0 -0
- data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
- data/website/static/1.13/cookbooks/remote-resources.md +0 -0
- data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
- data/website/static/1.13/features.html +249 -0
- data/website/static/1.13/feed.xml +106 -0
- data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
- data/website/static/1.13/guides/concepts/debugging.html +440 -0
- data/website/static/1.13/guides/concepts/endpoints.html +432 -0
- data/website/static/1.13/guides/concepts/error-handling.html +396 -0
- data/website/static/1.13/guides/concepts/links.html +501 -0
- data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
- data/website/static/1.13/guides/concepts/resources.html +2176 -0
- data/website/static/1.13/guides/concepts/testing.html +1469 -0
- data/website/static/1.13/guides/getting-started/installation.html +420 -0
- data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
- data/website/static/1.13/guides/index.html +269 -0
- data/website/static/1.13/guides/overview.html +325 -0
- data/website/static/1.13/guides/upgrading-2-0.html +193 -0
- data/website/static/1.13/guides/upgrading.html +314 -0
- data/website/static/1.13/guides/vandal.html +282 -0
- data/website/static/1.13/guides/why.html +1121 -0
- data/website/static/1.13/index.html +72 -0
- data/website/static/1.13/js/authentication.html +295 -0
- data/website/static/1.13/js/ddau.html +238 -0
- data/website/static/1.13/js/extra-params.html +270 -0
- data/website/static/1.13/js/index.html +321 -0
- data/website/static/1.13/js/installation.html +637 -0
- data/website/static/1.13/js/introduction.html +257 -0
- data/website/static/1.13/js/middleware.html +318 -0
- data/website/static/1.13/js/reads/fieldsets.html +271 -0
- data/website/static/1.13/js/reads/filtering.html +289 -0
- data/website/static/1.13/js/reads/includes.html +260 -0
- data/website/static/1.13/js/reads/index.html +497 -0
- data/website/static/1.13/js/reads/nested-queries.html +353 -0
- data/website/static/1.13/js/reads/pagination.html +260 -0
- data/website/static/1.13/js/reads/sorting.html +265 -0
- data/website/static/1.13/js/reads/statistics.html +289 -0
- data/website/static/1.13/js/state-syncing.html +340 -0
- data/website/static/1.13/js/writes/deferred.html +296 -0
- data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
- data/website/static/1.13/js/writes/index.html +391 -0
- data/website/static/1.13/js/writes/nested.html +330 -0
- data/website/static/1.13/js/writes/validations.html +272 -0
- data/website/static/1.13/quickstart.html +660 -0
- data/website/static/1.13/template +161 -0
- data/website/static/1.13/tutorial/index.html +250 -0
- data/website/static/1.13/tutorial/step_0.html +292 -0
- data/website/static/1.13/tutorial/step_1.html +517 -0
- data/website/static/1.13/tutorial/step_2.html +481 -0
- data/website/static/1.13/tutorial/step_3.html +323 -0
- data/website/static/1.13/tutorial/step_4.html +318 -0
- data/website/static/1.13/tutorial/step_5.html +265 -0
- data/website/static/1.13/tutorial/step_6.html +276 -0
- data/website/static/1.13/tutorial/step_7.html +390 -0
- data/website/static/1.13/tutorial/step_8.html +316 -0
- data/website/static/1.13/tutorial/step_9.html +365 -0
- data/website/static/CNAME +1 -0
- data/website/static/assets/img/error_payload.png +0 -0
- data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
- data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
- data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
- data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
- data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
- data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
- data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
- data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
- data/website/static/assets/img/meta_total_count.png +0 -0
- data/website/static/img/docusaurus-social-card.jpg +0 -0
- data/website/static/img/docusaurus.png +0 -0
- data/website/static/img/favicon.ico +0 -0
- data/website/static/img/logo.png +0 -0
- data/website/static/img/logo.svg +1 -0
- data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
- data/website/static/img/undraw_docusaurus_react.svg +170 -0
- data/website/static/img/undraw_docusaurus_tree.svg +40 -0
- metadata +251 -54
- data/gemfiles/rails_6.gemfile +0 -18
- data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7.gemfile +0 -18
- data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
- data/lib/graphiti/deprecated_tasks.rb +0 -49
- data/lib/graphiti/railtie.rb +0 -121
- /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Graphiti
|
|
4
|
+
module Rails
|
|
5
|
+
# This Railtie exposes some configuration options:
|
|
6
|
+
# * `config.graphiti.handled_exception_formats`, defaulting to `[:jsonapi]`.
|
|
7
|
+
# Formats in this list will always have their exceptions handled by Graphiti.
|
|
8
|
+
# * `config.graphiti.respond_to_formats`, defaulting to `[:json, :jsonapi, :xml]`.
|
|
9
|
+
# When {Graphiti::Rails::Responders} is included in a controller it will respond
|
|
10
|
+
# to these mime types by default.
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
config.graphiti = ActiveSupport::OrderedOptions.new
|
|
13
|
+
|
|
14
|
+
config.graphiti.handled_exception_formats = [:jsonapi]
|
|
15
|
+
|
|
16
|
+
old_respond_to_formats = Graphiti::DEPRECATOR.silence { Graphiti.config.try(:respond_to) }
|
|
17
|
+
config.graphiti.respond_to_formats = old_respond_to_formats || [:json, :jsonapi, :xml]
|
|
18
|
+
|
|
19
|
+
rake_tasks do
|
|
20
|
+
load File.expand_path("../../tasks/graphiti.rake", __dir__)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "graphiti-rails.config" do |app|
|
|
24
|
+
Graphiti::Rails.handled_exception_formats = app.config.graphiti.handled_exception_formats
|
|
25
|
+
Graphiti::Rails.respond_to_formats = app.config.graphiti.respond_to_formats
|
|
26
|
+
|
|
27
|
+
root = ::Rails.root
|
|
28
|
+
|
|
29
|
+
config_file = root.join(".graphiticfg.yml")
|
|
30
|
+
if config_file.exist?
|
|
31
|
+
cfg = YAML.load_file(config_file)
|
|
32
|
+
Graphiti.config.schema_path = root.join("public#{cfg["namespace"]}/schema.json")
|
|
33
|
+
else
|
|
34
|
+
Graphiti.config.schema_path = root.join("public/schema.json")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
initializer "graphti-rails.logger" do
|
|
39
|
+
config.after_initialize do
|
|
40
|
+
Graphiti.config.debug = ::Rails.logger.debug? && Graphiti.config.debug
|
|
41
|
+
Graphiti.logger = ::Rails.logger
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
initializer "graphiti-rails.init" do
|
|
46
|
+
if ::Rails.application.config.eager_load
|
|
47
|
+
config.after_initialize do |app|
|
|
48
|
+
::Rails.application.reload_routes!
|
|
49
|
+
Graphiti.setup!
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
register_mime_type
|
|
54
|
+
register_parameter_parser
|
|
55
|
+
register_renderers
|
|
56
|
+
establish_concurrency
|
|
57
|
+
configure_endpoint_lookup
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# from jsonapi-rails
|
|
61
|
+
MEDIA_TYPE = "application/vnd.api+json"
|
|
62
|
+
|
|
63
|
+
PARSER = lambda do |body|
|
|
64
|
+
data = JSON.parse(body)
|
|
65
|
+
data[:format] = :jsonapi
|
|
66
|
+
data.with_indifferent_access
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def register_mime_type
|
|
70
|
+
Mime::Type.register(MEDIA_TYPE, :jsonapi)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def register_parameter_parser
|
|
74
|
+
ActionDispatch::Request.parameter_parsers[:jsonapi] ||= PARSER
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_renderers
|
|
78
|
+
ActiveSupport.on_load(:action_controller) do
|
|
79
|
+
::ActionController::Renderers.add(:jsonapi) do |proxy, options|
|
|
80
|
+
self.content_type ||= Mime[:jsonapi]
|
|
81
|
+
|
|
82
|
+
# opts = {}
|
|
83
|
+
# if respond_to?(:default_jsonapi_render_options)
|
|
84
|
+
# opts = default_jsonapi_render_options
|
|
85
|
+
# end
|
|
86
|
+
|
|
87
|
+
if proxy.is_a?(Hash) # for destroy
|
|
88
|
+
render(options.merge(json: proxy))
|
|
89
|
+
else
|
|
90
|
+
proxy.to_jsonapi(options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
ActiveSupport.on_load(:action_controller) do
|
|
96
|
+
ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
|
|
97
|
+
self.content_type ||= Mime[:jsonapi]
|
|
98
|
+
|
|
99
|
+
validation = Graphiti::ErrorSerializers::Validation.new \
|
|
100
|
+
proxy.data, proxy.payload.relationships
|
|
101
|
+
|
|
102
|
+
render \
|
|
103
|
+
json: {errors: validation.errors},
|
|
104
|
+
status: :unprocessable_entity
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Only run concurrently if our environment supports it
|
|
110
|
+
def establish_concurrency
|
|
111
|
+
Graphiti.config.concurrency = !::Rails.env.test? &&
|
|
112
|
+
::Rails.application.config.cache_classes
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def configure_endpoint_lookup
|
|
116
|
+
Graphiti.config.context_for_endpoint = ->(path, action) {
|
|
117
|
+
method = :GET
|
|
118
|
+
case action
|
|
119
|
+
when :show then path = "#{path}/1"
|
|
120
|
+
when :create then method = :POST
|
|
121
|
+
when :update
|
|
122
|
+
path = "#{path}/1"
|
|
123
|
+
method = :PUT
|
|
124
|
+
when :destroy
|
|
125
|
+
path = "#{path}/1"
|
|
126
|
+
method = :DELETE
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
route = begin
|
|
130
|
+
::Rails.application.routes.recognize_path(path.to_s, method: method)
|
|
131
|
+
rescue
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
"#{route[:controller]}_controller".classify.safe_constantize if route
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "graphiti/rails/test_helpers"
|
|
2
|
+
|
|
3
|
+
module Graphiti
|
|
4
|
+
module Rails
|
|
5
|
+
# Rake's `namespace` takes a block, and a block does not open a new definee,
|
|
6
|
+
# so helpers defined inside one are defined on Object and turn up in every
|
|
7
|
+
# request spec in the host app. They live here to stay off that namespace.
|
|
8
|
+
module RakeHelpers
|
|
9
|
+
extend TestHelpers
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# ::Rails throughout, because bare Rails resolves to Graphiti::Rails here.
|
|
14
|
+
def session
|
|
15
|
+
@session ||= ActionDispatch::Integration::Session.new(::Rails.application)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setup_rails!
|
|
19
|
+
::Rails.application.eager_load!
|
|
20
|
+
::Rails.application.config.cache_classes = true
|
|
21
|
+
::Rails.application.config.action_controller.perform_caching = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def make_request(path, debug = false)
|
|
25
|
+
if path.split("/").length == 2
|
|
26
|
+
path = "#{ApplicationResource.endpoint_namespace}#{path}"
|
|
27
|
+
end
|
|
28
|
+
path << if path.include?("?")
|
|
29
|
+
"&cache=bust"
|
|
30
|
+
else
|
|
31
|
+
"?cache=bust"
|
|
32
|
+
end
|
|
33
|
+
path = "#{path}&debug=true" if debug
|
|
34
|
+
handle_request_exceptions do
|
|
35
|
+
headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
|
|
36
|
+
session.get(path.to_s, headers: headers)
|
|
37
|
+
end
|
|
38
|
+
JSON.parse(session.response.body)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# For use with the `responders` gem responders gem to get respond_with
|
|
2
|
+
module Graphiti
|
|
3
|
+
module Rails
|
|
4
|
+
module Responders
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
include ActionController::MimeResponds
|
|
9
|
+
respond_to(*Graphiti::Rails.respond_to_formats)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Override to avoid location url generation (for now)
|
|
13
|
+
def respond_with(*args, &blk)
|
|
14
|
+
opts = args.extract_options!
|
|
15
|
+
opts[:location] = nil
|
|
16
|
+
args << opts
|
|
17
|
+
super(*args, &blk)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Rails
|
|
3
|
+
# Wrap a request in `handle_request_exceptions` to assert on the rendered
|
|
4
|
+
# error payload. Exceptions propagate untouched in tests otherwise.
|
|
5
|
+
module TestHelpers
|
|
6
|
+
include RescueRegistry::RailsTestHelpers
|
|
7
|
+
|
|
8
|
+
# rescue_registry sets action_dispatch.show_exceptions to true/false.
|
|
9
|
+
# Rails 7.1 replaced those with :all/:rescuable/:none, and Rails 8 dropped
|
|
10
|
+
# boolean handling altogether. ExceptionWrapper#show? now treats
|
|
11
|
+
# everything that is not :none as "show", so passing false would leave
|
|
12
|
+
# exception handling on and silently do nothing.
|
|
13
|
+
def handle_request_exceptions(handle = true, &block)
|
|
14
|
+
super(handle ? :all : :none, &block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handle_request_exceptions?
|
|
18
|
+
::Rails.application.config.action_dispatch.show_exceptions != :none
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/graphiti/rails.rb
CHANGED
|
@@ -1,38 +1,56 @@
|
|
|
1
|
+
require "rescue_registry"
|
|
2
|
+
require "graphiti"
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
1
5
|
module Graphiti
|
|
2
|
-
# Rails
|
|
3
|
-
#
|
|
4
|
-
# * Mixes in Base
|
|
5
|
-
# * Adds a global around_action (see Base#wrap_context)
|
|
6
|
-
#
|
|
7
|
-
# @see Base#render_jsonapi
|
|
8
|
-
# @see Base#wrap_context
|
|
6
|
+
# Rails integration for Graphiti. See {file:README.md} for more details.
|
|
9
7
|
module Rails
|
|
8
|
+
# Graphiti::Rails was a controller mixin before it became this namespace.
|
|
9
|
+
# Including a namespace is not an error, it just does nothing, so honour
|
|
10
|
+
# the old spelling rather than leave a controller silently without its
|
|
11
|
+
# context, debugger and exception handlers.
|
|
10
12
|
def self.included(klass)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
include Graphiti::Context
|
|
16
|
-
include GraphitiErrors
|
|
17
|
-
around_action :wrap_context
|
|
18
|
-
around_action :debug
|
|
19
|
-
end
|
|
20
|
-
end
|
|
13
|
+
DEPRECATOR.deprecation_warning(
|
|
14
|
+
"Including Graphiti::Rails",
|
|
15
|
+
"include Graphiti::Rails::Controller instead"
|
|
16
|
+
)
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
Graphiti.with_context(jsonapi_context, action_name.to_sym) do
|
|
24
|
-
yield
|
|
25
|
-
end
|
|
18
|
+
klass.include(Controller)
|
|
26
19
|
end
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
yield
|
|
31
|
-
end
|
|
32
|
-
end
|
|
21
|
+
# graphiti-rails had its own, and apps that silenced it by name still resolve.
|
|
22
|
+
DEPRECATOR = Graphiti::DEPRECATOR
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
autoload :ConflictRequestHandler, "graphiti/rails/exception_handlers"
|
|
25
|
+
autoload :Context, "graphiti/rails/context"
|
|
26
|
+
autoload :Controller, "graphiti/rails/controller"
|
|
27
|
+
autoload :Debugging, "graphiti/rails/debugging"
|
|
28
|
+
autoload :ExceptionHandler, "graphiti/rails/exception_handlers"
|
|
29
|
+
autoload :FallbackHandler, "graphiti/rails/exception_handlers"
|
|
30
|
+
autoload :InvalidRequestHandler, "graphiti/rails/exception_handlers"
|
|
31
|
+
autoload :Responders, "graphiti/rails/responders"
|
|
32
|
+
autoload :TestHelpers, "graphiti/rails/test_helpers"
|
|
33
|
+
|
|
34
|
+
# @!attribute self.handled_exception_formats
|
|
35
|
+
# A list of formats as symbols whose exceptions will be handled by Graphiti. See {Railtie}.
|
|
36
|
+
cattr_accessor :handled_exception_formats, default: []
|
|
37
|
+
|
|
38
|
+
# @!attribute self.respond_to_formats
|
|
39
|
+
# A list of formats as symbols which will be available for Graphiti::Rails::Responders. See {Railtie}.
|
|
40
|
+
cattr_accessor :respond_to_formats, default: []
|
|
37
41
|
end
|
|
42
|
+
|
|
43
|
+
# Deprecated. Was core's own responders mixin, superseded by the one that came
|
|
44
|
+
# in with graphiti-rails. Remove in 3.0.
|
|
45
|
+
Responders = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
|
|
46
|
+
"Graphiti::Responders",
|
|
47
|
+
"Graphiti::Rails::Responders",
|
|
48
|
+
DEPRECATOR
|
|
49
|
+
)
|
|
38
50
|
end
|
|
51
|
+
|
|
52
|
+
ActiveSupport.on_load(:active_record) do
|
|
53
|
+
require "graphiti/adapters/active_record"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
require "graphiti/rails/railtie"
|
|
@@ -90,6 +90,7 @@ module Graphiti
|
|
|
90
90
|
:attributes_schema_by_default,
|
|
91
91
|
:relationships_readable_by_default,
|
|
92
92
|
:relationships_writable_by_default,
|
|
93
|
+
:always_include_resource_ids_by_default,
|
|
93
94
|
:filters_accept_nil_by_default,
|
|
94
95
|
:filters_deny_empty_by_default,
|
|
95
96
|
:graphql_entrypoint,
|
|
@@ -84,8 +84,8 @@ module Graphiti
|
|
|
84
84
|
|
|
85
85
|
if context&.respond_to?(:request)
|
|
86
86
|
path = context.request.env["PATH_INFO"]
|
|
87
|
-
unless allow_request?(path, params,
|
|
88
|
-
raise Errors::InvalidEndpoint.new(self, path,
|
|
87
|
+
unless allow_request?(path, params, current_action)
|
|
88
|
+
raise Errors::InvalidEndpoint.new(self, path, current_action)
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
end
|
|
@@ -38,8 +38,8 @@ module Graphiti
|
|
|
38
38
|
# Forward all headers
|
|
39
39
|
def request_headers
|
|
40
40
|
{}.tap do |headers|
|
|
41
|
-
# TODO: Maybe handle this in
|
|
42
|
-
if defined?(Rails) && context
|
|
41
|
+
# TODO: Maybe handle this in the Rails integration
|
|
42
|
+
if defined?(::Rails) && context
|
|
43
43
|
raw = context.request.headers.to_h
|
|
44
44
|
if (auth = raw["HTTP_AUTHORIZATION"])
|
|
45
45
|
headers["Authorization"] = auth
|
|
@@ -128,7 +128,7 @@ module Graphiti
|
|
|
128
128
|
# If eager loading, ensure routes are loaded first, then apply
|
|
129
129
|
# This happens in Railtie
|
|
130
130
|
def eagerly_apply_sideload?(sideload)
|
|
131
|
-
# TODO: Maybe handle this in
|
|
131
|
+
# TODO: Maybe handle this in the Rails integration
|
|
132
132
|
if defined?(::Rails) && (app = ::Rails.application)
|
|
133
133
|
app.config.eager_load ? false : true
|
|
134
134
|
else
|
data/lib/graphiti/resource.rb
CHANGED
|
@@ -51,10 +51,22 @@ module Graphiti
|
|
|
51
51
|
self.class.context
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Rails sets this from action_name, so it is whatever the controller action
|
|
55
|
+
# is called, not a fixed list. Persistence overrides it with :create/:update
|
|
56
|
+
# while saving, and :show while resolving sideloads afterwards.
|
|
57
|
+
def self.current_action
|
|
55
58
|
Graphiti.context[:namespace]
|
|
56
59
|
end
|
|
57
60
|
|
|
61
|
+
def current_action
|
|
62
|
+
self.class.current_action
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.context_namespace
|
|
66
|
+
Graphiti::DEPRECATOR.deprecation_warning(:context_namespace, "Use #current_action instead")
|
|
67
|
+
current_action
|
|
68
|
+
end
|
|
69
|
+
|
|
58
70
|
def context_namespace
|
|
59
71
|
self.class.context_namespace
|
|
60
72
|
end
|
data/lib/graphiti/responders.rb
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
extend ActiveSupport::Concern
|
|
1
|
+
# Deprecated entry point. graphiti required this path itself in 1.x, so an
|
|
2
|
+
# explicit require left behind would otherwise raise LoadError. Remove in 3.0.
|
|
3
|
+
require "graphiti/rails"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
respond_to(*Graphiti.config.respond_to)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Override to avoid location url generation (for now)
|
|
14
|
-
def respond_with(*args, &blk)
|
|
15
|
-
opts = args.extract_options!
|
|
16
|
-
opts[:location] = nil
|
|
17
|
-
args << opts
|
|
18
|
-
super(*args, &blk)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
5
|
+
Graphiti::DEPRECATOR.warn(
|
|
6
|
+
'require "graphiti/responders" is deprecated. Use "graphiti/rails" and include ' \
|
|
7
|
+
"Graphiti::Rails::Responders."
|
|
8
|
+
)
|
data/lib/graphiti/schema.rb
CHANGED
|
@@ -3,7 +3,7 @@ module Graphiti
|
|
|
3
3
|
attr_reader :resources
|
|
4
4
|
|
|
5
5
|
def self.generate(resources = nil)
|
|
6
|
-
# TODO: Maybe handle this in
|
|
6
|
+
# TODO: Maybe handle this in the Rails integration
|
|
7
7
|
::Rails.application.eager_load! if defined?(::Rails)
|
|
8
8
|
resources ||= Graphiti.resources.reject(&:abstract_class?)
|
|
9
9
|
resources.reject! { |r| r.name.nil? }
|
|
@@ -251,6 +251,10 @@ module Graphiti
|
|
|
251
251
|
schema[:guard] = true
|
|
252
252
|
end
|
|
253
253
|
|
|
254
|
+
if config.always_include_resource_ids?
|
|
255
|
+
schema[:linkage] = true
|
|
256
|
+
end
|
|
257
|
+
|
|
254
258
|
r[name] = schema
|
|
255
259
|
end
|
|
256
260
|
end
|
data/lib/graphiti/schema_diff.rb
CHANGED
|
@@ -106,6 +106,10 @@ module Graphiti
|
|
|
106
106
|
@errors << "#{old_resource[:name]}: relationship #{name.inspect} became guarded."
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
if old_rel[:linkage] && !new_rel[:linkage]
|
|
110
|
+
@errors << "#{old_resource[:name]}: relationship #{name.inspect} no longer includes resource linkage."
|
|
111
|
+
end
|
|
112
|
+
|
|
109
113
|
if new_rel[:resource] != old_rel[:resource]
|
|
110
114
|
@errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}."
|
|
111
115
|
end
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -48,29 +48,25 @@ module Graphiti
|
|
|
48
48
|
# Thread/Fiber storage snapshots, and Rails executor wrappers on every
|
|
49
49
|
# request purely to drive a thread pool that is intentionally synchronous.
|
|
50
50
|
# See https://github.com/graphiti-api/graphiti/issues/505
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if Graphiti.config.concurrency
|
|
52
|
+
future_resolve(&blk).value!
|
|
53
|
+
else
|
|
54
|
+
sync_resolve(&blk)
|
|
55
|
+
end
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def resolve_sideloads(results)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
if Graphiti.config.concurrency
|
|
60
|
+
future_resolve_sideloads(results).value!
|
|
61
|
+
else
|
|
62
|
+
sync_resolve_sideloads(results)
|
|
63
|
+
end
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
def future_resolve
|
|
66
|
+
def future_resolve(&blk)
|
|
63
67
|
return Concurrent::Promises.fulfilled_future([], self.class.global_thread_pool_executor) if @query.zero_results?
|
|
64
68
|
|
|
65
|
-
resolved =
|
|
66
|
-
@object = @resource.before_resolve(@object, @query)
|
|
67
|
-
payload[:results] = @resource.resolve(@object)
|
|
68
|
-
payload[:results]
|
|
69
|
-
}
|
|
70
|
-
resolved.compact!
|
|
71
|
-
assign_serializer(resolved)
|
|
72
|
-
yield resolved if block_given?
|
|
73
|
-
@opts[:after_resolve]&.call(resolved)
|
|
69
|
+
resolved = resolve_primary_data(&blk)
|
|
74
70
|
sideloaded = @query.parents.any?
|
|
75
71
|
close_adapter = Graphiti.config.concurrency && sideloaded
|
|
76
72
|
if close_adapter
|
|
@@ -125,9 +121,33 @@ module Graphiti
|
|
|
125
121
|
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
126
122
|
# Resolves the resource and its sideloads inline without any promise
|
|
127
123
|
# machinery. See #resolve.
|
|
128
|
-
def sync_resolve
|
|
124
|
+
def sync_resolve(&blk)
|
|
129
125
|
return [] if @query.zero_results?
|
|
130
126
|
|
|
127
|
+
resolved = resolve_primary_data(&blk)
|
|
128
|
+
sync_resolve_sideloads(resolved)
|
|
129
|
+
resolved
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Synchronous counterpart to #future_resolve_sideloads, used when
|
|
133
|
+
# concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
|
|
134
|
+
def sync_resolve_sideloads(results)
|
|
135
|
+
return if results == []
|
|
136
|
+
|
|
137
|
+
each_applicable_sideload do |sideload, sideload_query|
|
|
138
|
+
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
139
|
+
sideload.resolve(results, sideload_query, @resource)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
|
|
143
|
+
# nil (not the sideloads Hash). Callers don't rely on the return value.
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Resolve this scope's own data: run hooks, resolve the resource, and
|
|
148
|
+
# decorate the results. Shared by the sync and future paths — everything
|
|
149
|
+
# here runs inline on the calling thread in both modes.
|
|
150
|
+
def resolve_primary_data
|
|
131
151
|
resolved = broadcast_data { |payload|
|
|
132
152
|
@object = @resource.before_resolve(@object, @query)
|
|
133
153
|
payload[:results] = @resource.resolve(@object)
|
|
@@ -137,40 +157,28 @@ module Graphiti
|
|
|
137
157
|
assign_serializer(resolved)
|
|
138
158
|
yield resolved if block_given?
|
|
139
159
|
@opts[:after_resolve]&.call(resolved)
|
|
140
|
-
sync_resolve_sideloads(resolved) unless @query.sideloads.empty?
|
|
141
160
|
resolved
|
|
142
161
|
end
|
|
143
162
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def sync_resolve_sideloads(results)
|
|
147
|
-
return if results == []
|
|
148
|
-
|
|
149
|
-
@query.sideloads.each_pair do |name, q|
|
|
163
|
+
def each_applicable_sideload
|
|
164
|
+
@query.sideloads.each_pair do |name, sideload_query|
|
|
150
165
|
sideload = @resource.class.sideload(name)
|
|
151
166
|
next if sideload.nil? || sideload.shared_remote?
|
|
152
167
|
|
|
153
|
-
|
|
154
|
-
sideload.resolve(results, q, @resource)
|
|
168
|
+
yield sideload, sideload_query
|
|
155
169
|
end
|
|
156
|
-
|
|
157
|
-
# Match pre-1.8 semantics: the non-concurrent resolve_sideloads returned
|
|
158
|
-
# nil (not the sideloads Hash). Callers don't rely on the return value.
|
|
159
|
-
nil
|
|
160
170
|
end
|
|
161
171
|
|
|
162
172
|
def future_resolve_sideloads(results)
|
|
163
173
|
return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
|
|
164
174
|
|
|
165
|
-
sideload_promises =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
p = future_with_context(results, q, @resource) do |parent_results, sideload_query, parent_resource|
|
|
175
|
+
sideload_promises = []
|
|
176
|
+
each_applicable_sideload do |sideload, sideload_query|
|
|
177
|
+
promise = future_with_context(results, sideload_query, @resource) do |parent_results, future_query, parent_resource|
|
|
170
178
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
171
|
-
sideload.future_resolve(parent_results,
|
|
179
|
+
sideload.future_resolve(parent_results, future_query, parent_resource)
|
|
172
180
|
end
|
|
173
|
-
|
|
181
|
+
sideload_promises << promise.flat
|
|
174
182
|
end
|
|
175
183
|
|
|
176
184
|
Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
|
data/lib/graphiti/serializer.rb
CHANGED
|
@@ -13,10 +13,16 @@ module Graphiti
|
|
|
13
13
|
class_attribute :attributes_applied_via_resource
|
|
14
14
|
class_attribute :extra_attributes_applied_via_resource
|
|
15
15
|
class_attribute :relationship_condition_blocks
|
|
16
|
+
# Which sideload each generated relationship block was built for. A
|
|
17
|
+
# subclass inherits its parent's blocks, so this is how a redeclared
|
|
18
|
+
# relationship is told apart from one already applied, and from one the
|
|
19
|
+
# application wrote by hand.
|
|
20
|
+
class_attribute :relationship_sideloads
|
|
16
21
|
self.attributes_applied_via_resource = []
|
|
17
22
|
self.extra_attributes_applied_via_resource = []
|
|
18
23
|
# See #requested_relationships
|
|
19
24
|
self.relationship_condition_blocks ||= {}
|
|
25
|
+
self.relationship_sideloads ||= {}
|
|
20
26
|
|
|
21
27
|
def self.inherited(klass)
|
|
22
28
|
super
|
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
2
|
-
def initialize(name, opts)
|
|
3
|
-
opts = {always_include_resource_ids: false}.merge(opts)
|
|
4
|
-
super(name, opts)
|
|
5
|
-
end
|
|
6
|
-
|
|
7
2
|
def type
|
|
8
3
|
:belongs_to
|
|
9
4
|
end
|
|
10
5
|
|
|
6
|
+
def default_include_resource_ids?
|
|
7
|
+
linkage_from_foreign_key?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# The parent already carries the foreign key, and for a plain belongs_to
|
|
11
|
+
# that key *is* the related id, so linkage costs nothing. Anything that can
|
|
12
|
+
# change which record the relationship resolves to, or what type it carries,
|
|
13
|
+
# has to load the association instead:
|
|
14
|
+
#
|
|
15
|
+
# - a scope/params block or a base_scope can filter out the record the
|
|
16
|
+
# foreign key points at, so the key would claim a relationship the API
|
|
17
|
+
# would not actually return
|
|
18
|
+
# - a polymorphic target takes its type from the record, not from the
|
|
19
|
+
# relationship, so the key alone cannot say what type the id has
|
|
20
|
+
# - a remote resource has no local foreign key to read
|
|
21
|
+
# - a custom primary_key points the relationship at some other column, so
|
|
22
|
+
# the key holds that column's value rather than the related id
|
|
23
|
+
def linkage_from_foreign_key?
|
|
24
|
+
# Ask before resolving #resource: an unreadable relationship renders
|
|
25
|
+
# nothing, and its resource class may not even be inferrable.
|
|
26
|
+
return false unless readable?
|
|
27
|
+
return false unless foreign_key_is_related_id?
|
|
28
|
+
return false if polymorphic_child?
|
|
29
|
+
return false if self.class.scope_proc || self.class.params_proc
|
|
30
|
+
return false if @base_scope
|
|
31
|
+
return false if remote?
|
|
32
|
+
return false if resource.class.polymorphic.present?
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The foreign key can stand in for the related id only when the two hold the
|
|
38
|
+
# same value. base_filter matches the key against primary_key, so pointing
|
|
39
|
+
# that at another column means the key holds that column instead.
|
|
40
|
+
def foreign_key_is_related_id?
|
|
41
|
+
primary_key == :id
|
|
42
|
+
end
|
|
43
|
+
|
|
11
44
|
def load_params(parents, query)
|
|
12
45
|
query.hash.tap do |hash|
|
|
13
46
|
hash[:filter] ||= {}
|