graphql_migrate_execution 0.0.2 → 1.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.
Files changed (30) 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 +39 -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/fallback_value.rb +39 -0
  10. data/lib/graphql_migrate_execution/field_definition.rb +32 -4
  11. data/lib/graphql_migrate_execution/hash_key.rb +12 -0
  12. data/lib/graphql_migrate_execution/implicit.rb +22 -6
  13. data/lib/graphql_migrate_execution/migration.rb +45 -0
  14. data/lib/graphql_migrate_execution/not_implemented.rb +1 -1
  15. data/lib/graphql_migrate_execution/resolve_batch.rb +16 -0
  16. data/lib/graphql_migrate_execution/resolve_each.rb +7 -7
  17. data/lib/graphql_migrate_execution/resolve_static.rb +8 -7
  18. data/lib/graphql_migrate_execution/resolver_method.rb +80 -5
  19. data/lib/graphql_migrate_execution/strategy.rb +111 -25
  20. data/lib/graphql_migrate_execution/type_definition.rb +14 -1
  21. data/lib/graphql_migrate_execution/unsupported_current_path.rb +8 -0
  22. data/lib/graphql_migrate_execution/unsupported_extra.rb +8 -0
  23. data/lib/graphql_migrate_execution/version.rb +1 -1
  24. data/lib/graphql_migrate_execution/visitor.rb +54 -18
  25. data/lib/graphql_migrate_execution.rb +7 -30
  26. data/readme.md +158 -4
  27. metadata +8 -5
  28. data/lib/graphql_migrate_execution/add_future.rb +0 -9
  29. data/lib/graphql_migrate_execution/analyze.rb +0 -30
  30. 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: 9c879a6e266b7520abe76bf69a63cbc7ff8a446fe6438ad0ca37c89368f4c153
4
+ data.tar.gz: cb33f65e293a55a01d3d2e440eb23f40e2f5c3fc28f1241720f40cacf76ca5d4
5
5
  SHA512:
6
- metadata.gz: 24a586f44159ceac3d674171cbc25d31c342c7238f6d025954f833e6000231a61893c04bdac741df32e5e90c9f30dd22976069eca8c7af66a8c0b1571fc4c6f9
7
- data.tar.gz: fc25e0c9baf95aed3104802604bbd8aba57cd528dd136c912f5f459c973f08f1b510623b13b9a7abcae272ce98c834572ca64b4391841b184c6810e19d99ecc6
6
+ metadata.gz: 99bdbfe4e36d7dce2a78ef1254d4bde1039d3c5189aa96df6c7dab18023ae1f3d58293e1dc98ae0f639f14ced08ebe4f930565d344ff2821fa765ae122c00d8f
7
+ data.tar.gz: 9e29f27f38e4603a2076071d91dd304c81239e39c59080dfdc533aa9a1f0dc11175bd354a4c924e13b9abd60ba18ba2a7ab79f5270ae6107015ee949cb1ec5ee
@@ -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,10 +48,19 @@ 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
55
+ when /([A-Z][a-zA-Z_0-9]*(\.|\[)[:a-zA-Z0-9_\.\"\'\[\]]+)/
56
+ # Constant call
57
+ "objects.#{map_method} { |_obj| #{$1} }"
58
+ when /^("[a-zA-Z0-9 :_\.]+"|:[a-zA-Z0-9_]+)$/
59
+ # Constant literal
60
+ "Array.new(objects.size, #{$1})"
61
+ when /^([a-z_0-9.]+)$/
62
+ # Local variable or receiverless method, possibly followed by a method chain
63
+ "Array.new(objects.size, #{$1})"
35
64
  else
36
65
  raise ArgumentError, "Failed to transform Dataloader argument: #{old_load_arg_s.inspect}"
37
66
  end
@@ -49,11 +78,7 @@ module GraphqlMigrateExecution
49
78
  new_method_source.sub!(call_node.slice, "context.dataload_all(#{new_args})")
50
79
 
51
80
  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)
81
+ @result_source.sub!(old_method_source, combined_new_source)
57
82
  end
58
83
  end
59
84
  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
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ module GraphqlMigrateExecution
3
+ # Turns `fallback_value: ...` into `resolve_static: true`
4
+ class FallbackValue < Strategy
5
+ self.color = :GREEN
6
+
7
+ def migrate(field_definition)
8
+ indent = field_definition.node.location.slice_lines[/^ +/]
9
+ method_name = self.class.prefix_if_necessary(field_definition.name)
10
+ is_interface = field_definition.type_definition.is_interface
11
+ new_body = "\n".dup
12
+
13
+ if is_interface
14
+ method_prefix = "def #{method_name}"
15
+ new_body << "#{indent}resolver_methods do\n"
16
+ method_indent = indent + " "
17
+ else
18
+ method_prefix = "def self.#{method_name}"
19
+ method_indent = indent
20
+ end
21
+
22
+ new_body << "#{method_indent}#{method_prefix}(_context)\n"
23
+ new_body << method_indent + " #{field_definition.fallback_value}\n"
24
+ new_body << method_indent + "end"
25
+
26
+ if is_interface
27
+ new_body << "\n#{indent}end"
28
+ end
29
+
30
+ @result_source.sub!(field_definition.source, field_definition.source + "\n" + new_body)
31
+ keyword_v = method_name == field_definition.name.to_s ? true : method_name.to_sym.inspect
32
+ inject_field_keyword(field_definition, :resolve_static, keyword_v)
33
+ end
34
+
35
+ def cleanup(field_definition)
36
+ remove_field_keyword(field_definition, :fallback_value)
37
+ end
38
+ end
39
+ end
@@ -12,6 +12,7 @@ module GraphqlMigrateExecution
12
12
  @type_instance_method = nil
13
13
  @object_direct_method = nil
14
14
  @dig = nil
15
+ @fallback_value = nil
15
16
  @already_migrated = nil
16
17
 
17
18
  @resolver_method = nil
@@ -19,9 +20,23 @@ module GraphqlMigrateExecution
19
20
  end
20
21
 
21
22
  def migration_strategy
23
+ if unsupported_extras?
24
+ return UnsupportedExtra
25
+ elsif resolver_method&.uses_current_path
26
+ return UnsupportedCurrentPath
27
+ end
28
+
29
+ if @type_definition.is_resolver && @type_definition.returns_hash?
30
+ return HashKey
31
+ end
32
+
22
33
  case resolve_mode
23
34
  when nil, :implicit_resolve
24
- Implicit
35
+ if @type_definition.migration.implicit == "ignore"
36
+ DoNothing
37
+ else
38
+ Implicit
39
+ end
25
40
  when :hash_key, :object_direct_method, :dig
26
41
  DoNothing
27
42
  when :already_migrated
@@ -31,7 +46,9 @@ module GraphqlMigrateExecution
31
46
  when :resolve_static
32
47
  ResolveStatic
33
48
  when :resolve_batch
34
- NotImplemented
49
+ ResolveBatch
50
+ when :resolve_legacy_instance_method
51
+ DoNothing
35
52
  else
36
53
  raise ArgumentError, "Unexpected already_migrated: #{@already_migrated.inspect}"
37
54
  end
@@ -39,6 +56,8 @@ module GraphqlMigrateExecution
39
56
  resolver_method.migration_strategy
40
57
  when :resolver
41
58
  DoNothing
59
+ when :fallback_value
60
+ FallbackValue
42
61
  else
43
62
  raise "No migration strategy for resolve_mode #{@resolve_mode.inspect}"
44
63
  end
@@ -51,13 +70,13 @@ module GraphqlMigrateExecution
51
70
  end
52
71
 
53
72
  def future_resolve_shorthand
54
- method_name = resolver_method.name
73
+ method_name = Strategy.prefix_if_necessary(resolver_method.name).to_sym
55
74
  name == method_name ? true : method_name
56
75
  end
57
76
 
58
77
  attr_writer :resolve_mode
59
78
 
60
- attr_accessor :hash_key, :object_direct_method, :type_instance_method, :resolver, :dig, :already_migrated
79
+ attr_accessor :hash_key, :object_direct_method, :type_instance_method, :resolver, :dig, :already_migrated, :fallback_value
61
80
 
62
81
  def path
63
82
  @path ||= "#{type_definition.name}.#{@name}"
@@ -95,5 +114,14 @@ module GraphqlMigrateExecution
95
114
  end
96
115
  nil
97
116
  end
117
+
118
+ private
119
+
120
+ def unsupported_extras?
121
+ (kwargs = @node.arguments.arguments.last).is_a?(Prism::KeywordHashNode) &&
122
+ (extras_el = kwargs.elements.find { |el| el.key.is_a?(Prism::SymbolNode) && el.key.unescaped == "extras" }) &&
123
+ ((extras_val = extras_el.value).is_a?(Prism::ArrayNode)) &&
124
+ (extras_val.elements.any? { |el| (!el.is_a?(Prism::SymbolNode)) || (el.unescaped != "ast_node" && el.unescaped != "lookahead")})
125
+ end
98
126
  end
99
127
  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,45 @@
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
+ files = Dir.glob(@glob)
29
+ if files.size.zero?
30
+ warn "No files found for #{@glob.inspect}"
31
+ end
32
+ files.each do |filepath|
33
+ source = File.read(filepath)
34
+ action = Action.new(self, filepath, source)
35
+ action.run
36
+
37
+ if !@dry_run
38
+ File.write(filepath, action.result_source)
39
+ end
40
+
41
+ puts action.message
42
+ end
43
+ end
44
+ end
45
+ 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,18 @@
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_field_keyword(field_definition, :fallback_value)
15
+ remove_resolver_method(field_definition)
15
16
  end
16
17
  end
17
18
  end