graphql_migrate_execution 0.0.1 → 1.0.0

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/bin/graphql_migrate_execution +38 -0
  3. data/lib/graphql_migrate_execution/action.rb +33 -24
  4. data/lib/graphql_migrate_execution/dataloader_all.rb +30 -14
  5. data/lib/graphql_migrate_execution/dataloader_batch.rb +66 -4
  6. data/lib/graphql_migrate_execution/dataloader_manual.rb +2 -5
  7. data/lib/graphql_migrate_execution/dataloader_shorthand.rb +20 -7
  8. data/lib/graphql_migrate_execution/do_nothing.rb +1 -1
  9. data/lib/graphql_migrate_execution/field_definition.rb +25 -2
  10. data/lib/graphql_migrate_execution/hash_key.rb +12 -0
  11. data/lib/graphql_migrate_execution/implicit.rb +22 -6
  12. data/lib/graphql_migrate_execution/migration.rb +41 -0
  13. data/lib/graphql_migrate_execution/not_implemented.rb +1 -1
  14. data/lib/graphql_migrate_execution/resolve_batch.rb +16 -0
  15. data/lib/graphql_migrate_execution/resolve_each.rb +7 -7
  16. data/lib/graphql_migrate_execution/resolve_static.rb +7 -7
  17. data/lib/graphql_migrate_execution/resolver_method.rb +80 -5
  18. data/lib/graphql_migrate_execution/strategy.rb +63 -15
  19. data/lib/graphql_migrate_execution/type_definition.rb +13 -1
  20. data/lib/graphql_migrate_execution/unsupported_current_path.rb +8 -0
  21. data/lib/graphql_migrate_execution/unsupported_extra.rb +8 -0
  22. data/lib/graphql_migrate_execution/version.rb +1 -1
  23. data/lib/graphql_migrate_execution/visitor.rb +34 -12
  24. data/lib/graphql_migrate_execution.rb +6 -30
  25. data/readme.md +154 -4
  26. metadata +13 -9
  27. data/lib/graphql_migrate_execution/add_future.rb +0 -9
  28. data/lib/graphql_migrate_execution/analyze.rb +0 -30
  29. data/lib/graphql_migrate_execution/remove_legacy.rb +0 -9
@@ -15,13 +15,19 @@ module GraphqlMigrateExecution
15
15
  @calls_class = false
16
16
  @calls_dataloader = false
17
17
  @dataloader_call = false
18
+ @uses_current_path = false
19
+ @dataload_association = nil
20
+ @dataload_record = nil
21
+ @dataload_record_using = nil
22
+ @dataload_record_find_by = nil
23
+ @return_expressions = nil
18
24
  end
19
25
 
20
26
  attr_reader :name, :node, :parameter_names, :self_sends
21
27
 
22
- attr_reader :source_class_node, :source_arg_nodes, :load_arg_node, :dataload_association
28
+ attr_reader :source_class_node, :source_arg_nodes, :load_arg_node, :dataload_association, :dataload_record, :dataload_record_using, :dataload_record_find_by
23
29
 
24
- attr_accessor :calls_object, :calls_context, :calls_class, :calls_dataloader
30
+ attr_accessor :calls_object, :calls_context, :calls_class, :calls_dataloader, :uses_current_path
25
31
 
26
32
  attr_accessor :dataloader_call
27
33
 
@@ -42,6 +48,7 @@ module GraphqlMigrateExecution
42
48
  calls_to_self.delete(:dataload_association)
43
49
  calls_to_self.delete(:dataload_record)
44
50
  calls_to_self.delete(:dataload)
51
+ calls_to_self.delete(:dataload_all)
45
52
 
46
53
  # Global-ish methods:
47
54
  calls_to_self.delete(:raise)
@@ -67,6 +74,22 @@ module GraphqlMigrateExecution
67
74
  assoc_sym = assoc_arg.unescaped.to_sym
68
75
  @dataload_association = assoc_sym == name ? true : assoc_sym
69
76
  end
77
+ when :dataload_record
78
+ if (record_args = call_node.arguments.arguments) &&
79
+ (record_arg = record_args.first) &&
80
+ (record_arg.is_a?(Prism::ConstantReadNode) || record_arg.is_a?(Prism::ConstantPathNode)) &&
81
+ (using_arg = record_args[1]) &&
82
+ # Must be `object.{something}`
83
+ (using_arg.is_a?(Prism::CallNode)) &&
84
+ (using_arg.receiver.is_a?(Prism::CallNode) && using_arg.receiver.name == :object)
85
+ @dataload_record = record_arg.full_name
86
+ @dataload_record_using = using_arg.name
87
+
88
+ if (kwargs = record_args.last).is_a?(Prism::KeywordHashNode) && (find_by_kwarg = kwargs.elements.find { |el| el.key.is_a?(Prism::SymbolNode) && el.key.unescaped == "find_by" })
89
+ find_by_node = find_by_kwarg.value
90
+ @dataload_record_find_by = find_by_node.unescaped.to_sym # Assumes a SymbolNode
91
+ end
92
+ end
70
93
  else
71
94
  if (source_call = call_node.receiver) # eg dataloader.with(...).load(...)
72
95
  @source_class_node = source_call.arguments.arguments.first
@@ -78,15 +101,16 @@ module GraphqlMigrateExecution
78
101
  input_is_object = @load_arg_node.is_a?(Prism::CallNode) && @load_arg_node.name == :object
79
102
  # Guess whether these args are free of runtime context:
80
103
  shortcutable_source_args = @source_arg_nodes && (@source_arg_nodes.empty? || (@source_arg_nodes.all? { |a| Visitor.constant_node?(a) }))
81
- if @source_class_node.is_a?(Prism::ConstantPathNode) && shortcutable_source_args && input_is_object
104
+ source_ref_is_constant = @source_class_node.is_a?(Prism::ConstantPathNode) || @source_class_node.is_a?(Prism::ConstantReadNode)
105
+ if source_ref_is_constant && shortcutable_source_args && input_is_object
82
106
  DataloaderShorthand
83
107
  else
84
108
  case call_node.name
85
109
  when :load, :request, :dataload
86
110
  DataloaderAll
87
- when :load_all, :request_all, :dataload_record
111
+ when :load_all, :request_all, :dataload_all
88
112
  DataloaderBatch
89
- when :dataload_association
113
+ when :dataload_association, :dataload_record
90
114
  DataloaderShorthand
91
115
  else
92
116
  DataloaderManual
@@ -101,5 +125,56 @@ module GraphqlMigrateExecution
101
125
  NotImplemented
102
126
  end
103
127
  end
128
+
129
+ def returns_hash?
130
+ return_expressions.all? { |exp_node| exp_node.is_a?(Prism::HashNode) || (exp_node.is_a?(Prism::CallNode) && exp_node.name == :new && exp_node.receiver.is_a?(Prism::ConstantReadNode) && exp_node.receiver.name == :Hash) }
131
+ end
132
+
133
+ def return_expressions
134
+ if @return_expressions.nil?
135
+ @return_expressions = []
136
+ find_return_expressions(@node)
137
+ end
138
+ @return_expressions
139
+ end
140
+
141
+ def returns_string_hash?
142
+ return_expressions.any? { |exp_node| exp_node.is_a?(Prism::HashNode) && exp_node.elements.all? { |el| el.key.is_a?(Prism::StringNode) } }
143
+ end
144
+
145
+ private
146
+
147
+ def find_return_expressions(node)
148
+ case node
149
+ when Prism::DefNode
150
+ find_return_expressions(node.body)
151
+ when Prism::StatementsNode
152
+ find_return_expressions(node.body.last)
153
+ when Prism::IfNode # TODO else, `?`, case
154
+ find_return_expressions(node.statements)
155
+ find_return_expressions(node.subsequent)
156
+ when Prism::ElseNode, Prism::WhenNode
157
+ find_return_expressions(node.statements)
158
+ when Prism::UnlessNode
159
+ find_return_expressions(node.statements)
160
+ find_return_expressions(node.else_clause)
161
+ when Prism::CaseNode
162
+ node.conditions.each do |cond_node|
163
+ find_return_expressions(cond_node)
164
+ end
165
+ when Prism::ReturnNode
166
+ find_return_expressions(node.arguments.first)
167
+ when Prism::LocalVariableReadNode
168
+ if (lv_write_node = @node.body.body.find { |n| n.is_a?(Prism::LocalVariableWriteNode) && n.name == node.name })
169
+ find_return_expressions(lv_write_node.value)
170
+ else
171
+ # Couldn't find assignment :'(
172
+ @return_expressions << node
173
+ end
174
+ else
175
+ # This is an expression that produces a return value
176
+ @return_expressions << node
177
+ end
178
+ end
104
179
  end
105
180
  end
@@ -1,10 +1,51 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "./action"
3
+
2
4
  module GraphqlMigrateExecution
3
5
  class Strategy
4
- def add_future(field_definition, new_source)
6
+ include Action::Colorize
7
+ def initialize(action, field_definitions)
8
+ @action = action
9
+ @migration = action.migration
10
+ @filepath = action.filepath
11
+ @action_method = @migration.action_method
12
+ @message = action.message
13
+ @result_source = action.result_source
14
+ @field_definitions = field_definitions
15
+ end
16
+
17
+ def migrate(field_definition)
18
+ end
19
+
20
+ def cleanup(field_definition)
21
+ end
22
+
23
+ def run
24
+ case @action_method
25
+ when :analyze
26
+ @message << "\n#{colorize("#{colorize(self.class.strategy_name, self.class.color)} (#{@field_definitions.size})", :BOLD)}:\n"
27
+ max_path = @field_definitions.map { |f| f.path.size }.max + 2
28
+ @field_definitions.each do |field_defn|
29
+ name = field_defn.path.ljust(max_path)
30
+ @message << "\n - #{name} (#{field_defn.resolve_mode.inspect} -> #{field_defn.resolve_mode_key.inspect}) @ #{@filepath}:#{field_defn.source_line}"
31
+ end
32
+ @message << "\n"
33
+ when :migrate, :cleanup
34
+ indent_size = @action.strategy_name_padding + 1
35
+ indent = " " * indent_size
36
+ indent2_size = @action.field_name_padding
37
+ @message << "\n#{colorize(self.class.strategy_name.ljust(indent_size), self.class.color)}"
38
+ first = true
39
+ @field_definitions.each do |field_defn|
40
+ @message << "#{first ? "" : "#{indent}"}#{field_defn.path.ljust(indent2_size)} @ #{@filepath}:#{field_defn.source_line}\n"
41
+ first = false
42
+ public_send(@action_method, field_defn)
43
+ end
44
+ end
5
45
  end
6
46
 
7
- def remove_legacy(field_definition, new_source)
47
+ def self.strategy_name
48
+ name.split("::").last
8
49
  end
9
50
 
10
51
  class << self
@@ -13,29 +54,36 @@ module GraphqlMigrateExecution
13
54
 
14
55
  private
15
56
 
16
- def inject_resolve_keyword(new_source, field_definition, keyword)
57
+ def inject_resolve_keyword(field_definition, keyword)
17
58
  value = field_definition.future_resolve_shorthand.inspect
18
- inject_field_keyword(new_source, field_definition, keyword, value)
59
+ inject_field_keyword(field_definition, keyword, value)
19
60
  end
20
61
 
21
- def inject_field_keyword(new_source, field_definition, keyword, value)
62
+ def inject_field_keyword(field_definition, keyword, value)
22
63
  field_definition_source = field_definition.source
23
- new_definition_source = if field_definition_source[/ [a-z_]+:/] # Does it already have keywords?
24
- field_definition_source.sub(/(field.+?)((?: do)|(?: {)|$)/, "\\1, #{keyword}: #{value}\\2")
64
+ pair = "#{keyword}: #{value}"
65
+ if field_definition_source.include?(pair)
66
+ # Pass, don't re-add it
67
+ elsif field_definition_source.include?("#{keyword}:")
68
+ raise "Can't re-inject #{keyword} because it's already present in the definition:\n\n#{field_definition_source}"
25
69
  else
26
- field_definition_source + ", #{keyword}: #{value}"
70
+ new_definition_source = if field_definition_source[/ [a-z_]+:/] # Does it already have keywords?
71
+ field_definition_source.sub(/(field.+?)((?: do)|(?: {)|$)/, "\\1, #{pair}\\2")
72
+ else
73
+ field_definition_source + ", #{pair}"
74
+ end
75
+ @result_source.sub!(field_definition_source, new_definition_source)
27
76
  end
28
- new_source.sub!(field_definition_source, new_definition_source)
29
77
  end
30
78
 
31
79
 
32
- def remove_field_keyword(new_source, field_definition, keyword)
80
+ def remove_field_keyword(field_definition, keyword)
33
81
  field_definition_source = field_definition.source
34
82
  new_definition_source = field_definition_source.sub(/, #{keyword}: \S+(,|$)/, "\\1")
35
- new_source.sub!(field_definition_source, new_definition_source)
83
+ @result_source.sub!(field_definition_source, new_definition_source)
36
84
  end
37
85
 
38
- def replace_resolver_method(new_source, field_definition, new_params)
86
+ def replace_resolver_method(field_definition, new_params)
39
87
  resolver_method = field_definition.resolver_method
40
88
  method_name = resolver_method.name
41
89
  old_method = resolver_method.source
@@ -53,12 +101,12 @@ module GraphqlMigrateExecution
53
101
  new_inst_method = [old_lines.first, new_body, old_lines.last].join("\n")
54
102
 
55
103
  new_double_definition = new_class_method + "\n" + new_inst_method + "\n"
56
- new_source.sub!(old_method, new_double_definition)
104
+ @result_source.sub!(old_method, new_double_definition)
57
105
  end
58
106
 
59
- def remove_resolver_method(new_source, field_definition)
107
+ def remove_resolver_method(field_definition)
60
108
  src_pattern = /(\n*)(#{Regexp.quote(field_definition.resolver_method.source)})(\n*)/
61
- new_source.sub!(src_pattern) do
109
+ @result_source.sub!(src_pattern) do
62
110
  # $2 includes a newline, too
63
111
  "#{$1.length > 1 ? "\n" : ""}#{$3.length > 0 ? "\n" : ""}"
64
112
  end
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
3
  class TypeDefinition
4
- def initialize(name)
4
+ def initialize(name, migration)
5
5
  @name = name
6
+ @migration = migration
6
7
  @field_definitions = {}
7
8
  @resolver_methods = {}
9
+ @is_resolver = false
8
10
  end
9
11
 
12
+ attr_accessor :is_resolver, :migration
13
+
10
14
  attr_reader :resolver_methods, :name, :field_definitions
11
15
 
12
16
  def field_definition(name, node)
@@ -16,5 +20,13 @@ module GraphqlMigrateExecution
16
20
  def resolver_method(name, node)
17
21
  @resolver_methods[name] = ResolverMethod.new(name, node)
18
22
  end
23
+
24
+ def returns_hash?
25
+ @resolver_methods.each_value.first.returns_hash?
26
+ end
27
+
28
+ def returns_string_hash?
29
+ @resolver_methods.each_value.first.returns_string_hash?
30
+ end
19
31
  end
20
32
  end
@@ -0,0 +1,8 @@
1
+ module GraphqlMigrateExecution
2
+ # These use `context[:current_path]` or `context.current_path` which isn't supported. Refactor these fields then try migrating again.
3
+ #
4
+ # Open an issue on GraphQL-Ruby's GitHub repo to discuss further.
5
+ class UnsupportedCurrentPath < Strategy
6
+ self.color = :RED
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module GraphqlMigrateExecution
2
+ # These use a field `extra` which isn't supported. Remove this configuration and refactor the field, then try migrating again.
3
+ #
4
+ # (Currently, only `:ast_node` and `:lookahead` are currently supported. Please open an issue on GraphQL-Ruby if this is a problem for you.)
5
+ class UnsupportedExtra < Strategy
6
+ self.color = :RED
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module GraphqlMigrateExecution
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -7,6 +7,7 @@ module GraphqlMigrateExecution
7
7
  @type_definition_stack = []
8
8
  @current_field_definition = nil
9
9
  @current_resolver_method = nil
10
+ @is_public = true
10
11
  end
11
12
 
12
13
  def visit_class_node(node)
@@ -73,6 +74,10 @@ module GraphqlMigrateExecution
73
74
  else
74
75
  warn "GraphQL-Ruby warning: Skipping unrecognized field definition: #{node.inspect}"
75
76
  end
77
+ elsif node.receiver.nil? && ((node.name == :private) || (node.name == :protected))
78
+ @is_public = false
79
+ elsif node.receiver.nil? && node.name == :public
80
+ @is_public = true
76
81
  elsif @current_resolver_method
77
82
  if node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
78
83
  @current_resolver_method.self_sends.add(node.name)
@@ -89,6 +94,15 @@ module GraphqlMigrateExecution
89
94
  case node.name
90
95
  when :dataloader, :dataload, :dataload_association, :dataload_record, :dataload_all
91
96
  @current_resolver_method.calls_dataloader = true
97
+ when :current_path
98
+ if node.receiver.is_a?(Prism::CallNode) && node.receiver.name == :context
99
+ @current_resolver_method.uses_current_path = true
100
+ end
101
+ when :[]
102
+ if node.receiver.is_a?(Prism::CallNode) && node.receiver.name == :context &&
103
+ (arg = node.arguments.arguments.first).is_a?(Prism::SymbolNode) && (arg.unescaped == "current_path")
104
+ @current_resolver_method.uses_current_path = true
105
+ end
92
106
  end
93
107
  end
94
108
  super
@@ -99,22 +113,30 @@ module GraphqlMigrateExecution
99
113
  end
100
114
 
101
115
  def visit_def_node(node)
102
- if node.receiver.nil?
116
+ if @is_public
103
117
  td = @type_definition_stack.last
104
- @current_resolver_method = td.resolver_method(node.name, node)
105
- end
118
+ if node.receiver.nil?
119
+ @current_resolver_method = td.resolver_method(node.name, node)
120
+ end
106
121
 
107
- body = node.body.body
108
- if body.length == 1 && (call_node = body.first).is_a?(Prism::CallNode)
109
- case call_node.name
110
- when :load, :request, :load_all, :request_all
111
- if (call_node2 = call_node.receiver).is_a?(Prism::CallNode) && call_node2.name == :with
122
+ if node.name == :resolve && td.resolver_methods.size == 1
123
+ td.is_resolver = true
124
+ elsif td.is_resolver && td.remove_resolver_methods.size > 1
125
+ td.is_resolver = false
126
+ end
127
+
128
+ body = node.body.body
129
+ if @current_resolver_method && body.length == 1 && (call_node = body.first).is_a?(Prism::CallNode)
130
+ case call_node.name
131
+ when :load, :request, :load_all, :request_all
132
+ if (call_node2 = call_node.receiver).is_a?(Prism::CallNode) && call_node2.name == :with
133
+ @current_resolver_method.dataloader_call = true
134
+ end
135
+ when :dataload_record, :dataload_association, :dataload, :dataload_all
112
136
  @current_resolver_method.dataloader_call = true
137
+ else
138
+ # not a single dataloader call
113
139
  end
114
- when :dataload_record, :dataload_association, :dataload
115
- @current_resolver_method.dataloader_call = true
116
- else
117
- # not a single dataloader call
118
140
  end
119
141
  end
120
142
  super
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
+ require "prism"
2
3
  require "graphql_migrate_execution/action"
3
- require "graphql_migrate_execution/add_future"
4
- require "graphql_migrate_execution/remove_legacy"
5
- require "graphql_migrate_execution/analyze"
4
+ require "graphql_migrate_execution/migration"
6
5
  require "graphql_migrate_execution/field_definition"
7
6
  require "graphql_migrate_execution/resolver_method"
8
7
  require "graphql_migrate_execution/type_definition"
@@ -10,6 +9,7 @@ require "graphql_migrate_execution/visitor"
10
9
  require "graphql_migrate_execution/strategy"
11
10
  require "graphql_migrate_execution/implicit"
12
11
  require "graphql_migrate_execution/do_nothing"
12
+ require "graphql_migrate_execution/resolve_batch"
13
13
  require "graphql_migrate_execution/resolve_each"
14
14
  require "graphql_migrate_execution/resolve_static"
15
15
  require "graphql_migrate_execution/not_implemented"
@@ -17,35 +17,11 @@ require "graphql_migrate_execution/dataloader_all"
17
17
  require "graphql_migrate_execution/dataloader_batch"
18
18
  require "graphql_migrate_execution/dataloader_manual"
19
19
  require "graphql_migrate_execution/dataloader_shorthand"
20
+ require "graphql_migrate_execution/hash_key"
21
+ require "graphql_migrate_execution/unsupported_extra"
22
+ require "graphql_migrate_execution/unsupported_current_path"
20
23
  require "graphql_migrate_execution/not_implemented"
21
24
  require "irb"
22
25
 
23
26
  module GraphqlMigrateExecution
24
- class Migration
25
- def initialize(glob, concise: false, migrate: false, cleanup: false, only: nil, implicit: nil, colorable: IRB::Color.colorable?)
26
- @glob = glob
27
- @skip_description = concise
28
- @colorable = colorable
29
- @only = only
30
- @implicit = implicit
31
- @action_class = if migrate
32
- AddFuture
33
- elsif cleanup
34
- RemoveLegacy
35
- else
36
- Analyze
37
- end
38
- end
39
-
40
- attr_reader :skip_description, :colorable
41
-
42
-
43
- def run
44
- Dir.glob(@glob).each do |filepath|
45
- source = File.read(filepath)
46
- file_migrate = @action_class.new(self, filepath, source)
47
- puts file_migrate.run
48
- end
49
- end
50
- end
51
27
  end
data/readme.md CHANGED
@@ -25,15 +25,165 @@ Inspect the files matched by `glob` and ...
25
25
 
26
26
  Options:
27
27
 
28
- --migrate Update the files with future-compatibile configuration
28
+ --migrate Update the files with future-compatible configuration
29
29
  --cleanup Remove resolver instance methods for GraphQL-Ruby's old runtime
30
- --concise Don't print migration strategy descriptions
31
- --implicit MODE Handle implicit field resolution using MODE
32
- --only PATTERN Only analyze or update fields whose path (`Type.field`) matches /PATTERN/
30
+ --dry-run Don't actually modify files
31
+ --implicit [MODE] Handle implicit field resolution this way (ignore / hash_key / hash_key_string)
33
32
  ```
34
33
 
34
+ ## Supported Field Resolution Patterns
35
+
36
+ Check out the docs for refactors implemented by this tool:
37
+
38
+ - Dataloader-based fields:
39
+ - [`DataloaderShorthand`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/DataloaderShorthand.html): use the new `dataload: ...` field configuration shorthand
40
+ - [`DataloaderAll`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/DataloaderAll.html): use a `dataload_all(...)` call to fetch data for a batch of objects
41
+ - [`DataloaderBatch`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/DataloaderBatch.html): Fetch a list of results _for each object_ (2-layer list)
42
+ - [`DataloaderManual`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/DataloaderManual.html): 💔 Identifies dataloader usage which can't be migrated
43
+ - Migrate method:
44
+ - These identify Ruby code in the method which only uses `context` and `object` and migrates it to a suitable class method. Then, it updates the instance method to call the new class method and adds the suitable future-compatible config.
45
+ - [`ResolveBatch`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/ResolveBatch.html)
46
+ - [`ResolveEach`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/ResolveEach.html)
47
+ - [`ResolveStatic`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/ResolveStatic.html)
48
+ - 💔 Not migratable:
49
+ - [`NotImplemented`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/NotImplemented.html): This field couldn't be matched to a refactor
50
+ - [`UnsupportedCurrentPath`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/UnsupportedCurrentPath.html): uses `context[:current_path]` which isn't supported anymore - [`UnsupportedExtra`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/UnsupportedExtra.html): as at least one `extras: ...` configuration which isn't supported anymore
51
+ - Configuration:
52
+ - [`DoNothing`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/DoNothing.html): Already includes future-compatible configuration
53
+ - [`HashKey`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/HashKey.html): Can be migrated using `hash_key:` (especially useful for Resolvers and Mutations)
54
+ - [`Implicit`](https://rmosolgo.github.io/graphql_migrate_execution/GraphqlMigrateExecution/Implicit.html): ⚠️ GraphQL-Ruby's default field resolution is changing, see the doc
55
+
56
+ ## Unsupported Field Resolution Patterns
57
+
58
+ Here are a few fields in my app that this tool didn't handle automatically, along with my manual migrations:
59
+
60
+ - __Working with a dataloaded value__:
61
+
62
+ This resolver called arbitrary code _after_ using Dataloader:
63
+
64
+ ```ruby
65
+ field :is_locked_to_viewer, Boolean, null: false
66
+
67
+ def is_locked_to_viewer
68
+ status = dataload(Sources::GrowthTaskStatusForUserSource, context[:current_user], object)
69
+ status == :LOCKED
70
+ end
71
+ ```
72
+
73
+ I _could_ have handled this by refactoring the dataload call to return `true|false`. Then it could have been auto-migrated. Instead, I migrated it like this:
74
+
75
+ ```ruby
76
+ field :is_locked_to_viewer, Boolean, null: false, resolve_batch: true
77
+
78
+ def self.is_locked_to_viewer(objects, context)
79
+ statuses = context.dataload_all(Sources::GrowthTaskStatusForUserSource, context[:current_user], objects)
80
+ statuses.map { |s| s == :LOCKED }
81
+ end
82
+
83
+ def is_locked_to_viewer
84
+ self.class.is_locked_to_viewer([ object ], context).first
85
+ end
86
+ ```
87
+
88
+ - __Conditional dataloader call__:
89
+
90
+ This field only called dataloader in some cases:
91
+
92
+ ```ruby
93
+ field :viewer_growth_task_submission, GrowthTaskSubmissionType
94
+
95
+ def viewer_growth_task_submission
96
+ if object.frequency.present?
97
+ # TODO should not include a recurring submission whose duration has passed
98
+ nil
99
+ else
100
+ context.dataloader.with(Sources::GrowthTaskForViewerSource, context[:current_user]).request(object.id)
101
+ end
102
+ end
103
+ ```
104
+
105
+ It _could_ have been auto-migrated if I made two refactors:
106
+
107
+ - Update the Source to receive `object` instead of `object.id`
108
+ - Update the Source's `#fetch` to return `nil` based on `object.frequency.present?`
109
+
110
+ But I didn't do that. Instead, I migrated it manually:
111
+
112
+ ```ruby
113
+ field :viewer_growth_task_submission, GrowthTaskSubmissionType, resolve_batch: true
114
+
115
+ def self.viewer_growth_task_submission(objects, context)
116
+ requests = objects.map do |object|
117
+ if object.frequency.present?
118
+ # TODO should not include a recurring submission whose duration has passed
119
+ nil
120
+ else
121
+ context.dataloader.with(Sources::GrowthTaskForViewerSource, context[:current_user]).request(object.id)
122
+ end
123
+ end
124
+ requests.map { |l| l&.load }
125
+ end
126
+
127
+ def viewer_growth_task_submission
128
+ self.class.viewer_growth_task_submission([ object ], context).first
129
+ end
130
+ ```
131
+
132
+ - __Resolver that calls another resolver:__
133
+
134
+ The tool just gives up when it sees calls on `self`. It didn't handle this:
135
+
136
+ ```ruby
137
+ field :current_user, Types::UserType
138
+
139
+ def current_user
140
+ context[:current_user]
141
+ end
142
+
143
+ field :unread_notification_count, Integer, null: false
144
+
145
+ def unread_notification_count
146
+ # vvvvvvvvv Calls the resolver method above
147
+ current_user ? current_user.notification_events.unread.count : 0
148
+ end
149
+ ```
150
+
151
+ I migrated it manually:
152
+
153
+ ```ruby
154
+ field :unread_notification_count, Integer, null: false, resolve_static: true
155
+
156
+ def self.unread_notification_count(context)
157
+ if (cu = current_user(context))
158
+ cu.notification_events.unread.count
159
+ else
160
+ 0
161
+ end
162
+ end
163
+
164
+ def unread_notification_count
165
+ self.class.unread_notification_count(context)
166
+ end
167
+ ```
168
+
169
+ - __Single-line method definition__:
170
+
171
+ The tool's heavy-handed Ruby source generation botched this:
172
+
173
+ ```ruby
174
+ field :growth_levels, Types::GrowthLevelType.connection_type, null: false, resolve_each: true
175
+ def growth_levels; object.growth_levels.by_sequence; end;
176
+ ```
177
+
178
+ This tool could be improved to properly handle single-line methods -- open an issue if you need this.
179
+
35
180
  ## Develop
36
181
 
37
182
  ```
38
183
  bundle exec rake test # TEST=test/...
39
184
  ```
185
+
186
+
187
+ ## TODO
188
+
189
+ - [ ] Does `--cleanup` work on my app? I haven't run it yet.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_migrate_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-13 00:00:00.000000000 Z
10
+ date: 2026-03-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: irb
@@ -82,29 +82,33 @@ dependencies:
82
82
  description: A development script for migrating to GraphQL-Ruby's new runtime engine
83
83
  email:
84
84
  - rdmosolgo@gmail.com
85
- executables: []
85
+ executables:
86
+ - graphql_migrate_execution
86
87
  extensions: []
87
88
  extra_rdoc_files: []
88
89
  files:
89
90
  - MIT-LICENSE
91
+ - bin/graphql_migrate_execution
90
92
  - lib/graphql_migrate_execution.rb
91
93
  - lib/graphql_migrate_execution/action.rb
92
- - lib/graphql_migrate_execution/add_future.rb
93
- - lib/graphql_migrate_execution/analyze.rb
94
94
  - lib/graphql_migrate_execution/dataloader_all.rb
95
95
  - lib/graphql_migrate_execution/dataloader_batch.rb
96
96
  - lib/graphql_migrate_execution/dataloader_manual.rb
97
97
  - lib/graphql_migrate_execution/dataloader_shorthand.rb
98
98
  - lib/graphql_migrate_execution/do_nothing.rb
99
99
  - lib/graphql_migrate_execution/field_definition.rb
100
+ - lib/graphql_migrate_execution/hash_key.rb
100
101
  - lib/graphql_migrate_execution/implicit.rb
102
+ - lib/graphql_migrate_execution/migration.rb
101
103
  - lib/graphql_migrate_execution/not_implemented.rb
102
- - lib/graphql_migrate_execution/remove_legacy.rb
104
+ - lib/graphql_migrate_execution/resolve_batch.rb
103
105
  - lib/graphql_migrate_execution/resolve_each.rb
104
106
  - lib/graphql_migrate_execution/resolve_static.rb
105
107
  - lib/graphql_migrate_execution/resolver_method.rb
106
108
  - lib/graphql_migrate_execution/strategy.rb
107
109
  - lib/graphql_migrate_execution/type_definition.rb
110
+ - lib/graphql_migrate_execution/unsupported_current_path.rb
111
+ - lib/graphql_migrate_execution/unsupported_extra.rb
108
112
  - lib/graphql_migrate_execution/version.rb
109
113
  - lib/graphql_migrate_execution/visitor.rb
110
114
  - readme.md
@@ -113,9 +117,9 @@ licenses:
113
117
  - MIT
114
118
  metadata:
115
119
  homepage_uri: https://graphql-ruby.org
116
- changelog_uri: https://github.com/rmosolgo/graphql-ruby/blob/master/CHANGELOG.md
117
- source_code_uri: https://github.com/rmosolgo/graphql-ruby
118
- bug_tracker_uri: https://github.com/rmosolgo/graphql-ruby/issues
120
+ changelog_uri: https://github.com/rmosolgo/graphql_migrate_execution
121
+ source_code_uri: https://github.com/rmosolgo/graphql_migrate_execution
122
+ bug_tracker_uri: https://github.com/rmosolgo/graphql_migrate_execution/issues
119
123
  mailing_list_uri: https://buttondown.email/graphql-ruby
120
124
  rubygems_mfa_required: 'true'
121
125
  rdoc_options: []