graphql_migrate_execution 0.0.1 → 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 +38 -0
- 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 +13 -9
- 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
|
@@ -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
|
|
@@ -82,29 +82,33 @@ dependencies:
|
|
|
82
82
|
description: A development script for migrating to GraphQL-Ruby's new runtime engine
|
|
83
83
|
email:
|
|
84
84
|
- rdmosolgo@gmail.com
|
|
85
|
-
executables:
|
|
85
|
+
executables:
|
|
86
|
+
- graphql_migrate_execution
|
|
86
87
|
extensions: []
|
|
87
88
|
extra_rdoc_files: []
|
|
88
89
|
files:
|
|
89
90
|
- MIT-LICENSE
|
|
91
|
+
- bin/graphql_migrate_execution
|
|
90
92
|
- lib/graphql_migrate_execution.rb
|
|
91
93
|
- lib/graphql_migrate_execution/action.rb
|
|
92
|
-
- lib/graphql_migrate_execution/add_future.rb
|
|
93
|
-
- lib/graphql_migrate_execution/analyze.rb
|
|
94
94
|
- lib/graphql_migrate_execution/dataloader_all.rb
|
|
95
95
|
- lib/graphql_migrate_execution/dataloader_batch.rb
|
|
96
96
|
- lib/graphql_migrate_execution/dataloader_manual.rb
|
|
97
97
|
- lib/graphql_migrate_execution/dataloader_shorthand.rb
|
|
98
98
|
- lib/graphql_migrate_execution/do_nothing.rb
|
|
99
99
|
- lib/graphql_migrate_execution/field_definition.rb
|
|
100
|
+
- lib/graphql_migrate_execution/hash_key.rb
|
|
100
101
|
- lib/graphql_migrate_execution/implicit.rb
|
|
102
|
+
- lib/graphql_migrate_execution/migration.rb
|
|
101
103
|
- lib/graphql_migrate_execution/not_implemented.rb
|
|
102
|
-
- lib/graphql_migrate_execution/
|
|
104
|
+
- lib/graphql_migrate_execution/resolve_batch.rb
|
|
103
105
|
- lib/graphql_migrate_execution/resolve_each.rb
|
|
104
106
|
- lib/graphql_migrate_execution/resolve_static.rb
|
|
105
107
|
- lib/graphql_migrate_execution/resolver_method.rb
|
|
106
108
|
- lib/graphql_migrate_execution/strategy.rb
|
|
107
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
|
|
108
112
|
- lib/graphql_migrate_execution/version.rb
|
|
109
113
|
- lib/graphql_migrate_execution/visitor.rb
|
|
110
114
|
- readme.md
|
|
@@ -113,9 +117,9 @@ licenses:
|
|
|
113
117
|
- MIT
|
|
114
118
|
metadata:
|
|
115
119
|
homepage_uri: https://graphql-ruby.org
|
|
116
|
-
changelog_uri: https://github.com/rmosolgo/
|
|
117
|
-
source_code_uri: https://github.com/rmosolgo/
|
|
118
|
-
bug_tracker_uri: https://github.com/rmosolgo/
|
|
120
|
+
changelog_uri: https://github.com/rmosolgo/graphql_migrate_execution
|
|
121
|
+
source_code_uri: https://github.com/rmosolgo/graphql_migrate_execution
|
|
122
|
+
bug_tracker_uri: https://github.com/rmosolgo/graphql_migrate_execution/issues
|
|
119
123
|
mailing_list_uri: https://buttondown.email/graphql-ruby
|
|
120
124
|
rubygems_mfa_required: 'true'
|
|
121
125
|
rdoc_options: []
|