graphql_migrate_execution 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0f6058451a84d02d1fa53a18b8efcd287b31447eb9dade84f04d5ffc84d10d8e
4
+ data.tar.gz: dcf635144c19e61ac91bb1f0306db57be16bbbb5574e00f7eaa8463692f3ced0
5
+ SHA512:
6
+ metadata.gz: edc61bb290ed152c015678797cf66a4f3356476e71c3c3a2a5be507f410dc02bd625f795b6dc91574d3815110e7da49214e062503d0f9e522c64aa3caf43827f
7
+ data.tar.gz: aa765c1ddb59287f57a71d2a06b4e73e819088d95f5849aa52d68c53212cc806e41bd3e097ac3e3bcebe058730380e2e737f2a469e7d7b82a3e2fb0fcd925292
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2026 Robert Mosolgo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class Action
4
+ def initialize(migration, path, source)
5
+ @migration = migration
6
+ @path = path
7
+ @source = source
8
+ @type_definitions = Hash.new { |h, k| h[k] = TypeDefinition.new(k) }
9
+ @field_definitions_by_strategy = Hash.new { |h, k| h[k] = [] }
10
+ @total_field_definitions = 0
11
+ end
12
+
13
+ attr_reader :type_definitions
14
+
15
+ def run
16
+ parse_result = Prism.parse(@source, filepath: @path)
17
+ visitor = Visitor.new(@source, @type_definitions)
18
+ visitor.visit(parse_result.value)
19
+ @type_definitions.each do |name, type_defn|
20
+ type_defn.field_definitions.each do |f_name, f_defn|
21
+ @total_field_definitions += 1
22
+ f_defn.check_for_resolver_method
23
+ @field_definitions_by_strategy[f_defn.migration_strategy] << f_defn
24
+ end
25
+ end
26
+ nil
27
+ end
28
+
29
+ private
30
+
31
+ def call_method_on_strategy(method_name)
32
+ new_source = @source.dup
33
+ @field_definitions_by_strategy.each do |strategy_class, field_definitions|
34
+ strategy = strategy_class.new
35
+ field_definitions.each do |field_defn|
36
+ strategy.public_send(method_name, field_defn, new_source)
37
+ end
38
+ end
39
+ new_source
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class AddFuture < Action
4
+ def run
5
+ super
6
+ call_method_on_strategy(:add_future)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require "irb"
3
+ module GraphqlMigrateExecution
4
+ class Analyze < Action
5
+ def run
6
+ super
7
+ message = "Found #{@total_field_definitions} field definitions:".dup
8
+
9
+ @field_definitions_by_strategy.each do |strategy_class, definitions|
10
+ message << "\n\n#{color("#{color(strategy_class.name.split("::").last, strategy_class.color)} (#{definitions.size})", :BOLD)}:\n"
11
+ if !@migration.skip_description
12
+ message << "\n#{strategy_class::DESCRIPTION.split("\n").map { |l| l.length > 0 ? " #{l}" : l }.join("\n")}\n"
13
+ end
14
+ max_path = definitions.map { |f| f.path.size }.max + 2
15
+ definitions.each do |field_defn|
16
+ name = field_defn.path.ljust(max_path)
17
+ message << "\n - #{name} (#{field_defn.resolve_mode.inspect} -> #{field_defn.resolve_mode_key.inspect}) @ #{@path}:#{field_defn.source_line}"
18
+ end
19
+ end
20
+
21
+ message
22
+ end
23
+
24
+ private
25
+
26
+ def color(str, color_or_colors)
27
+ IRB::Color.colorize(str, Array(color_or_colors), colorable: @migration.colorable)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class DataloaderAll < Strategy
4
+ DESCRIPTION = <<~DESC
5
+ These fields can be migrated to a `.load_all` call.
6
+ DESC
7
+ self.color = :GREEN
8
+
9
+ def add_future(field_definition, new_source)
10
+ inject_resolve_keyword(new_source, field_definition, :resolve_batch)
11
+ def_node = field_definition.resolver_method.node
12
+ call_node = def_node.body.body.first
13
+ case call_node.name
14
+ when :request, :load
15
+ load_arg_node = call_node.arguments.arguments.first
16
+ with_node = call_node.receiver
17
+ source_class_node, *source_args_nodes = with_node.arguments
18
+ when :dataload
19
+ source_class_node, *source_args_nodes, load_arg_node = call_node.arguments.arguments
20
+ else
21
+ raise ArgumentError, "Unexpected DataloadAll method name: #{def_node.name.inspect}"
22
+ end
23
+
24
+ old_load_arg_s = load_arg_node.slice
25
+ new_load_arg_s = case old_load_arg_s
26
+ when "object"
27
+ "objects"
28
+ when /object((\.|\[)[:a-zA-Z0-9_\.\"\'\[\]]+)/
29
+ call_chain = $1
30
+ if /^\.[a-z0-9_A-Z]+$/.match?(call_chain)
31
+ "objects.map(&:#{call_chain[1..-1]})"
32
+ else
33
+ "objects.map { |obj| obj#{call_chain} }"
34
+ end
35
+ else
36
+ raise ArgumentError, "Failed to transform Dataloader argument: #{old_load_arg_s.inspect}"
37
+ end
38
+ new_args = [
39
+ source_class_node.slice,
40
+ *source_args_nodes.map(&:slice),
41
+ new_load_arg_s
42
+ ].join(", ")
43
+
44
+ old_method_source = def_node.slice_lines
45
+ new_method_source = old_method_source.sub(/def ([a-z_A-Z0-9]+)(\(|$| )/) do
46
+ is_adding_args = $2.size == 0
47
+ "def self.#{$1}#{is_adding_args ? "(" : $2}objects, context#{is_adding_args ? ")" : ", "}"
48
+ end
49
+ new_method_source.sub!(call_node.slice, "context.dataload_all(#{new_args})")
50
+
51
+ combined_new_source = new_method_source + "\n" + old_method_source
52
+ new_source.sub!(old_method_source, combined_new_source)
53
+ end
54
+
55
+ def remove_legacy(field_definition, new_source)
56
+ remove_resolver_method(new_source, field_definition)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class DataloaderBatch < Strategy
4
+ DESCRIPTION = <<~DESC
5
+ These fields can be rewritten to dataload in a `resolve_batch:` method.
6
+ DESC
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class DataloaderManual < Strategy
4
+ DESCRIPTION = <<~DESC
5
+ These fields use Dataloader in a way that can't be automatically migrated. You'll have to migrate them manually.
6
+ If you have a lot of these, consider opening up an issue on GraphQL-Ruby -- maybe we can find a way to programmatically support them.
7
+ DESC
8
+
9
+ self.color = :RED
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class DataloaderShorthand < Strategy
4
+ DESCRIPTION = <<~DESC
5
+ These fields can use a `dataload: ...` configuration.
6
+ DESC
7
+ self.color = :GREEN
8
+
9
+ def add_future(field_definition, new_source)
10
+ rm = field_definition.resolver_method
11
+ if (da = rm.dataload_association)
12
+ dataload_config = "{ association: #{da.inspect} }"
13
+ elsif rm.source_arg_nodes.empty?
14
+ dataload_config = rm.source_class_node.full_name
15
+ else
16
+ dataload_config = "{ with: #{rm.source_class_node.full_name}, by: [#{rm.source_arg_nodes.map { |n| Visitor.source_for_constant_node(n) }.join(", ")}] }"
17
+ end
18
+ inject_field_keyword(new_source, field_definition, :dataload, dataload_config)
19
+ end
20
+
21
+ def remove_legacy(field_definition, new_source)
22
+ remove_resolver_method(new_source, field_definition)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class DoNothing < Strategy
4
+ DESCRIPTION = "These field definitions are already future-compatible. No migration is required."
5
+ self.color = :GREEN
6
+ end
7
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class FieldDefinition
4
+ def initialize(type_definition, name, node)
5
+ @type_definition = type_definition
6
+ @name = name.to_sym
7
+ @node = node
8
+
9
+ @resolve_mode = nil
10
+ @hash_key = nil
11
+ @resolver = nil
12
+ @type_instance_method = nil
13
+ @object_direct_method = nil
14
+ @dig = nil
15
+ @already_migrated = nil
16
+
17
+ @resolver_method = nil
18
+ @unknown_options = []
19
+ end
20
+
21
+ def migration_strategy
22
+ case resolve_mode
23
+ when nil, :implicit_resolve
24
+ Implicit
25
+ when :hash_key, :object_direct_method, :dig
26
+ DoNothing
27
+ when :already_migrated
28
+ case @already_migrated.keys.first
29
+ when :resolve_each
30
+ ResolveEach
31
+ when :resolve_static
32
+ ResolveStatic
33
+ when :resolve_batch
34
+ NotImplemented
35
+ else
36
+ raise ArgumentError, "Unexpected already_migrated: #{@already_migrated.inspect}"
37
+ end
38
+ when :type_instance_method
39
+ resolver_method.migration_strategy
40
+ when :resolver
41
+ DoNothing
42
+ else
43
+ raise "No migration strategy for resolve_mode #{@resolve_mode.inspect}"
44
+ end
45
+ end
46
+
47
+ attr_reader :name, :node, :unknown_options, :type_definition, :resolve_mode
48
+
49
+ def source
50
+ node.location.slice
51
+ end
52
+
53
+ def future_resolve_shorthand
54
+ method_name = resolver_method.name
55
+ name == method_name ? true : method_name
56
+ end
57
+
58
+ attr_writer :resolve_mode
59
+
60
+ attr_accessor :hash_key, :object_direct_method, :type_instance_method, :resolver, :dig, :already_migrated
61
+
62
+ def path
63
+ @path ||= "#{type_definition.name}.#{@name}"
64
+ end
65
+
66
+ def source_line
67
+ @node.location.start_line
68
+ end
69
+
70
+ def resolver_method
71
+ case @resolver_method
72
+ when nil
73
+ method_name = @type_instance_method || @name
74
+ @resolver_method = @type_definition.resolver_methods[method_name] || :NOT_FOUND
75
+ resolver_method
76
+ when :NOT_FOUND
77
+ nil
78
+ else
79
+ @resolver_method
80
+ end
81
+ end
82
+
83
+ def implicit_resolve
84
+ @name
85
+ end
86
+
87
+ def resolve_mode_key
88
+ resolve_mode && public_send(resolve_mode)
89
+ end
90
+
91
+ def check_for_resolver_method
92
+ if resolve_mode.nil? && (resolver_method)
93
+ @resolve_mode = :type_instance_method
94
+ @type_instance_method = @name
95
+ end
96
+ nil
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class Implicit < Strategy
4
+ DESCRIPTION = <<~DESC
5
+ These fields use GraphQL-Ruby's default, implicit resolution behavior. It's changing in the future, please audit these fields and choose a migration strategy:
6
+
7
+ - `--preserve-implicit`: Don't add any new configuration; use GraphQL-Ruby's future direct method send behavior (ie `object.public_send(field_name, **arguments)`)
8
+ - `--shim-implicit`: Add a method to preserve GraphQL-Ruby's previous dynamic implicit behavior (ie, checking for `respond_to?` and `key?`)
9
+ DESC
10
+
11
+ self.color = :YELLOW
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class NotImplemented < Strategy
4
+ DESCRIPTION = "GraphQL-Ruby doesn't have a migration strategy for these fields. Automated migration may be possible -- please open an issue on GitHub with the source for these fields to investigate."
5
+ self.color = :RED
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class RemoveLegacy < Action
4
+ def run
5
+ super
6
+ call_method_on_strategy(:remove_legacy)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class ResolveEach < Strategy
4
+ DESCRIPTION = "These can be converted with `resolve_each:`. Dataloader was not detected in these resolver methods."
5
+ self.color = :GREEN
6
+
7
+ def add_future(field_definition, new_source)
8
+ inject_resolve_keyword(new_source, field_definition, :resolve_each)
9
+ replace_resolver_method(new_source, field_definition, "object, context")
10
+ end
11
+
12
+ def remove_legacy(field_definition, new_source)
13
+ remove_field_keyword(new_source, field_definition, :resolver_method)
14
+ remove_resolver_method(new_source, field_definition)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class ResolveStatic < Strategy
4
+ DESCRIPTION = "These can be converted with `resolve_static:`. Dataloader was not detected in these resolver methods."
5
+ self.color = :GREEN
6
+
7
+ def add_future(field_definition, new_source)
8
+ inject_resolve_keyword(new_source, field_definition, :resolve_static)
9
+ replace_resolver_method(new_source, field_definition, "context")
10
+ end
11
+
12
+ def remove_legacy(field_definition, new_source)
13
+ remove_field_keyword(new_source, field_definition, :resolver_method)
14
+ remove_resolver_method(new_source, field_definition)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class ResolverMethod
4
+ def initialize(name, node)
5
+ @name = name
6
+ @node = node
7
+ @parameter_names = if node.parameters
8
+ node.parameters.keywords.map(&:name)
9
+ else
10
+ []
11
+ end
12
+ @self_sends = Set.new
13
+ @calls_object = false
14
+ @calls_context = false
15
+ @calls_class = false
16
+ @calls_dataloader = false
17
+ @dataloader_call = false
18
+ end
19
+
20
+ attr_reader :name, :node, :parameter_names, :self_sends
21
+
22
+ attr_reader :source_class_node, :source_arg_nodes, :load_arg_node, :dataload_association
23
+
24
+ attr_accessor :calls_object, :calls_context, :calls_class, :calls_dataloader
25
+
26
+ attr_accessor :dataloader_call
27
+
28
+ def source
29
+ node.location.slice_lines
30
+ end
31
+
32
+ def migration_strategy
33
+ calls_to_self = self_sends.to_a
34
+ if @calls_context
35
+ calls_to_self.delete(:context)
36
+ end
37
+ if @calls_object
38
+ calls_to_self.delete(:object)
39
+ end
40
+
41
+ calls_to_self.delete(:dataloader)
42
+ calls_to_self.delete(:dataload_association)
43
+ calls_to_self.delete(:dataload_record)
44
+ calls_to_self.delete(:dataload)
45
+
46
+ # Global-ish methods:
47
+ calls_to_self.delete(:raise)
48
+
49
+ # Locals:
50
+ calls_to_self -= @parameter_names
51
+
52
+ if calls_to_self.empty?
53
+ if calls_dataloader
54
+ if !dataloader_call
55
+ return DataloaderManual
56
+ end
57
+
58
+ call_node = node.body.body.first
59
+ case call_node.name
60
+ when :dataload
61
+ @source_class_node = call_node.arguments.arguments.first
62
+ @source_arg_nodes = call_node.arguments.arguments[1...-1]
63
+ @load_arg_node = call_node.arguments.arguments.last
64
+ when :dataload_association
65
+ if (assoc_args = call_node.arguments.arguments).size == 1 &&
66
+ ((assoc_arg = assoc_args.first).is_a?(Prism::SymbolNode))
67
+ assoc_sym = assoc_arg.unescaped.to_sym
68
+ @dataload_association = assoc_sym == name ? true : assoc_sym
69
+ end
70
+ else
71
+ if (source_call = call_node.receiver) # eg dataloader.with(...).load(...)
72
+ @source_class_node = source_call.arguments.arguments.first
73
+ @source_arg_nodes = source_call.arguments.arguments[1..-1]
74
+ @load_arg_node = call_node.arguments.arguments.last
75
+ end
76
+ end
77
+
78
+ input_is_object = @load_arg_node.is_a?(Prism::CallNode) && @load_arg_node.name == :object
79
+ # Guess whether these args are free of runtime context:
80
+ 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
82
+ DataloaderShorthand
83
+ else
84
+ case call_node.name
85
+ when :load, :request, :dataload
86
+ DataloaderAll
87
+ when :load_all, :request_all, :dataload_record
88
+ DataloaderBatch
89
+ when :dataload_association
90
+ DataloaderShorthand
91
+ else
92
+ DataloaderManual
93
+ end
94
+ end
95
+ elsif calls_object
96
+ ResolveEach
97
+ else
98
+ ResolveStatic
99
+ end
100
+ else
101
+ NotImplemented
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class Strategy
4
+ def add_future(field_definition, new_source)
5
+ end
6
+
7
+ def remove_legacy(field_definition, new_source)
8
+ end
9
+
10
+ class << self
11
+ attr_accessor :color
12
+ end
13
+
14
+ private
15
+
16
+ def inject_resolve_keyword(new_source, field_definition, keyword)
17
+ value = field_definition.future_resolve_shorthand.inspect
18
+ inject_field_keyword(new_source, field_definition, keyword, value)
19
+ end
20
+
21
+ def inject_field_keyword(new_source, field_definition, keyword, value)
22
+ 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")
25
+ else
26
+ field_definition_source + ", #{keyword}: #{value}"
27
+ end
28
+ new_source.sub!(field_definition_source, new_definition_source)
29
+ end
30
+
31
+
32
+ def remove_field_keyword(new_source, field_definition, keyword)
33
+ field_definition_source = field_definition.source
34
+ new_definition_source = field_definition_source.sub(/, #{keyword}: \S+(,|$)/, "\\1")
35
+ new_source.sub!(field_definition_source, new_definition_source)
36
+ end
37
+
38
+ def replace_resolver_method(new_source, field_definition, new_params)
39
+ resolver_method = field_definition.resolver_method
40
+ method_name = resolver_method.name
41
+ old_method = resolver_method.source
42
+ new_class_method = old_method
43
+ .sub("def ", 'def self.')
44
+
45
+ if resolver_method.parameter_names.empty?
46
+ new_class_method.sub!(method_name.to_s, "#{method_name}(#{new_params})")
47
+ else
48
+ new_class_method.sub!("def self.#{method_name}(", "def self.#{method_name}(#{new_params}, ")
49
+ end
50
+
51
+ old_lines = old_method.split("\n")
52
+ new_body = old_lines.first[/^ +/] + " self.class.#{method_name}(#{new_params}#{resolver_method.parameter_names.map { |n| ", #{n}: #{n}"}.join})"
53
+ new_inst_method = [old_lines.first, new_body, old_lines.last].join("\n")
54
+
55
+ new_double_definition = new_class_method + "\n" + new_inst_method + "\n"
56
+ new_source.sub!(old_method, new_double_definition)
57
+ end
58
+
59
+ def remove_resolver_method(new_source, field_definition)
60
+ src_pattern = /(\n*)(#{Regexp.quote(field_definition.resolver_method.source)})(\n*)/
61
+ new_source.sub!(src_pattern) do
62
+ # $2 includes a newline, too
63
+ "#{$1.length > 1 ? "\n" : ""}#{$3.length > 0 ? "\n" : ""}"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class TypeDefinition
4
+ def initialize(name)
5
+ @name = name
6
+ @field_definitions = {}
7
+ @resolver_methods = {}
8
+ end
9
+
10
+ attr_reader :resolver_methods, :name, :field_definitions
11
+
12
+ def field_definition(name, node)
13
+ @field_definitions[name] = FieldDefinition.new(self, name, node)
14
+ end
15
+
16
+ def resolver_method(name, node)
17
+ @resolver_methods[name] = ResolverMethod.new(name, node)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module GraphqlMigrateExecution
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ class Visitor < Prism::Visitor
4
+ def initialize(source, type_definitions)
5
+ @source = source
6
+ @type_definitions = type_definitions
7
+ @type_definition_stack = []
8
+ @current_field_definition = nil
9
+ @current_resolver_method = nil
10
+ end
11
+
12
+ def visit_class_node(node)
13
+ if node.superclass
14
+ td = @type_definitions[node.name]
15
+ @type_definition_stack << td
16
+ end
17
+ super
18
+ ensure
19
+ if td
20
+ @type_definition_stack.pop
21
+ end
22
+ end
23
+
24
+ def visit_module_node(node)
25
+ td = @type_definitions[node.name]
26
+ @type_definition_stack << td
27
+ super
28
+ ensure
29
+ @type_definition_stack.pop
30
+ end
31
+
32
+ def visit_keyword_hash_node(node)
33
+ if @current_field_definition
34
+ node.elements.each do |assoc|
35
+ if assoc.key.is_a?(Prism::SymbolNode)
36
+ case assoc.key.unescaped
37
+ when "hash_key"
38
+ @current_field_definition.resolve_mode ||= :hash_key
39
+ @current_field_definition.hash_key = get_keyword_value(assoc.value)
40
+ when "resolver"
41
+ @current_field_definition.resolve_mode ||= :resolver
42
+ @current_field_definition.resolver = get_keyword_value(assoc.value)
43
+ when "method"
44
+ @current_field_definition.resolve_mode ||= :object_direct_method
45
+ @current_field_definition.object_direct_method = get_keyword_value(assoc.value)
46
+ when "resolver_method"
47
+ @current_field_definition.resolve_mode ||= :type_instance_method
48
+ @current_field_definition.type_instance_method = get_keyword_value(assoc.value)
49
+ when "dig"
50
+ @current_field_definition.resolve_mode ||= :dig
51
+ @current_field_definition.dig = get_keyword_value(assoc.value)
52
+ when "resolve_each", "resolve_static", "resolve_batch"
53
+ # These should override any other keywords that are discovered
54
+ @current_field_definition.resolve_mode = :already_migrated
55
+ @current_field_definition.already_migrated = { assoc.key.unescaped.to_sym => get_keyword_value(assoc.value) }
56
+ else
57
+ # fallback_value, connection, extensions, extras, resolver, mutation, subscription
58
+ @current_field_definition.unknown_options << assoc.key.unescaped
59
+ end
60
+ end
61
+ end
62
+ end
63
+ super
64
+ end
65
+
66
+ def visit_call_node(node)
67
+ if node.receiver.nil? && node.name == :field
68
+ first_arg = node.arguments.arguments.first # rubocop:disable Development/ContextIsPassedCop
69
+ if first_arg.is_a?(Prism::SymbolNode)
70
+ field_name = first_arg.unescaped
71
+ td = @type_definition_stack.last
72
+ @current_field_definition = td.field_definition(field_name, node)
73
+ else
74
+ warn "GraphQL-Ruby warning: Skipping unrecognized field definition: #{node.inspect}"
75
+ end
76
+ elsif @current_resolver_method
77
+ if node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
78
+ @current_resolver_method.self_sends.add(node.name)
79
+ case node.name
80
+ when :object
81
+ @current_resolver_method.calls_object = true
82
+ when :context
83
+ @current_resolver_method.calls_context = true
84
+ when :class
85
+ @current_resolver_method.calls_class = true
86
+ end
87
+ end
88
+
89
+ case node.name
90
+ when :dataloader, :dataload, :dataload_association, :dataload_record, :dataload_all
91
+ @current_resolver_method.calls_dataloader = true
92
+ end
93
+ end
94
+ super
95
+ ensure
96
+ if td
97
+ @current_field_definition = nil
98
+ end
99
+ end
100
+
101
+ def visit_def_node(node)
102
+ if node.receiver.nil?
103
+ td = @type_definition_stack.last
104
+ @current_resolver_method = td.resolver_method(node.name, node)
105
+ end
106
+
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
112
+ @current_resolver_method.dataloader_call = true
113
+ end
114
+ when :dataload_record, :dataload_association, :dataload
115
+ @current_resolver_method.dataloader_call = true
116
+ else
117
+ # not a single dataloader call
118
+ end
119
+ end
120
+ super
121
+ ensure
122
+ @current_resolver_method = nil
123
+ end
124
+
125
+ private
126
+
127
+ def get_keyword_value(value_node)
128
+ case value_node
129
+ when Prism::SymbolNode
130
+ value_node.unescaped.to_sym
131
+ when Prism::StringNode
132
+ value_node.unescaped
133
+ when Prism::IntegerNode, Prism::FloatNode
134
+ value_node.value
135
+ when Prism::TrueNode
136
+ true
137
+ when Prism::FalseNode
138
+ false
139
+ when Prism::ConstantPathNode, Prism::ConstantReadNode
140
+ value_node.full_name
141
+ when Prism::CallNode
142
+ :DYNAMIC_CALL_NODE
143
+ when Prism::ArrayNode
144
+ value_node.elements.map { |n| get_keyword_value(n) }
145
+ when Prism::NilNode
146
+ "nil"
147
+ else
148
+ # nil, constants, `self` ...?
149
+ raise ArgumentError, "GraphQL-MigrateExecution can't parse this keyword argument yet, but it could. Please open an issue on GraphQL-Ruby with this error message (node class: #{value_node.class})\n\n#{value_node.inspect}"
150
+ end
151
+ end
152
+
153
+ def self.constant_node?(node)
154
+ case node
155
+ when Prism::ConstantPathNode,
156
+ Prism::ConstantReadNode,
157
+ Prism::StringNode,
158
+ Prism::SymbolNode,
159
+ Prism::IntegerNode,
160
+ Prism::FloatNode,
161
+ Prism::TrueNode,
162
+ Prism::FalseNode,
163
+ Prism::NilNode
164
+ true
165
+ when Prism::ArrayNode
166
+ node.elements.all? { |n| constant_node?(n) }
167
+ else
168
+ false
169
+ end
170
+ end
171
+
172
+ def self.source_for_constant_node(value_node)
173
+ case value_node
174
+ when Prism::SymbolNode
175
+ ":#{value_node.unescaped}"
176
+ when Prism::StringNode
177
+ value_node.unescaped.inspect
178
+ when Prism::IntegerNode, Prism::FloatNode
179
+ value_node.value.inspect
180
+ when Prism::TrueNode
181
+ "true"
182
+ when Prism::FalseNode
183
+ "false"
184
+ when Prism::ConstantPathNode, Prism::ConstantReadNode
185
+ value_node.full_name
186
+ when Prism::ArrayNode
187
+ value_node.elements.map { |n| get_keyword_value(n) }
188
+ when Prism::NilNode
189
+ "nil"
190
+ else
191
+ # nil, constants, `self` ...?
192
+ raise ArgumentError, "GraphQL-MigrateExecution can't parse this keyword argument yet, but it could. Please open an issue on GraphQL-Ruby with this error message (node class: #{value_node.class})\n\n#{value_node.inspect}"
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ 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"
6
+ require "graphql_migrate_execution/field_definition"
7
+ require "graphql_migrate_execution/resolver_method"
8
+ require "graphql_migrate_execution/type_definition"
9
+ require "graphql_migrate_execution/visitor"
10
+ require "graphql_migrate_execution/strategy"
11
+ require "graphql_migrate_execution/implicit"
12
+ require "graphql_migrate_execution/do_nothing"
13
+ require "graphql_migrate_execution/resolve_each"
14
+ require "graphql_migrate_execution/resolve_static"
15
+ require "graphql_migrate_execution/not_implemented"
16
+ require "graphql_migrate_execution/dataloader_all"
17
+ require "graphql_migrate_execution/dataloader_batch"
18
+ require "graphql_migrate_execution/dataloader_manual"
19
+ require "graphql_migrate_execution/dataloader_shorthand"
20
+ require "graphql_migrate_execution/not_implemented"
21
+ require "irb"
22
+
23
+ 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
+ end
data/readme.md ADDED
@@ -0,0 +1,39 @@
1
+ # GraphqlMigrateExecution
2
+
3
+ [![Test](https://github.com/rmosolgo/graphql_migrate_execution/actions/workflows/ci.yaml/badge.svg)](https://github.com/rmosolgo/graphql_migrate_execution/actions/workflows/ci.yaml) [![Gem Version](https://badge.fury.io/rb/graphql_migrate_execution.svg)](https://badge.fury.io/rb/graphql_migrate_execution)
4
+
5
+ A command-line development tool to update your Ruby source code to support [GraphQL::Execution::Next](https://graphql-ruby.org/execution/next), then clean up unused legacy configs after you don't need them anymore.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ bundle add graphql_migrate_execution
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```
16
+ Usage: graphql_migrate_execution glob [options]
17
+
18
+ A development tool for adopting GraphQL-Ruby's new runtime module, GraphQL::Execution::Next
19
+
20
+ Inspect the files matched by `glob` and ...
21
+
22
+ - (default) print an analysis result for what can be updated
23
+ - `--migrate`: update files with new configuration
24
+ - `--cleanup`: remove legacy configuration and instance methods
25
+
26
+ Options:
27
+
28
+ --migrate Update the files with future-compatibile configuration
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/
33
+ ```
34
+
35
+ ## Develop
36
+
37
+ ```
38
+ bundle exec rake test # TEST=test/...
39
+ ```
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql_migrate_execution
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Robert Mosolgo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-03-13 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: irb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ostruct
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest-focus
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: A development script for migrating to GraphQL-Ruby's new runtime engine
83
+ email:
84
+ - rdmosolgo@gmail.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - MIT-LICENSE
90
+ - lib/graphql_migrate_execution.rb
91
+ - lib/graphql_migrate_execution/action.rb
92
+ - lib/graphql_migrate_execution/add_future.rb
93
+ - lib/graphql_migrate_execution/analyze.rb
94
+ - lib/graphql_migrate_execution/dataloader_all.rb
95
+ - lib/graphql_migrate_execution/dataloader_batch.rb
96
+ - lib/graphql_migrate_execution/dataloader_manual.rb
97
+ - lib/graphql_migrate_execution/dataloader_shorthand.rb
98
+ - lib/graphql_migrate_execution/do_nothing.rb
99
+ - lib/graphql_migrate_execution/field_definition.rb
100
+ - lib/graphql_migrate_execution/implicit.rb
101
+ - lib/graphql_migrate_execution/not_implemented.rb
102
+ - lib/graphql_migrate_execution/remove_legacy.rb
103
+ - lib/graphql_migrate_execution/resolve_each.rb
104
+ - lib/graphql_migrate_execution/resolve_static.rb
105
+ - lib/graphql_migrate_execution/resolver_method.rb
106
+ - lib/graphql_migrate_execution/strategy.rb
107
+ - lib/graphql_migrate_execution/type_definition.rb
108
+ - lib/graphql_migrate_execution/version.rb
109
+ - lib/graphql_migrate_execution/visitor.rb
110
+ - readme.md
111
+ homepage: https://github.com/rmosolgo/graphql_migrate_execution
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ 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
119
+ mailing_list_uri: https://buttondown.email/graphql-ruby
120
+ rubygems_mfa_required: 'true'
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 2.7.0
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 4.0.3
136
+ specification_version: 4
137
+ summary: A development script for migrating to GraphQL-Ruby's new runtime engine
138
+ test_files: []