The project is in a healthy, maintained state
A development script for migrating to GraphQL-Ruby's new runtime engine
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 0
 Project Readme

GraphqlMigrateExecution

Test Gem Version

A command-line development tool to update your Ruby source code to support GraphQL::Execution::Next, then clean up unused legacy configs after you don't need them anymore.

Install

bundle add graphql_migrate_execution

Use

Usage: graphql_migrate_execution glob [options]

A development tool for adopting GraphQL-Ruby's new runtime module, GraphQL::Execution::Next

Inspect the files matched by `glob` and ...

- (default) print an analysis result for what can be updated
- `--migrate`: update files with new configuration
- `--cleanup`: remove legacy configuration and instance methods

Options:

        --migrate                    Update the files with future-compatible configuration
        --cleanup                    Remove resolver instance methods for GraphQL-Ruby's old runtime
        --dry-run                    Don't actually modify files
        --implicit [MODE]            Handle implicit field resolution this way (ignore / hash_key / hash_key_string)

Supported Field Resolution Patterns

Check out the docs for refactors implemented by this tool:

  • Dataloader-based fields:
  • Migrate method:
    • 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.
    • ResolveBatch
    • ResolveEach
    • ResolveStatic
  • 💔 Not migratable:
  • Configuration:
    • DoNothing: Already includes future-compatible configuration
    • HashKey: Can be migrated using hash_key: (especially useful for Resolvers and Mutations)
    • Implicit: ⚠️ GraphQL-Ruby's default field resolution is changing, see the doc

Unsupported Field Resolution Patterns

Here are a few fields in my app that this tool didn't handle automatically, along with my manual migrations:

  • Working with a dataloaded value:

    This resolver called arbitrary code after using Dataloader:

    field :is_locked_to_viewer, Boolean, null: false
    
    def is_locked_to_viewer
      status = dataload(Sources::GrowthTaskStatusForUserSource, context[:current_user], object)
      status == :LOCKED
    end

    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:

    field :is_locked_to_viewer, Boolean, null: false, resolve_batch: true
    
    def self.is_locked_to_viewer(objects, context)
      statuses = context.dataload_all(Sources::GrowthTaskStatusForUserSource, context[:current_user], objects)
      statuses.map { |s| s == :LOCKED }
    end
    
    def is_locked_to_viewer
      self.class.is_locked_to_viewer([ object ], context).first
    end
  • Conditional dataloader call:

    This field only called dataloader in some cases:

    field :viewer_growth_task_submission, GrowthTaskSubmissionType
    
    def viewer_growth_task_submission
      if object.frequency.present?
        # TODO should not include a recurring submission whose duration has passed
        nil
      else
        context.dataloader.with(Sources::GrowthTaskForViewerSource, context[:current_user]).request(object.id)
      end
    end

    It could have been auto-migrated if I made two refactors:

    • Update the Source to receive object instead of object.id
    • Update the Source's #fetch to return nil based on object.frequency.present?

    But I didn't do that. Instead, I migrated it manually:

    field :viewer_growth_task_submission, GrowthTaskSubmissionType, resolve_batch: true
    
    def self.viewer_growth_task_submission(objects, context)
      requests = objects.map do |object|
        if object.frequency.present?
          # TODO should not include a recurring submission whose duration has passed
          nil
        else
          context.dataloader.with(Sources::GrowthTaskForViewerSource, context[:current_user]).request(object.id)
        end
      end
      requests.map { |l| l&.load }
    end
    
    def viewer_growth_task_submission
      self.class.viewer_growth_task_submission([ object ], context).first
    end
  • Resolver that calls another resolver:

    The tool just gives up when it sees calls on self. It didn't handle this:

    field :current_user, Types::UserType
    
    def current_user
      context[:current_user]
    end
    
    field :unread_notification_count, Integer, null: false
    
    def unread_notification_count
      # vvvvvvvvv Calls the resolver method above
      current_user ? current_user.notification_events.unread.count : 0
    end

    I migrated it manually:

    field :unread_notification_count, Integer, null: false, resolve_static: true
    
    def self.unread_notification_count(context)
      if (cu = current_user(context))
        cu.notification_events.unread.count
      else
        0
      end
    end
    
    def unread_notification_count
      self.class.unread_notification_count(context)
    end
  • Single-line method definition:

    The tool's heavy-handed Ruby source generation botched this:

    field :growth_levels, Types::GrowthLevelType.connection_type, null: false, resolve_each: true
    def growth_levels; object.growth_levels.by_sequence; end;

    This tool could be improved to properly handle single-line methods -- open an issue if you need this.

Develop

bundle exec rake test # TEST=test/...

TODO

  • @object is not migrated, only object is
  • **kwargs is not correctly migrated into resolve_static methods
  • Doesn't support def ... =-style single-line methods