Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions bin/graphql-migrate-execution
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "graphql/migrate_execution"
require "optparse"

doctor_options = {}
parser = OptionParser.new
parser.on("--skip-description", "Don't print migration strategy descriptions")
parser.parse!(into: doctor_options)
filename = ARGV.shift || begin
warn "graphql-migrate-execution requires a filename or path as a first argument, please pass one."
exit 1
end

doctor = GraphQL::MigrateExecution.new(filename, **doctor_options)
doctor.run
43 changes: 43 additions & 0 deletions lib/graphql/migrate_execution.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true
require "prism"
require "graphql/migrate_execution/action"
require "graphql/migrate_execution/add_future"
require "graphql/migrate_execution/remove_legacy"
require "graphql/migrate_execution/analyze"

require "graphql/migrate_execution/field_definition"
require "graphql/migrate_execution/resolver_method"
require "graphql/migrate_execution/type_definition"
require "graphql/migrate_execution/visitor"

require "graphql/migrate_execution/strategy"
require "graphql/migrate_execution/implicit"
require "graphql/migrate_execution/do_nothing"
require "graphql/migrate_execution/resolve_each"
require "graphql/migrate_execution/resolve_static"
require "graphql/migrate_execution/not_implemented"
require "graphql/migrate_execution/dataloader_all"
require "graphql/migrate_execution/dataloader_association"
require "graphql/migrate_execution/dataloader_batch"
require "graphql/migrate_execution/dataloader_manual"

require "graphql/migrate_execution/not_implemented"

module GraphQL
class MigrateExecution
def initialize(glob, skip_description: false)
@glob = glob
@skip_description = skip_description
end

attr_reader :skip_description

def run
Dir.glob(@glob).each do |filepath|
source = File.read(filepath)
file_migrate = Analyze.new(self, filepath, source)
puts file_migrate.run
end
end
end
end
44 changes: 44 additions & 0 deletions lib/graphql/migrate_execution/action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class Action
def initialize(migration, path, source)
@migration = migration
@path = path
@source = source
@type_definitions = Hash.new { |h, k| h[k] = TypeDefinition.new(k) }
@field_definitions_by_strategy = Hash.new { |h, k| h[k] = [] }
@total_field_definitions = 0
end

attr_reader :type_definitions

def run
parse_result = Prism.parse(@source, filepath: @path)
visitor = Visitor.new(@source, @type_definitions)
visitor.visit(parse_result.value)
@type_definitions.each do |name, type_defn|
type_defn.field_definitions.each do |f_name, f_defn|
@total_field_definitions += 1
f_defn.check_for_resolver_method
@field_definitions_by_strategy[f_defn.migration_strategy] << f_defn
end
end
nil
end

private

def call_method_on_strategy(method_name)
new_source = @source.dup
@field_definitions_by_strategy.each do |strategy_class, field_definitions|
strategy = strategy_class.new
field_definitions.each do |field_defn|
strategy.public_send(method_name, field_defn, new_source)
end
end
new_source
end
end
end
end
11 changes: 11 additions & 0 deletions lib/graphql/migrate_execution/add_future.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class AddFuture < Action
def run
super
call_method_on_strategy(:add_future)
end
end
end
end
25 changes: 25 additions & 0 deletions lib/graphql/migrate_execution/analyze.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class Analyze < Action
def run
super
message = "Found #{@total_field_definitions} field definitions:".dup

@field_definitions_by_strategy.each do |strategy_class, definitions|
message << "\n\n#{strategy_class.name.split("::").last} (#{definitions.size}):"
if !@migration.skip_description
message << "\n#{strategy_class::DESCRIPTION.split("\n").map { |l| l.length > 0 ? " #{l}" : l }.join("\n")}\n"
end
max_path = definitions.map { |f| f.path.size }.max + 2
definitions.each do |field_defn|
name = field_defn.path.ljust(max_path)
message << "\n - #{name} (#{field_defn.resolve_mode.inspect} -> #{field_defn.resolve_mode_key.inspect}) @ #{@path}:#{field_defn.source_line}"
end
end

message
end
end
end
end
61 changes: 61 additions & 0 deletions lib/graphql/migrate_execution/dataloader_all.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true
# rubocop:disable Development/ContextIsPassedCop
module GraphQL
class MigrateExecution
class DataloaderAll < Strategy
DESCRIPTION = <<~DESC
These fields can use a `dataload:` option.
DESC

def add_future(field_definition, new_source)
inject_field_keyword(new_source, field_definition, :resolve_batch)
def_node = field_definition.resolver_method.node
call_node = def_node.body.body.first
case call_node.name
when :request, :load
load_arg_node = call_node.arguments.arguments.first
with_node = call_node.receiver
source_class_node, *source_args_nodes = with_node.arguments
when :dataload
source_class_node, *source_args_nodes, load_arg_node = call_node.arguments.arguments
else
raise ArgumentError, "Unexpected DataloadAll method name: #{def_node.name.inspect}"
end

old_load_arg_s = load_arg_node.slice
new_load_arg_s = case old_load_arg_s
when "object"
"objects"
when /object((\.|\[)[:a-zA-Z0-9_\.\"\'\[\]]+)/
call_chain = $1
if /^\.[a-z0-9_A-Z]+$/.match?(call_chain)
"objects.map(&:#{call_chain[1..-1]})"
else
"objects.map { |obj| obj#{call_chain} }"
end
else
raise ArgumentError, "Failed to transform Dataloader argument: #{old_load_arg_s.inspect}"
end
new_args = [
source_class_node.slice,
*source_args_nodes.map(&:slice),
new_load_arg_s
].join(", ")

old_method_source = def_node.slice_lines
new_method_source = old_method_source.sub(/def ([a-z_A-Z0-9]+)(\(|$| )/) do
is_adding_args = $2.size == 0
"def self.#{$1}#{is_adding_args ? "(" : $2}objects, context#{is_adding_args ? ")" : ", "}"
end
new_method_source.sub!(call_node.slice, "context.dataload_all(#{new_args})")

combined_new_source = new_method_source + "\n" + old_method_source
new_source.sub!(old_method_source, combined_new_source)
end

def remove_legacy(field_definition, new_source)
remove_resolver_method(new_source, field_definition)
end
end
end
end
10 changes: 10 additions & 0 deletions lib/graphql/migrate_execution/dataloader_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class DataloaderAssociation < Strategy
DESCRIPTION = <<~DESC
These fields can use a `dataload_association:` option.
DESC
end
end
end
10 changes: 10 additions & 0 deletions lib/graphql/migrate_execution/dataloader_batch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class DataloaderBatch < Strategy
DESCRIPTION = <<~DESC
These fields can be rewritten to dataload in a `resolve_batch:` method.
DESC
end
end
end
11 changes: 11 additions & 0 deletions lib/graphql/migrate_execution/dataloader_manual.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class DataloaderManual < Strategy
DESCRIPTION = <<~DESC
These fields use Dataloader in a way that can't be automatically migrated. You'll have to migrate them manually.
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.
DESC
end
end
end
8 changes: 8 additions & 0 deletions lib/graphql/migrate_execution/do_nothing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class DoNothing < Strategy
DESCRIPTION = "These field definitions are already future-compatible. No migration is required."
end
end
end
101 changes: 101 additions & 0 deletions lib/graphql/migrate_execution/field_definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class FieldDefinition
def initialize(type_definition, name, node)
@type_definition = type_definition
@name = name.to_sym
@node = node

@resolve_mode = nil
@hash_key = nil
@resolver = nil
@type_instance_method = nil
@object_direct_method = nil
@dig = nil
@already_migrated = nil

@resolver_method = nil
@unknown_options = []
end

def migration_strategy
case resolve_mode
when nil, :implicit_resolve
Implicit
when :hash_key, :object_direct_method, :dig
DoNothing
when :already_migrated
case @already_migrated.keys.first
when :resolve_each
ResolveEach
when :resolve_static
ResolveStatic
when :resolve_batch
NotImplemented
else
raise ArgumentError, "Unexpected already_migrated: #{@already_migrated.inspect}"
end
when :type_instance_method
resolver_method.migration_strategy
when :resolver
NotImplemented
else
raise "No migration strategy for resolve_mode #{@resolve_mode.inspect}"
end
end

attr_reader :name, :node, :unknown_options, :type_definition, :resolve_mode

def source
node.location.slice
end

def future_resolve_shorthand
method_name = resolver_method.name
name == method_name ? true : method_name
end

attr_writer :resolve_mode

attr_accessor :hash_key, :object_direct_method, :type_instance_method, :resolver, :dig, :already_migrated

def path
@path ||= "#{type_definition.name}.#{@name}"
end

def source_line
@node.location.start_line
end

def resolver_method
case @resolver_method
when nil
method_name = @type_instance_method || @name
@resolver_method = @type_definition.resolver_methods[method_name] || :NOT_FOUND
resolver_method
when :NOT_FOUND
nil
else
@resolver_method
end
end

def implicit_resolve
@name
end

def resolve_mode_key
resolve_mode && public_send(resolve_mode)
end

def check_for_resolver_method
if resolve_mode.nil? && (resolver_method)
@resolve_mode = :type_instance_method
@type_instance_method = @name
end
nil
end
end
end
end
13 changes: 13 additions & 0 deletions lib/graphql/migrate_execution/implicit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class Implicit < Strategy
DESCRIPTION = <<~DESC
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:

- `--preserve-implicit`: Don't add any new configuration; use GraphQL-Ruby's future direct method send behavior (ie `object.public_send(field_name, **arguments)`)
- `--shim-implicit`: Add a method to preserve GraphQL-Ruby's previous dynamic implicit behavior (ie, checking for `respond_to?` and `key?`)
DESC
end
end
end
8 changes: 8 additions & 0 deletions lib/graphql/migrate_execution/not_implemented.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class NotImplemented < Strategy
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."
end
end
end
11 changes: 11 additions & 0 deletions lib/graphql/migrate_execution/remove_legacy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module GraphQL
class MigrateExecution
class RemoveLegacy < Action
def run
super
call_method_on_strategy(:remove_legacy)
end
end
end
end
Loading
Loading