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.
- 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 +30 -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/field_definition.rb +25 -2
- 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 +41 -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 +7 -7
- data/lib/graphql_migrate_execution/resolver_method.rb +80 -5
- data/lib/graphql_migrate_execution/strategy.rb +63 -15
- data/lib/graphql_migrate_execution/type_definition.rb +13 -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 +34 -12
- data/lib/graphql_migrate_execution.rb +6 -30
- data/readme.md +154 -4
- metadata +7 -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: 2835acca245bcfe0c626420ae2ce5ac96ee916e0a4dbb35a8db5f1c97a786e1b
|
|
4
|
+
data.tar.gz: 4522d268937cf7a4fd925b4cf0a8fb492ec797006cf48e9972ae74847390eb21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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,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
|
|
51
|
+
"objects.#{map_method}(&:#{call_chain[1..-1]})"
|
|
32
52
|
else
|
|
33
|
-
"objects
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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,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
|
|
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
|
-
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
|
|
@@ -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
|
-
|
|
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, :
|
|
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
|
-
|
|
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
|
|
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(
|
|
57
|
+
def inject_resolve_keyword(field_definition, keyword)
|
|
17
58
|
value = field_definition.future_resolve_shorthand.inspect
|
|
18
|
-
inject_field_keyword(
|
|
59
|
+
inject_field_keyword(field_definition, keyword, value)
|
|
19
60
|
end
|
|
20
61
|
|
|
21
|
-
def inject_field_keyword(
|
|
62
|
+
def inject_field_keyword(field_definition, keyword, value)
|
|
22
63
|
field_definition_source = field_definition.source
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
83
|
+
@result_source.sub!(field_definition_source, new_definition_source)
|
|
36
84
|
end
|
|
37
85
|
|
|
38
|
-
def replace_resolver_method(
|
|
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
|
-
|
|
104
|
+
@result_source.sub!(old_method, new_double_definition)
|
|
57
105
|
end
|
|
58
106
|
|
|
59
|
-
def remove_resolver_method(
|
|
107
|
+
def remove_resolver_method(field_definition)
|
|
60
108
|
src_pattern = /(\n*)(#{Regexp.quote(field_definition.resolver_method.source)})(\n*)/
|
|
61
|
-
|
|
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
|
|
@@ -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
|
|
116
|
+
if @is_public
|
|
103
117
|
td = @type_definition_stack.last
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
if node.receiver.nil?
|
|
119
|
+
@current_resolver_method = td.resolver_method(node.name, node)
|
|
120
|
+
end
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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/
|
|
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-
|
|
28
|
+
--migrate Update the files with future-compatible configuration
|
|
29
29
|
--cleanup Remove resolver instance methods for GraphQL-Ruby's old runtime
|
|
30
|
-
--
|
|
31
|
-
--implicit 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
|
|
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-
|
|
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/
|
|
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,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
|