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.
- checksums.yaml +4 -4
- data/bin/graphql_migrate_execution +5 -6
- data/lib/graphql_migrate_execution/action.rb +33 -24
- data/lib/graphql_migrate_execution/dataloader_all.rb +39 -14
- data/lib/graphql_migrate_execution/dataloader_batch.rb +66 -4
- data/lib/graphql_migrate_execution/dataloader_manual.rb +2 -5
- data/lib/graphql_migrate_execution/dataloader_shorthand.rb +20 -7
- data/lib/graphql_migrate_execution/do_nothing.rb +1 -1
- data/lib/graphql_migrate_execution/fallback_value.rb +39 -0
- data/lib/graphql_migrate_execution/field_definition.rb +32 -4
- data/lib/graphql_migrate_execution/hash_key.rb +12 -0
- data/lib/graphql_migrate_execution/implicit.rb +22 -6
- data/lib/graphql_migrate_execution/migration.rb +45 -0
- data/lib/graphql_migrate_execution/not_implemented.rb +1 -1
- data/lib/graphql_migrate_execution/resolve_batch.rb +16 -0
- data/lib/graphql_migrate_execution/resolve_each.rb +7 -7
- data/lib/graphql_migrate_execution/resolve_static.rb +8 -7
- data/lib/graphql_migrate_execution/resolver_method.rb +80 -5
- data/lib/graphql_migrate_execution/strategy.rb +111 -25
- data/lib/graphql_migrate_execution/type_definition.rb +14 -1
- data/lib/graphql_migrate_execution/unsupported_current_path.rb +8 -0
- data/lib/graphql_migrate_execution/unsupported_extra.rb +8 -0
- data/lib/graphql_migrate_execution/version.rb +1 -1
- data/lib/graphql_migrate_execution/visitor.rb +54 -18
- data/lib/graphql_migrate_execution.rb +7 -30
- data/readme.md +158 -4
- metadata +8 -5
- data/lib/graphql_migrate_execution/add_future.rb +0 -9
- data/lib/graphql_migrate_execution/analyze.rb +0 -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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c879a6e266b7520abe76bf69a63cbc7ff8a446fe6438ad0ca37c89368f4c153
|
|
4
|
+
data.tar.gz: cb33f65e293a55a01d3d2e440eb23f40e2f5c3fc28f1241720f40cacf76ca5d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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("--
|
|
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
|
-
|
|
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
|
-
@
|
|
7
|
-
@
|
|
8
|
-
@
|
|
9
|
-
@
|
|
10
|
-
@
|
|
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 :
|
|
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(@
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
total_field_definitions += 1
|
|
22
35
|
f_defn.check_for_resolver_method
|
|
23
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
@field_definitions_by_strategy.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
inject_resolve_keyword(
|
|
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
|
|
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
|
|
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
|
|
51
|
+
"objects.#{map_method}(&:#{call_chain[1..-1]})"
|
|
32
52
|
else
|
|
33
|
-
"objects
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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(
|
|
31
|
+
inject_field_keyword(field_definition, :dataload, dataload_config)
|
|
19
32
|
end
|
|
20
33
|
|
|
21
|
-
def
|
|
22
|
-
remove_resolver_method(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8
|
-
inject_resolve_keyword(
|
|
9
|
-
replace_resolver_method(
|
|
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
|
|
13
|
-
remove_field_keyword(
|
|
14
|
-
remove_resolver_method(
|
|
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
|
|
8
|
-
inject_resolve_keyword(
|
|
9
|
-
replace_resolver_method(
|
|
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
|
|
13
|
-
remove_field_keyword(
|
|
14
|
-
|
|
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
|