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. ResolveBatchResolveEachResolveStatic
- These identify Ruby code in the method which only uses
- 💔 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:
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
objectinstead ofobject.id - Update the Source's
#fetchto returnnilbased onobject.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
- Update the Source to receive
-
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
-
@objectis not migrated, onlyobjectis -
**kwargsis not correctly migrated intoresolve_staticmethods - Doesn't support
def ... =-style single-line methods