GraphqlMigrateExecution
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:
-
DataloaderShorthand: use the newdataload: ...field configuration shorthand -
DataloaderAll: use adataload_all(...)call to fetch data for a batch of objects -
DataloaderBatch: Fetch a list of results for each object (2-layer list) -
DataloaderManual: 💔 Identifies dataloader usage which can’t be migrated -
Migrate method:
-
These identify Ruby code in the method which only uses
contextandobjectand 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. -
💔 Not migratable:
-
NotImplemented: This field couldn’t be matched to a refactor -
UnsupportedCurrentPath: usescontext[:current_path]which isn’t supported anymore -UnsupportedExtra: as at least oneextras: ...configuration which isn’t supported anymore -
Configuration:
-
DoNothing: Already includes future-compatible configuration -
HashKey: Can be migrated usinghash_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:
“‘ruby field :is_locked_to_viewer, Boolean, null: false
def is_locked_to_viewer status = dataload(Sources::GrowthTaskStatusForUserSource, context, 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:
“‘ruby 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, 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:
“‘ruby 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).request(object.id) end end “‘
It could have been auto-migrated if I made two refactors:
-
Update the Source to receive
objectinstead ofobject.id -
Update the Source’s fetch to return
nilbased onobject.frequency.present?
But I didn’t do that. Instead, I migrated it manually:
“‘ruby 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).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:
“‘ruby field :current_user, Types::UserType
def current_user context 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:
“‘ruby 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:
ruby 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
-
[ ] Does
--cleanupwork on my app? I haven’t run it yet.