graphiti 2.0.0.beta.4 → 2.0.0.beta.6

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 +4 -4
  2. data/.github/workflows/ci.yml +5 -0
  3. data/.github/workflows/docs.yml +4 -7
  4. data/CHANGELOG.md +29 -0
  5. data/docs/concepts/links.md +3 -3
  6. data/docs/concepts/relationships.md +109 -9
  7. data/docs/topics/debugging.md +26 -0
  8. data/docs/upgrading.md +18 -17
  9. data/graphiti.gemspec +1 -19
  10. data/lib/generators/graphiti/resource_generator.rb +25 -1
  11. data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
  12. data/lib/graphiti/audit/report.rb +226 -0
  13. data/lib/graphiti/audit.rb +231 -0
  14. data/lib/graphiti/errors.rb +47 -0
  15. data/lib/graphiti/rails/rake_helpers.rb +42 -0
  16. data/lib/graphiti/request_validators/validator.rb +10 -1
  17. data/lib/graphiti/resource/configuration.rb +16 -1
  18. data/lib/graphiti/resource/polymorphism.rb +10 -0
  19. data/lib/graphiti/schema.rb +1 -1
  20. data/lib/graphiti/sideload/belongs_to.rb +22 -29
  21. data/lib/graphiti/sideload.rb +48 -13
  22. data/lib/graphiti/spec_helpers/matchers.rb +196 -0
  23. data/lib/graphiti/spec_helpers/rspec.rb +1 -0
  24. data/lib/graphiti/spec_helpers.rb +1 -0
  25. data/lib/graphiti/util/serializer_relationships.rb +27 -4
  26. data/lib/graphiti/version.rb +1 -1
  27. data/lib/graphiti.rb +2 -0
  28. data/lib/tasks/graphiti.rake +16 -32
  29. data/website/docusaurus.config.js +67 -11
  30. data/website/src/css/custom.css +29 -2
  31. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  32. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  33. data/website/static/1.13/assets/img/sunrise.png +0 -0
  34. data/website/static/1.13/assets/main.css +4 -4
  35. data/website/static/CNAME +1 -0
  36. metadata +9 -9
@@ -0,0 +1,226 @@
1
+ module Graphiti
2
+ class Audit
3
+ class Report
4
+ ANSI = {red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, bold: 1, dim: 2}.freeze
5
+
6
+ ISSUE_HEADINGS = {
7
+ broken_relationship: "raised while being inspected",
8
+ missing_association_method: "will raise when the relationship is included: the model has no association method",
9
+ missing_guard_method: "will raise whenever the resource renders: the readable guard is not defined",
10
+ missing_sideload_filter: "will raise when the relationship is included: the related resource is missing the filter"
11
+ }.freeze
12
+
13
+ CHECKLIST = {
14
+ broken_relationship: ["all relationships inspectable", "relationship", "raised while being inspected"],
15
+ missing_association_method: ["all association methods defined", "association method", "missing"],
16
+ missing_guard_method: ["all readable guards defined", "readable guard", "missing"],
17
+ missing_sideload_filter: ["all sideload filters declared", "sideload filter", "missing"],
18
+ loads_on_every_render: ["all id-rendering loads preloaded", "relationship", "loading ids without preloading"]
19
+ }.freeze
20
+
21
+ Cell = Struct.new(:raw, :painted) do
22
+ def pad_to(width)
23
+ painted + " " * (width - raw.length)
24
+ end
25
+ end
26
+
27
+ def initialize(rows, color: $stdout.tty?)
28
+ @rows = rows
29
+ @color = color
30
+ end
31
+
32
+ def to_s
33
+ return "graphiti: no relationships found.\n" if @rows.empty?
34
+
35
+ (body + [summary]).join("\n")
36
+ end
37
+
38
+ private
39
+
40
+ def body
41
+ [""] + issue_sections + loading_section + would_load_section + checks_section
42
+ end
43
+
44
+ def issue_sections
45
+ row_finding_pairs = @rows.flat_map do |row|
46
+ row.findings.map { |finding| [row, finding] }
47
+ end
48
+
49
+ row_finding_pairs
50
+ .group_by { |_, finding| finding.check }
51
+ .sort_by { |check, _| ISSUE_HEADINGS.keys.index(check) || ISSUE_HEADINGS.size }
52
+ .flat_map { |check, group| issue_section(check, group) }
53
+ end
54
+
55
+ def issue_section(check, group)
56
+ findings = group.map(&:last)
57
+ label = findings.first.error? ? "ERROR" : "WARNING"
58
+ color = findings.first.error? ? :red : :yellow
59
+ fixes = findings.filter_map(&:remedy).uniq
60
+
61
+ section_heading(label, color, ISSUE_HEADINGS.fetch(check, check.to_s.tr("_", " "))) +
62
+ lines_grouped_by_resource(group.map { |row, finding| [row, finding.message] }) +
63
+ (fixes.size == 1 ? [fix_line(fixes.first), ""] : [])
64
+ end
65
+
66
+ def loading_section
67
+ rows = @rows.select { |row| loads_without_preload?(row) }
68
+ return [] if rows.empty?
69
+
70
+ section_heading("WARNING", :yellow, "rendering resource ids by loading an association the base_scope does not preload") +
71
+ lines_grouped_by_resource(rows.map { |row| [row, nil] }) +
72
+ [" #{paint("That is a query per record rendered.", :dim)}",
73
+ fix_line("preload the association in base_scope, or drop `resource_ids`"),
74
+ " #{paint("See www.graphiti.dev/concepts/relationships#customizing-relationships", :dim)}",
75
+ ""]
76
+ end
77
+
78
+ def loads_without_preload?(row)
79
+ row.resource_ids_source == :load && row.preloaded == false
80
+ end
81
+
82
+ def would_load_section
83
+ rows = @rows.select(&:would_start_loading?)
84
+ return [] if rows.empty?
85
+
86
+ verb = rows.size == 1 ? "renders" : "render"
87
+
88
+ section_heading("FYI", :blue, "#{count(rows.size, "belongs_to relationship")} #{verb} no resource ids") +
89
+ lines_grouped_by_resource(rows.map { |row| [row, nil] }) +
90
+ [" #{paint("Nothing to fix. A request that includes the relationship still gets its ids.", :dim)}",
91
+ "",
92
+ " #{paint("To render ids on every response, opt in:", :dim)}",
93
+ ""] +
94
+ opt_in_lines +
95
+ ["",
96
+ " #{paint("Either would load the association on every render. That potential performance cost is why ids are opt-in.", :dim)}",
97
+ " #{paint("Preloading the association in base_scope keeps that load cheap.", :dim)}",
98
+ " #{paint("See www.graphiti.dev/concepts/relationships#belongs-to-resource-ids", :dim)}",
99
+ ""]
100
+ end
101
+
102
+ def checks_section
103
+ failure_counts = @rows.flat_map(&:findings).map(&:check).tally
104
+ loading = @rows.count { |row| loads_without_preload?(row) }
105
+ failure_counts[:loads_on_every_render] = loading if loading > 0
106
+
107
+ no_loads = @rows.none? { |row| row.resource_ids_source == :load }
108
+
109
+ lines = CHECKLIST.filter_map do |check, (passed, failure_noun, failure_suffix)|
110
+ if (failures = failure_counts[check])
111
+ " #{paint("✗", :red)} #{count(failures, failure_noun)} #{failure_suffix}"
112
+ elsif check == :loads_on_every_render && no_loads
113
+ nil
114
+ else
115
+ " #{paint("✓", :green)} #{passed}"
116
+ end
117
+ end
118
+
119
+ [paint("checks", :bold), ""] + lines + [""]
120
+ end
121
+
122
+ def section_heading(label, color, heading)
123
+ ["#{paint(label, color, :bold)} #{paint(heading, :bold)}", ""]
124
+ end
125
+
126
+ def fix_line(text)
127
+ " #{paint("fix:", :green)} #{text}"
128
+ end
129
+
130
+ def lines_grouped_by_resource(pairs)
131
+ width = pairs.map { |row, _| declaration_cell(row).raw.length }.max
132
+
133
+ pairs.group_by { |row, _| row.resource }.sort_by { |resource, _| resource }.flat_map do |resource, group|
134
+ [" #{paint(resource, :bold)}"] +
135
+ group.sort_by { |row, _| row.relationship.to_s }.map { |row, note|
136
+ declaration = declaration_cell(row)
137
+ note ? " #{declaration.pad_to(width)} #{note}" : " #{declaration.painted}"
138
+ } +
139
+ [""]
140
+ end
141
+ end
142
+
143
+ def opt_in_lines
144
+ options = [
145
+ [[["resource_ids: ", :cyan], ["true", :yellow]], "on one relationship"],
146
+ [[["self.belongs_to_resource_ids_by_default = ", nil], [":always", :cyan]], "across the whole API"]
147
+ ]
148
+
149
+ rows = options.map do |parts, scope|
150
+ [cell(parts), cell([[scope, :dim]])]
151
+ end
152
+
153
+ aligned(rows, " ")
154
+ end
155
+
156
+ def declaration_cell(row)
157
+ cell(declaration_parts(row))
158
+ end
159
+
160
+ def declaration_parts(row)
161
+ parts = [[row.type.to_s, :blue], [" ", nil], [":#{row.relationship}", :cyan]]
162
+
163
+ row.options.each do |key, value|
164
+ parts << [", ", nil] << ["#{key}: ", :cyan] << option_part(value)
165
+ end
166
+
167
+ parts
168
+ end
169
+
170
+ def option_part(value)
171
+ case value
172
+ when "..." then ["{ ... }", :dim]
173
+ when true, false then [value.inspect, :yellow]
174
+ when Symbol then [value.inspect, :cyan]
175
+ when String then [value.inspect, :green]
176
+ else [value.inspect, nil]
177
+ end
178
+ end
179
+
180
+ def cell(parts)
181
+ Cell.new(
182
+ parts.map { |text, _| text }.join,
183
+ parts.map { |text, style| paint(text, style) }.join
184
+ )
185
+ end
186
+
187
+ def aligned(rows, indent)
188
+ widths = rows.transpose.map { |column| column.map { |cell| cell.raw.length }.max }
189
+
190
+ rows.map do |cells|
191
+ line = cells.each_with_index.map { |cell, index|
192
+ index == cells.size - 1 ? cell.painted : cell.pad_to(widths[index])
193
+ }.join(" ")
194
+ indent + line
195
+ end
196
+ end
197
+
198
+ def summary
199
+ errors = @rows.count(&:error?)
200
+ loading = @rows.count { |row| loads_without_preload?(row) }
201
+ no_ids = @rows.count(&:would_start_loading?)
202
+
203
+ counts = [
204
+ count(@rows.group_by(&:resource).size, "resource"),
205
+ count(@rows.size, "relationship"),
206
+ paint(count(errors, "error"), errors > 0 ? :red : nil)
207
+ ]
208
+ counts << paint("#{loading} loading ids without preloading", :yellow) if loading > 0
209
+ counts << "#{no_ids} without resource ids" if no_ids > 0
210
+
211
+ "graphiti: #{counts.join(", ")}."
212
+ end
213
+
214
+ def count(number, noun)
215
+ "#{number} #{noun}#{"s" unless number == 1}"
216
+ end
217
+
218
+ def paint(text, *styles)
219
+ styles = styles.compact
220
+ return text unless @color && styles.any?
221
+
222
+ "\e[#{styles.map { |style| ANSI.fetch(style) }.join(";")}m#{text}\e[0m"
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,231 @@
1
+ module Graphiti
2
+ # Resources register themselves as they load, so eager load before auditing.
3
+ class Audit
4
+ Finding = Struct.new(:severity, :check, :message, :remedy, keyword_init: true) do
5
+ def error?
6
+ severity == :error
7
+ end
8
+ end
9
+
10
+ Row = Struct.new(
11
+ :resource,
12
+ :relationship,
13
+ :type,
14
+ :target,
15
+ :resource_ids_source,
16
+ :resource_ids_source_if_always,
17
+ :resource_ids_default,
18
+ :resource_ids_blocker,
19
+ :preloaded,
20
+ :options,
21
+ :findings,
22
+ keyword_init: true
23
+ ) do
24
+ def would_start_loading?
25
+ return false if resource_ids_default == :never
26
+
27
+ resource_ids_source != :load && resource_ids_source_if_always == :load
28
+ end
29
+
30
+ def severity
31
+ return :error if findings.any?(&:error?)
32
+ findings.empty? ? :ok : :warning
33
+ end
34
+
35
+ def error?
36
+ severity == :error
37
+ end
38
+ end
39
+
40
+ def self.run(resources = nil)
41
+ new(resources).run
42
+ end
43
+
44
+ def self.findings(resources = nil)
45
+ run(resources).flat_map(&:findings)
46
+ end
47
+
48
+ def initialize(resources = nil)
49
+ @resources = resources || Graphiti.resources.reject(&:abstract_class?)
50
+ end
51
+
52
+ def run
53
+ @resources.sort_by { |resource| resource.name.to_s }.flat_map { |resource| rows_for(resource) }
54
+ end
55
+
56
+ private
57
+
58
+ def rows_for(resource_class)
59
+ model = inferred_model(resource_class)
60
+
61
+ resource_class.sideloads.map do |name, sideload|
62
+ row_for(resource_class, name, sideload, model)
63
+ end
64
+ end
65
+
66
+ def row_for(resource_class, name, sideload, model)
67
+ source = resource_ids_source(sideload)
68
+
69
+ Row.new(
70
+ resource: resource_class.name,
71
+ relationship: name,
72
+ type: sideload.type,
73
+ target: target_name(sideload),
74
+ resource_ids_source: source,
75
+ resource_ids_source_if_always: resource_ids_source_if_always(resource_class, sideload),
76
+ resource_ids_default: resource_class.belongs_to_resource_ids_by_default,
77
+ resource_ids_blocker: sideload.resource_ids_blocker,
78
+ preloaded: source == :load ? association_preloaded?(resource_class, sideload) : nil,
79
+ options: declaration_options(sideload),
80
+ findings: [
81
+ missing_association_method(sideload, model),
82
+ missing_guard_method(resource_class, sideload),
83
+ missing_sideload_filter(sideload)
84
+ ].compact
85
+ )
86
+ rescue => error
87
+ Row.new(
88
+ resource: resource_class.name,
89
+ relationship: name,
90
+ type: sideload.type,
91
+ resource_ids_source: :none,
92
+ options: {},
93
+ findings: [
94
+ Finding.new(
95
+ severity: :error,
96
+ check: :broken_relationship,
97
+ message: "#{error.class}: #{error.message.strip.lines.first.to_s.strip}"
98
+ )
99
+ ]
100
+ )
101
+ end
102
+
103
+ def declaration_options(sideload)
104
+ options = sideload.non_default_options
105
+ options[:base_scope] = "..." if sideload.customized_base_scope?
106
+ options[:scope] = "..." if sideload.class.scope_proc
107
+ options[:params] = "..." if sideload.class.params_proc
108
+ options
109
+ end
110
+
111
+ def resource_ids_source(sideload)
112
+ return :none unless sideload.render_resource_ids?
113
+
114
+ sideload.resource_ids_blocker.nil? ? :key : :load
115
+ end
116
+
117
+ def resource_ids_source_if_always(resource_class, sideload)
118
+ original = resource_class.belongs_to_resource_ids_by_default
119
+ resource_class.belongs_to_resource_ids_by_default = :always
120
+ resource_ids_source(sideload)
121
+ ensure
122
+ resource_class.belongs_to_resource_ids_by_default = original
123
+ end
124
+
125
+ def target_name(sideload)
126
+ if sideload.type == :polymorphic_belongs_to
127
+ children = sideload.children.values.filter_map { |child| short_name(child) }
128
+ return children.empty? ? nil : children.join(", ")
129
+ end
130
+
131
+ sideload.resource.class.name
132
+ rescue
133
+ nil
134
+ end
135
+
136
+ def short_name(sideload)
137
+ sideload.resource.class.name&.split("::")&.last
138
+ rescue
139
+ nil
140
+ end
141
+
142
+ def inferred_model(resource_class)
143
+ model = resource_class.model
144
+ model.is_a?(Class) ? model : nil
145
+ rescue Errors::ModelNotFound
146
+ nil
147
+ end
148
+
149
+ def missing_association_method(sideload, model)
150
+ return unless model
151
+ return if sideload.type == :polymorphic_belongs_to
152
+ return if model.method_defined?(sideload.association_name)
153
+ return if model.private_method_defined?(sideload.association_name)
154
+
155
+ Finding.new(
156
+ severity: :error,
157
+ check: :missing_association_method,
158
+ message: "#{model.name} has no ##{sideload.association_name} method",
159
+ remedy: "define it, point the relationship at the real association with `as:`, or remove the relationship"
160
+ )
161
+ end
162
+
163
+ def missing_guard_method(resource_class, sideload)
164
+ guard = sideload.readable_guard_name
165
+ return unless guard
166
+ return if defines?(resource_class, guard)
167
+ return if defines?(sideload.resource.class, guard)
168
+
169
+ Finding.new(
170
+ severity: :error,
171
+ check: :missing_guard_method,
172
+ message: "##{guard} is defined on neither #{resource_class.name} nor its related resource",
173
+ remedy: "define the guard on either resource"
174
+ )
175
+ end
176
+
177
+ def defines?(resource_class, method_name)
178
+ resource_class.method_defined?(method_name) ||
179
+ resource_class.private_method_defined?(method_name)
180
+ end
181
+
182
+ def missing_sideload_filter(sideload)
183
+ return if builds_its_own_query?(sideload)
184
+
185
+ key = sideload_filter_key(sideload)
186
+ return unless key
187
+
188
+ related = sideload.resource.class
189
+ return if related.filters.key?(key)
190
+
191
+ Finding.new(
192
+ severity: :error,
193
+ check: :missing_sideload_filter,
194
+ message: "#{related.name} is missing `filter #{key.inspect}`",
195
+ remedy: "declare the filter on the related resource"
196
+ )
197
+ end
198
+
199
+ def builds_its_own_query?(sideload)
200
+ sideload.class.params_proc || sideload.class.scope_proc
201
+ end
202
+
203
+ def sideload_filter_key(sideload)
204
+ case sideload.type
205
+ when :has_many, :has_one then sideload.foreign_key
206
+ when :many_to_many then sideload.true_foreign_key
207
+ when :belongs_to then sideload.primary_key
208
+ end
209
+ end
210
+
211
+ def association_preloaded?(resource_class, sideload)
212
+ scope = resource_class.new.base_scope
213
+ return nil unless scope.respond_to?(:includes_values)
214
+
215
+ preloads = [scope.includes_values, scope.preload_values, scope.eager_load_values]
216
+ association_names(preloads).include?(sideload.association_name.to_sym)
217
+ rescue
218
+ nil
219
+ end
220
+
221
+ def association_names(preload_values)
222
+ Array(preload_values).flat_map do |value|
223
+ case value
224
+ when Hash then value.keys.map(&:to_sym)
225
+ when Array then association_names(value)
226
+ else [value.to_sym]
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -689,6 +689,53 @@ module Graphiti
689
689
  end
690
690
  end
691
691
 
692
+ class InvalidBelongsToResourceIds < Base
693
+ def initialize(resource_class, value)
694
+ @resource_class = resource_class
695
+ @value = value
696
+ end
697
+
698
+ def message
699
+ <<~MSG
700
+ #{@resource_class.name}: belongs_to_resource_ids_by_default must be one of :foreign_key, :always, or :never. Got #{@value.inspect}.
701
+
702
+ :foreign_key - render resource ids wherever the foreign key already holds the related id (default)
703
+ :always - render them for every belongs_to, loading the association when the foreign key cannot answer
704
+ :never - render none
705
+
706
+ To render resource ids for a collection, ask for it one relationship at a time with `resource_ids: true`.
707
+ MSG
708
+ end
709
+ end
710
+
711
+ class MissingRelationshipMethod < Base
712
+ def initialize(resource_class, sideload, model)
713
+ @resource_class = resource_class
714
+ @sideload = sideload
715
+ @model = model
716
+ end
717
+
718
+ def message
719
+ <<~MSG
720
+ #{@resource_class.name}: relationship #{@sideload.name.inspect} is declared, but #{@model.class.name} has no ##{@sideload.association_name} method.
721
+
722
+ Rendering the relationship reads the association off the model. Define ##{@sideload.association_name} on #{@model.class.name}, point the relationship at the real association with `as:`, or remove the relationship.
723
+ #{resource_ids_note}
724
+ MSG
725
+ end
726
+
727
+ private
728
+
729
+ def resource_ids_note
730
+ return "" unless @sideload.render_resource_ids?
731
+
732
+ <<~MSG
733
+
734
+ resource_ids is set on this relationship, so every render reads the association, not just requests that include it. See www.graphiti.dev/concepts/relationships#customizing-relationships.
735
+ MSG
736
+ end
737
+ end
738
+
692
739
  class MissingDependentFilter < Base
693
740
  def initialize(resource, filters)
694
741
  @resource = resource
@@ -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
@@ -11,10 +11,12 @@ module Graphiti
11
11
  end
12
12
 
13
13
  def validate
14
+ validate_page
15
+
14
16
  # Right now, all requests - even reads - go through the validator
15
17
  # In the future these should have their own validation logic, but
16
18
  # for now we can just bypass
17
- return true unless @params.has_key?(:data)
19
+ return errors.blank? unless @params.has_key?(:data)
18
20
 
19
21
  resource = @root_resource
20
22
  if @params[:data].has_key?(:type)
@@ -47,6 +49,13 @@ module Graphiti
47
49
 
48
50
  private
49
51
 
52
+ def validate_page
53
+ page = @params[:page]
54
+ return if page.nil? || page.respond_to?(:each_pair)
55
+
56
+ @errors.add(:page, :invalid, message: "must be an object")
57
+ end
58
+
50
59
  def process_relationships(resource, relationships, payload_path)
51
60
  relationships.each_key do |name|
52
61
  unless resource.class.sideload(name.to_sym)
@@ -6,6 +6,16 @@ module Graphiti
6
6
  DEFAULT_MAX_PAGE_SIZE = 1_000
7
7
 
8
8
  module Overrides
9
+ BELONGS_TO_RESOURCE_IDS_VALUES = [:foreign_key, :always, :never].freeze
10
+
11
+ def belongs_to_resource_ids_by_default=(val)
12
+ unless BELONGS_TO_RESOURCE_IDS_VALUES.include?(val)
13
+ raise Errors::InvalidBelongsToResourceIds.new(self, val)
14
+ end
15
+
16
+ super
17
+ end
18
+
9
19
  def serializer=(val)
10
20
  if val
11
21
  if super(Class.new(val))
@@ -21,6 +31,10 @@ module Graphiti
21
31
  send(:prepend, Polymorphism)
22
32
  end
23
33
 
34
+ def polymorphic?
35
+ polymorphic.present?
36
+ end
37
+
24
38
  def type=(val)
25
39
  val = val&.to_sym
26
40
  if (val = super)
@@ -90,7 +104,7 @@ module Graphiti
90
104
  :attributes_schema_by_default,
91
105
  :relationships_readable_by_default,
92
106
  :relationships_writable_by_default,
93
- :always_include_resource_ids_by_default,
107
+ :belongs_to_resource_ids_by_default,
94
108
  :filters_accept_nil_by_default,
95
109
  :filters_deny_empty_by_default,
96
110
  :graphql_entrypoint,
@@ -116,6 +130,7 @@ module Graphiti
116
130
  default(klass, :attributes_schema_by_default, true)
117
131
  default(klass, :relationships_readable_by_default, true)
118
132
  default(klass, :relationships_writable_by_default, true)
133
+ default(klass, :belongs_to_resource_ids_by_default, :foreign_key)
119
134
  default(klass, :filters_accept_nil_by_default, false)
120
135
  default(klass, :filters_deny_empty_by_default, false)
121
136
 
@@ -8,6 +8,8 @@ module Graphiti
8
8
  end
9
9
 
10
10
  def serializer_for(model)
11
+ return super unless self.class.polymorphic?
12
+
11
13
  if polymorphic_child?
12
14
  serializer
13
15
  else
@@ -17,10 +19,14 @@ module Graphiti
17
19
  end
18
20
 
19
21
  def associate_all(*args)
22
+ return super unless self.class.polymorphic?
23
+
20
24
  _associate(:associate_all, *args)
21
25
  end
22
26
 
23
27
  def associate(*args)
28
+ return super unless self.class.polymorphic?
29
+
24
30
  _associate(:associate, *args)
25
31
  end
26
32
 
@@ -34,6 +40,8 @@ module Graphiti
34
40
 
35
41
  module ClassMethods
36
42
  def inherited(klass)
43
+ return super unless polymorphic?
44
+
37
45
  klass.type = nil
38
46
  klass.model = klass.infer_model
39
47
  klass.endpoint = klass.infer_endpoint
@@ -42,6 +50,8 @@ module Graphiti
42
50
  end
43
51
 
44
52
  def sideload(name)
53
+ return super unless polymorphic?
54
+
45
55
  if (split_on = name.to_s.split(/^on__/)).length > 1
46
56
  on_type, name = split_on[1].split("--").map(&:to_sym)
47
57
  end
@@ -251,7 +251,7 @@ module Graphiti
251
251
  schema[:guard] = true
252
252
  end
253
253
 
254
- if config.always_include_resource_ids?
254
+ if config.render_resource_ids?
255
255
  schema[:linkage] = true
256
256
  end
257
257