graphiti 2.0.0.beta.5 → 2.0.0.beta.7

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.
@@ -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
@@ -4,8 +4,15 @@ module Graphiti
4
4
  class Query
5
5
  attr_reader :resource, :association_name, :params, :action
6
6
 
7
- # TODO: make the trailing optionals keywords once the satellite gems are rolled in - see Runner#query for the nil-padding this forces
8
- def initialize(resource, params, association_name = nil, nested_include = nil, parents = [], action = nil)
7
+ def initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil)
8
+ if positional.any?
9
+ Graphiti::DEPRECATOR.warn("Passing Query.new trailing arguments positionally is deprecated. Use association_name:/nested_include:/parents:/action: keywords.")
10
+ association_name ||= positional[0]
11
+ nested_include ||= positional[1]
12
+ parents = positional[2] if positional.length > 2
13
+ action ||= positional[3]
14
+ end
15
+
9
16
  @resource = resource
10
17
  @association_name = association_name
11
18
  @params = params
@@ -21,6 +28,13 @@ module Graphiti
21
28
  !!@association_name
22
29
  end
23
30
 
31
+ # Concurrent::Map because sideload scopes resolve on pool threads
32
+ def entity_map
33
+ return root.entity_map unless root == self
34
+
35
+ @entity_map ||= Concurrent::Map.new
36
+ end
37
+
24
38
  def top_level?
25
39
  !association?
26
40
  end
@@ -110,9 +124,10 @@ module Graphiti
110
124
  relationship_name = sideload ? sideload.name : key
111
125
  hash[relationship_name] = Query.new sl_resource,
112
126
  @params,
113
- key,
114
- sub_hash,
115
- query_parents, :all
127
+ association_name: key,
128
+ nested_include: sub_hash,
129
+ parents: query_parents,
130
+ action: :all
116
131
  else
117
132
  handle_missing_sideload(key)
118
133
  end
@@ -124,6 +139,10 @@ module Graphiti
124
139
  @parents ||= []
125
140
  end
126
141
 
142
+ def root
143
+ parents.first || self
144
+ end
145
+
127
146
  def fields
128
147
  @fields ||= begin
129
148
  hash = parse_fieldset(@params[:fields] || {})
@@ -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
 
@@ -17,7 +17,7 @@ module Graphiti
17
17
 
18
18
  # @api private
19
19
  def _all(params, opts, base_scope)
20
- runner = Runner.new(self, params, opts.delete(:query), :all)
20
+ runner = Runner.new(self, params, query: opts.delete(:query), action: :all)
21
21
  opts[:params] = params
22
22
  runner.proxy(base_scope, opts.merge(caching_options))
23
23
  end
@@ -36,7 +36,7 @@ module Graphiti
36
36
  params[:filter] ||= {}
37
37
  params[:filter][:id] = id if id
38
38
 
39
- runner = Runner.new(self, params, nil, :find)
39
+ runner = Runner.new(self, params, action: :find)
40
40
 
41
41
  find_options = {
42
42
  single: true,
@@ -56,7 +56,7 @@ module Graphiti
56
56
  # Wrap models fetched outside graphiti so they render like any other proxy
57
57
  def wrap(models, base_scope = nil)
58
58
  validate_wrap_models!(models)
59
- runner = Runner.new(self, {}, nil, :find)
59
+ runner = Runner.new(self, {}, action: :find)
60
60
  runner.proxy(base_scope, bypass_required_filters: true).tap do |proxy|
61
61
  proxy.data = models
62
62
  end