graphql_migrate_execution 0.0.2 → 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 +5 -6
  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 +7 -5
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fbd874076a766e4abf14571cc0d4d9a348f5868f89df74f9e0f2ad421e65fd4
4
- data.tar.gz: 013e06b1123c06b80d9fd2e270ace5032890c8d35cb8bac8f91e78e8805755ac
3
+ metadata.gz: 2835acca245bcfe0c626420ae2ce5ac96ee916e0a4dbb35a8db5f1c97a786e1b
4
+ data.tar.gz: 4522d268937cf7a4fd925b4cf0a8fb492ec797006cf48e9972ae74847390eb21
5
5
  SHA512:
6
- metadata.gz: 24a586f44159ceac3d674171cbc25d31c342c7238f6d025954f833e6000231a61893c04bdac741df32e5e90c9f30dd22976069eca8c7af66a8c0b1571fc4c6f9
7
- data.tar.gz: fc25e0c9baf95aed3104802604bbd8aba57cd528dd136c912f5f459c973f08f1b510623b13b9a7abcae272ce98c834572ca64b4391841b184c6810e19d99ecc6
6
+ metadata.gz: ad8ba9248a12a116f6e6170fbae55029914bd43b71e2aefcab54c85b87af4347e19cc3aa6a0ba87a9df2a1d7caf7594f901ae974e88208c82e473adc61378e3e
7
+ data.tar.gz: 128a44c801b750c31ad661af93745568a529389afb9fd738534125aba4a708d7c9ab7293b0e02cde6453473486763a09561e16892ac75c93fc39e91659e67672
@@ -20,13 +20,10 @@ parser.banner = <<~TEXT
20
20
 
21
21
  TEXT
22
22
 
23
- parser.on("--migrate", "Update the files with future-compatibile configuration")
23
+ parser.on("--migrate", "Update the files with future-compatible configuration")
24
24
  parser.on("--cleanup", "Remove resolver instance methods for GraphQL-Ruby's old runtime")
25
- parser.on("--concise", "Don't print migration strategy descriptions")
26
-
27
- # TODO:
28
- parser.on("--implicit MODE", "Handle implicit field resolution using MODE")
29
- parser.on("--only PATTERN", "Only analyze or update fields whose path (`Type.field`) matches /PATTERN/")
25
+ parser.on("--dry-run", "Don't actually modify files")
26
+ parser.on("--implicit [MODE]", String, "Handle implicit field resolution this way (ignore / hash_key / hash_key_string)")
30
27
 
31
28
  parser.parse!(into: options)
32
29
 
@@ -35,5 +32,7 @@ filename = ARGV.shift || begin
35
32
  exit 1
36
33
  end
37
34
 
35
+ options[:dry_run] = options.delete(:"dry-run")
36
+
38
37
  migration = GraphqlMigrateExecution::Migration.new(filename, **options)
39
38
  migration.run
@@ -1,42 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
3
  class Action
4
- def initialize(migration, path, source)
4
+ module Colorize
5
+ private
6
+ def colorize(str, color_or_colors)
7
+ IRB::Color.colorize(str, Array(color_or_colors), colorable: @migration.colorable)
8
+ end
9
+ end
10
+
11
+ include Colorize
12
+
13
+ def initialize(migration, filepath, ruby_source)
5
14
  @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
15
+ @filepath = filepath
16
+ @ruby_source = ruby_source
17
+ @message = "".dup
18
+ @result_source = @ruby_source.dup
19
+ @strategy_name_padding = nil
20
+ @field_name_padding = nil
11
21
  end
12
22
 
13
- attr_reader :type_definitions
23
+ attr_reader :message, :result_source, :migration, :filepath, :strategy_name_padding, :field_name_padding
14
24
 
15
25
  def run
16
- parse_result = Prism.parse(@source, filepath: @path)
17
- visitor = Visitor.new(@source, @type_definitions)
26
+ parse_result = Prism.parse(@ruby_source, filepath: @filepath)
27
+ type_definitions = Hash.new { |h, k| h[k] = TypeDefinition.new(k, @migration) }
28
+ visitor = Visitor.new(@ruby_source, type_definitions)
18
29
  visitor.visit(parse_result.value)
19
- @type_definitions.each do |name, type_defn|
30
+ total_field_definitions = 0
31
+ field_definitions_by_strategy = Hash.new { |h, k| h[k] = [] }
32
+ type_definitions.each do |name, type_defn|
20
33
  type_defn.field_definitions.each do |f_name, f_defn|
21
- @total_field_definitions += 1
34
+ total_field_definitions += 1
22
35
  f_defn.check_for_resolver_method
23
- @field_definitions_by_strategy[f_defn.migration_strategy] << f_defn
36
+ field_definitions_by_strategy[f_defn.migration_strategy] << f_defn
24
37
  end
25
38
  end
26
- nil
27
- end
28
-
29
- private
30
39
 
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
40
+ @message << "#{colorize(@filepath, :BOLD)}: "
41
+ @message << "Found #{total_field_definitions} field definition#{total_field_definitions == 1 ? "" : "s"}:\n"
42
+ @strategy_name_padding = field_definitions_by_strategy.each_key.map { |sc| sc.strategy_name.size }.max
43
+ @field_name_padding = field_definitions_by_strategy.each_value.flat_map { |fds| fds.map { |fd| fd.path.size } }.max
44
+ field_definitions_by_strategy.each do |strategy_class, field_definitions|
45
+ strategy = strategy_class.new(self, field_definitions)
46
+ strategy.run
38
47
  end
39
- new_source
48
+ @message << "\n"
40
49
  end
41
50
  end
42
51
  end
@@ -1,21 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # This field calls dataloader with some property of `object`. It can be migrated to use `dataload_all(...)` and `objects.map { |object| ... }`.
4
+ #
5
+ # ```ruby
6
+ # # Previous:
7
+ # def my_field
8
+ # dataload(Sources::GetThing, object.some_attribute)
9
+ # end
10
+ #
11
+ # # New:
12
+ # def self.my_field(objects, context)
13
+ # context.dataload_all(Sources::GetThing, objects.map { |object| object.some_attribute })
14
+ # end
15
+ # ```
3
16
  class DataloaderAll < Strategy
4
- DESCRIPTION = <<~DESC
5
- These fields can be migrated to a `.load_all` call.
6
- DESC
7
17
  self.color = :GREEN
8
18
 
9
- def add_future(field_definition, new_source)
10
- inject_resolve_keyword(new_source, field_definition, :resolve_batch)
19
+ def migrate(field_definition)
20
+ inject_resolve_keyword(field_definition, :resolve_batch)
21
+ inject_batch_dataloader_method(field_definition, [:request, :load], :dataload, "map")
22
+ end
23
+
24
+ def cleanup(field_definition)
25
+ remove_resolver_method(field_definition)
26
+ end
27
+
28
+ private
29
+
30
+ def inject_batch_dataloader_method(field_definition, longhand_methods, shorthand_method, map_method)
11
31
  def_node = field_definition.resolver_method.node
12
32
  call_node = def_node.body.body.first
13
33
  case call_node.name
14
- when :request, :load
34
+ when *longhand_methods
15
35
  load_arg_node = call_node.arguments.arguments.first
16
36
  with_node = call_node.receiver
17
37
  source_class_node, *source_args_nodes = with_node.arguments
18
- when :dataload
38
+ when shorthand_method
19
39
  source_class_node, *source_args_nodes, load_arg_node = call_node.arguments.arguments
20
40
  else
21
41
  raise ArgumentError, "Unexpected DataloadAll method name: #{def_node.name.inspect}"
@@ -28,9 +48,9 @@ module GraphqlMigrateExecution
28
48
  when /object((\.|\[)[:a-zA-Z0-9_\.\"\'\[\]]+)/
29
49
  call_chain = $1
30
50
  if /^\.[a-z0-9_A-Z]+$/.match?(call_chain)
31
- "objects.map(&:#{call_chain[1..-1]})"
51
+ "objects.#{map_method}(&:#{call_chain[1..-1]})"
32
52
  else
33
- "objects.map { |obj| obj#{call_chain} }"
53
+ "objects.#{map_method} { |obj| obj#{call_chain} }"
34
54
  end
35
55
  else
36
56
  raise ArgumentError, "Failed to transform Dataloader argument: #{old_load_arg_s.inspect}"
@@ -49,11 +69,7 @@ module GraphqlMigrateExecution
49
69
  new_method_source.sub!(call_node.slice, "context.dataload_all(#{new_args})")
50
70
 
51
71
  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)
72
+ @result_source.sub!(old_method_source, combined_new_source)
57
73
  end
58
74
  end
59
75
  end
@@ -1,8 +1,70 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "./dataloader_all"
3
+
2
4
  module GraphqlMigrateExecution
3
- class DataloaderBatch < Strategy
4
- DESCRIPTION = <<~DESC
5
- These fields can be rewritten to dataload in a `resolve_batch:` method.
6
- DESC
5
+ # These fields return an array of values using Dataloader, based on a method or attribute of `object`. They can be migrated to `dataloader_all` calls, using `object.flat_map`.
6
+ #
7
+ # **TODO**: This is not quite right yet. It returns a single array instead of an array of arrays.
8
+ #
9
+ # Instead, this should create an Array of arrays using `dataloader.request_all`.
10
+ class DataloaderBatch < DataloaderAll
11
+ self.color = :GREEN
12
+
13
+ def migrate(field_definition)
14
+ inject_resolve_keyword(field_definition, :resolve_batch)
15
+ # inject_batch_dataloader_method(field_definition, [:request_all, :load_all], :dataload_all, "flat_map")
16
+
17
+ def_node = field_definition.resolver_method.node
18
+ call_node = def_node.body.body.first
19
+ case call_node.name
20
+ when :request_all, :load_all
21
+ load_arg_node = call_node.arguments.arguments.first
22
+ with_node = call_node.receiver
23
+ source_class_node, *source_args_nodes = with_node.arguments
24
+ when :dataload_all
25
+ source_class_node, *source_args_nodes, load_arg_node = call_node.arguments.arguments
26
+ else
27
+ raise ArgumentError, "Unexpected DataloadAll method name: #{def_node.name.inspect}"
28
+ end
29
+
30
+ old_load_arg_s = load_arg_node.slice
31
+ new_load_arg_s = case old_load_arg_s
32
+ when "object"
33
+ "object"
34
+ when /object((\.|\[)[:a-zA-Z0-9_\.\"\'\[\]]+)/
35
+ call_chain = $1
36
+ "object#{call_chain}"
37
+ else
38
+ raise ArgumentError, "Failed to transform Dataloader argument: #{old_load_arg_s.inspect}"
39
+ end
40
+ new_source_args = [
41
+ source_class_node.slice,
42
+ *source_args_nodes.map(&:slice)
43
+ ].join(", ")
44
+
45
+ old_method_source = def_node.slice_lines
46
+ new_method_source = old_method_source.sub(/def ([a-z_A-Z0-9]+)(\(|$| )/) do
47
+ is_adding_args = $2.size == 0
48
+ "def self.#{$1}#{is_adding_args ? "(" : $2}objects, context#{is_adding_args ? ")" : ", "}"
49
+ end
50
+
51
+ old_source_lines = call_node.slice_lines
52
+ leading_whitespace = old_source_lines[/^\s+/]
53
+
54
+ new_method_body = <<~RUBY
55
+ #{leading_whitespace}requests = objects.map { |object| context.dataloader.with(#{new_source_args}).request_all(#{new_load_arg_s}) }
56
+ #{leading_whitespace}requests.map! { |reqs| reqs.map!(&:load) } # replace dataloader requests with loaded data
57
+ #{leading_whitespace}requests
58
+ RUBY
59
+
60
+ new_method_source.sub!(old_source_lines, new_method_body)
61
+
62
+ combined_new_source = new_method_source + "\n" + old_method_source
63
+ @result_source.sub!(old_method_source, combined_new_source)
64
+ end
65
+
66
+ def cleanup(field_definition)
67
+ remove_resolver_method(field_definition)
68
+ end
7
69
  end
8
70
  end
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # These fields use Dataloader in a way that can't be automatically migrated. You'll have to migrate them manually.
4
+ # 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.
3
5
  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
6
  self.color = :RED
10
7
  end
11
8
  end
@@ -1,25 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # These fields can use a `dataload: ...` configuration. They use a single, simple dataloader call:
4
+ #
5
+ # - `dataload_assocation(...)`
6
+ # - `dataload_record(...)`
7
+ # - `dataload(...)` or `dataloader.with(...).load(...)`
8
+ #
9
+ # and they don't make calls on `self` inside those expressions (except for `object` and `context`).
3
10
  class DataloaderShorthand < Strategy
4
- DESCRIPTION = <<~DESC
5
- These fields can use a `dataload: ...` configuration.
6
- DESC
7
11
  self.color = :GREEN
8
12
 
9
- def add_future(field_definition, new_source)
13
+ def migrate(field_definition)
10
14
  rm = field_definition.resolver_method
11
15
  if (da = rm.dataload_association)
12
16
  dataload_config = "{ association: #{da.inspect} }"
17
+ elsif (dr = rm.dataload_record)
18
+ dataload_config = "{ model: #{dr}".dup
19
+ if (dr_using = rm.dataload_record_using)
20
+ dataload_config << ", using: #{dr_using.inspect}"
21
+ end
22
+ if (fb = rm.dataload_record_find_by)
23
+ dataload_config << ", find_by: #{fb.inspect}"
24
+ end
25
+ dataload_config << " }"
13
26
  elsif rm.source_arg_nodes.empty?
14
27
  dataload_config = rm.source_class_node.full_name
15
28
  else
16
29
  dataload_config = "{ with: #{rm.source_class_node.full_name}, by: [#{rm.source_arg_nodes.map { |n| Visitor.source_for_constant_node(n) }.join(", ")}] }"
17
30
  end
18
- inject_field_keyword(new_source, field_definition, :dataload, dataload_config)
31
+ inject_field_keyword(field_definition, :dataload, dataload_config)
19
32
  end
20
33
 
21
- def remove_legacy(field_definition, new_source)
22
- remove_resolver_method(new_source, field_definition)
34
+ def cleanup(field_definition)
35
+ remove_resolver_method(field_definition)
23
36
  end
24
37
  end
25
38
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # These field definitions are already future-compatible. No migration is required.
3
4
  class DoNothing < Strategy
4
- DESCRIPTION = "These field definitions are already future-compatible. No migration is required."
5
5
  self.color = :GREEN
6
6
  end
7
7
  end
@@ -19,9 +19,23 @@ module GraphqlMigrateExecution
19
19
  end
20
20
 
21
21
  def migration_strategy
22
+ if unsupported_extras?
23
+ return UnsupportedExtra
24
+ elsif resolver_method&.uses_current_path
25
+ return UnsupportedCurrentPath
26
+ end
27
+
28
+ if @type_definition.is_resolver && @type_definition.returns_hash?
29
+ return HashKey
30
+ end
31
+
22
32
  case resolve_mode
23
33
  when nil, :implicit_resolve
24
- Implicit
34
+ if @type_definition.migration.implicit == "ignore"
35
+ DoNothing
36
+ else
37
+ Implicit
38
+ end
25
39
  when :hash_key, :object_direct_method, :dig
26
40
  DoNothing
27
41
  when :already_migrated
@@ -31,7 +45,7 @@ module GraphqlMigrateExecution
31
45
  when :resolve_static
32
46
  ResolveStatic
33
47
  when :resolve_batch
34
- NotImplemented
48
+ ResolveBatch
35
49
  else
36
50
  raise ArgumentError, "Unexpected already_migrated: #{@already_migrated.inspect}"
37
51
  end
@@ -95,5 +109,14 @@ module GraphqlMigrateExecution
95
109
  end
96
110
  nil
97
111
  end
112
+
113
+ private
114
+
115
+ def unsupported_extras?
116
+ (kwargs = @node.arguments.arguments.last).is_a?(Prism::KeywordHashNode) &&
117
+ (extras_el = kwargs.elements.find { |el| el.key.is_a?(Prism::SymbolNode) && el.key.unescaped == "extras" }) &&
118
+ ((extras_val = extras_el.value).is_a?(Prism::ArrayNode)) &&
119
+ (extras_val.elements.any? { |el| (!el.is_a?(Prism::SymbolNode)) || (el.unescaped != "ast_node" && el.unescaped != "lookahead")})
120
+ end
98
121
  end
99
122
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ # These can be future-proofed with `hash_key: ...` configurations.
4
+ class HashKey < Strategy
5
+ self.color = :GREEN
6
+
7
+ def migrate(field_definition)
8
+ key = field_definition.type_definition.returns_string_hash? ? field_definition.name.to_s.inspect : field_definition.name.inspect
9
+ inject_field_keyword(field_definition, :hash_key, key)
10
+ end
11
+ end
12
+ end
@@ -1,13 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # These fields use GraphQL-Ruby's default, implicit resolution behavior. It's changing in the future:
4
+ # - __Currently__, it tries a combination of method calls and hash key lookups
5
+ # - __In the future__, it will only try a method call: `object.public_send(field_name, **field_args)`
6
+ #
7
+ # If your field _sometimes_ uses a method call, and other times uses a hash key, you'll have to implement that logic in the field itself
8
+ #
9
+ #. If your field always uses a method call, use `--implicit=ignore` to disable the warning from this refactor. (Your field will be supported as-is).
10
+ #
11
+ # If your field always uses a hash key, use `--implicit=hash_key` (to add a Symbol-based `hash_key: ...` configuration) or `--implicit=hash_key_string` (to add a String-based one).
3
12
  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
13
 
11
14
  self.color = :YELLOW
15
+
16
+ def migrate(field_definition)
17
+ case @migration.implicit
18
+ when "ignore", nil
19
+ # do nothing
20
+ when "hash_key"
21
+ inject_field_keyword(field_definition, :hash_key, field_definition.name.inspect)
22
+ when "hash_key_string"
23
+ inject_field_keyword(field_definition, :hash_key, field_definition.name.to_s.inspect)
24
+ else
25
+ raise ArgumentError, "Unexpected `--implicit` argument: #{@migration.implicit.inspect}"
26
+ end
27
+ end
12
28
  end
13
29
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ # A run of this tool, called by `bin/graphql_migrate_execution`.
4
+ class Migration
5
+ def initialize(glob, dry_run: false, migrate: false, cleanup: false, implicit: nil, colorable: IRB::Color.colorable?)
6
+ @glob = glob
7
+ if /\/[^.]*$/.match?(@glob)
8
+ if !@glob.end_with?("/")
9
+ @glob += "/"
10
+ end
11
+ @glob += "*.rb"
12
+ end
13
+ @dry_run = dry_run || (migrate == false && cleanup == false)
14
+ @colorable = colorable
15
+ @implicit = implicit
16
+ @action_method = if migrate
17
+ :migrate
18
+ elsif cleanup
19
+ :cleanup
20
+ else
21
+ :analyze
22
+ end
23
+ end
24
+
25
+ attr_reader :colorable, :action_method, :implicit
26
+
27
+ def run
28
+ Dir.glob(@glob).each do |filepath|
29
+ source = File.read(filepath)
30
+ action = Action.new(self, filepath, source)
31
+ action.run
32
+
33
+ if !@dry_run
34
+ File.write(filepath, action.result_source)
35
+ end
36
+
37
+ puts action.message
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # 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.
3
4
  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
5
  self.color = :RED
6
6
  end
7
7
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ # This is just used for cleaning up code.
4
+ class ResolveBatch < Strategy
5
+ self.color = :GREEN
6
+
7
+ def migrate(field_definition)
8
+ raise "Not implemented yet -- this doesn't actually migrate code, just cleans up old code."
9
+ end
10
+
11
+ def cleanup(field_definition)
12
+ remove_field_keyword(field_definition, :resolver_method)
13
+ remove_resolver_method(field_definition)
14
+ end
15
+ end
16
+ end
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # These can be converted with `resolve_each:`. Dataloader was not detected in these resolver methods.
3
4
  class ResolveEach < Strategy
4
- DESCRIPTION = "These can be converted with `resolve_each:`. Dataloader was not detected in these resolver methods."
5
5
  self.color = :GREEN
6
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")
7
+ def migrate(field_definition)
8
+ inject_resolve_keyword(field_definition, :resolve_each)
9
+ replace_resolver_method(field_definition, "object, context")
10
10
  end
11
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)
12
+ def cleanup(field_definition)
13
+ remove_field_keyword(field_definition, :resolver_method)
14
+ remove_resolver_method(field_definition)
15
15
  end
16
16
  end
17
17
  end
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphqlMigrateExecution
3
+ # These can be converted with `resolve_static:`. Dataloader was not detected in these resolver methods.
3
4
  class ResolveStatic < Strategy
4
- DESCRIPTION = "These can be converted with `resolve_static:`. Dataloader was not detected in these resolver methods."
5
5
  self.color = :GREEN
6
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")
7
+ def migrate(field_definition)
8
+ inject_resolve_keyword(field_definition, :resolve_static)
9
+ replace_resolver_method(field_definition, "context")
10
10
  end
11
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)
12
+ def cleanup(field_definition)
13
+ remove_field_keyword(field_definition, :resolver_method)
14
+ remove_resolver_method(field_definition)
15
15
  end
16
16
  end
17
17
  end
@@ -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.2"
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.2
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
@@ -91,22 +91,24 @@ files:
91
91
  - bin/graphql_migrate_execution
92
92
  - lib/graphql_migrate_execution.rb
93
93
  - lib/graphql_migrate_execution/action.rb
94
- - lib/graphql_migrate_execution/add_future.rb
95
- - lib/graphql_migrate_execution/analyze.rb
96
94
  - lib/graphql_migrate_execution/dataloader_all.rb
97
95
  - lib/graphql_migrate_execution/dataloader_batch.rb
98
96
  - lib/graphql_migrate_execution/dataloader_manual.rb
99
97
  - lib/graphql_migrate_execution/dataloader_shorthand.rb
100
98
  - lib/graphql_migrate_execution/do_nothing.rb
101
99
  - lib/graphql_migrate_execution/field_definition.rb
100
+ - lib/graphql_migrate_execution/hash_key.rb
102
101
  - lib/graphql_migrate_execution/implicit.rb
102
+ - lib/graphql_migrate_execution/migration.rb
103
103
  - lib/graphql_migrate_execution/not_implemented.rb
104
- - lib/graphql_migrate_execution/remove_legacy.rb
104
+ - lib/graphql_migrate_execution/resolve_batch.rb
105
105
  - lib/graphql_migrate_execution/resolve_each.rb
106
106
  - lib/graphql_migrate_execution/resolve_static.rb
107
107
  - lib/graphql_migrate_execution/resolver_method.rb
108
108
  - lib/graphql_migrate_execution/strategy.rb
109
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
110
112
  - lib/graphql_migrate_execution/version.rb
111
113
  - lib/graphql_migrate_execution/visitor.rb
112
114
  - readme.md
@@ -1,9 +0,0 @@
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
@@ -1,30 +0,0 @@
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
@@ -1,9 +0,0 @@
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