简体   繁体   中英

GraphQL Ruby: How to write DRY mutations with multiple parameters?

I'm writing a devise-jwt -based authentication system for my graphql-ruby using app. In the process, I've made a mutation for creating a new user account, which takes 7 parameters, which creates quite a lot of repetition in my code:

module Mutations
  class SignUpMutation < Mutations::BaseMutation
    argument :email, String, required: true
    argument :password, String, required: true
    argument :family_name, String, required: true
    argument :family_name_phonetic, String, required: true
    argument :given_name, String, required: true
    argument :given_name_phonetic, String, required: true
    argument :newsletter_optin, Boolean, required: false

    field :token, String, null: true
    field :user, Types::UserType, null: true

    def resolve(email:, password:,
                family_name:, family_name_phonetic:,
                given_name:, given_name_phonetic:,
                newsletter_optin:
               )
      result = {
        token: nil,
        user: nil
      }
      new_user = User.new(
        email: email,
        password: password,
        family_name: family_name,
        family_name_phonetic: family_name_phonetic,
        given_name: given_name,
        given_name_phonetic: given_name_phonetic,
        newsletter_optin: newsletter_optin
      )
      if new_user.save!
        result[:token] = new_user.token
        result[:user] = new_user
      end
      result
    end
  end
end

How could I DRY this up to avoid repeating the names of the mutation arguments all over the place?

Thank you in advance!

Answering my own question. The correct way to not have to deal with so many parameters is to use Input Objects instead of separate parameters. From the graphql-ruby documentation :

Input object types are complex inputs for GraphQL operations. They're great for fields that need a lot of structured input, like mutations or search fields.

So I've defined my Input Object as such:

module Types
  class UserAttributes < Types::BaseInputObject
    description 'Attributes for creating or updating a user'
    argument :email, String, required: true
    argument :password, String, required: true
    argument :family_name, String, required: true
    argument :family_name_phonetic, String, required: true
    argument :given_name, String, required: true
    argument :given_name_phonetic, String, required: true
    argument :newsletter_optin, Boolean, required: false
  end
end

and then refactored my mutation like this:

module Mutations
  class SignUpMutation < Mutations::BaseMutation
    argument :attributes, Types::UserAttributes, required: true

    field :token, String, null: true
    field :user, Types::UserType, null: true

    def resolve(attributes:)
      result = {
        token: nil,
        user: nil
      }
      new_user = User.new(attributes.to_hash)
      if new_user.save!
        result[:token] = new_user.token
        result[:user] = new_user
      end
      result
    end
  end
end

Finally, this code feels more ruby-like :)

If you'd like, you could do something like this:

[
  :email,
  :password,
  :family_name,
  :family_name_phonetic,
  :given_name,
  :given_name_phonetic
].each do |arg|
  argument arg, String, required: true
end

You might think any more than this is overkill, but Ruby is very flexible. If you really wanted to, you could even do something like

def resolve(email:, password:,
            family_name:, family_name_phonetic:,
            given_name:, given_name_phonetic:,
            newsletter_optin:)
  result = {
    token: nil,
    user: nil
  }
  params = method(__method__).parameters.map(&:last)
  opts = params.map{|p| [p, eval(p.to_s)]}.to_h
  new_user = User.new(opts)
  if new_user.save!
    result[:token] = new_user.token
    result[:user] = new_user
  end
  result
end

You can see this answer for an explanation

If you wanted even more than this, you could use a more detailed field list, and define_method - you could get it all the way to the point where you only type eg :email once.

Would that be better? Maybe, if you've got hundreds of these to do. Or if you want to start defining things at runtime.

You may try double splat (**) operator.

module Mutations
  class SignUpMutation < Mutations::BaseMutation
    argument :email, String, required: true
    argument :password, String, required: true
    argument :family_name, String, required: true
    argument :family_name_phonetic, String, required: true
    argument :given_name, String, required: true
    argument :given_name_phonetic, String, required: true
    argument :newsletter_optin, Boolean, required: false

    field :token, String, null: true
    field :user, Types::UserType, null: true

    def resolve(**arguments)
      result = {
        token: nil,
        user: nil
      }

      new_user = User.new(
        email: arguments[:email],
        password: arguments[:password],
        family_name: arguments[:family_name],
        family_name_phonetic: arguments[:family_name_phonetic],
        given_name: arguments[:given_name],
        given_name_phonetic: arguments[:given_name_phonetic],
        newsletter_optin: arguments[:newsletter_optin]
      )

      if new_user.save!
        result[:token] = new_user.token
        result[:user] = new_user
      end

      result
    end
  end
end

Of course, creating a new type like you have done would be neater. But there're cases you can combine them together, like

module Mutations
  class SignUpMutation < Mutations::BaseMutation
    argument :another_attribute, String, required: true
    argument :attributes, Types::UserAttributes, required: true

    field :token, String, null: true
    field :user, Types::UserType, null: true

    def resolve(**arguments)
      result = {
        token: nil,
        user: nil
      }

      # use arguments[:another_attribute] for something else.

      new_user = User.new(arguments[:attributes].to_hash)

      if new_user.save!
        result[:token] = new_user.token
        result[:user] = new_user
      end

      result
    end
  end
end

In your case I would use input objects as well, but what would you do if you had an existing API with clients relying on the schema and you want to "DRY up" those duplicated arguments that are all the same across different mutations/fields?

If you just go ahead and implement a new input object you'll change the schema and the clients will very likely break. I suppose there is no way of keeping the schema identical when moving existing arguments into an input object, right?

A better approach without disturbing the existing GraphQL schema would be to define a InputType with all the common arguments like:

module Types
    module Inputs
        class CommonInputType < Types::Root::BaseInputObject
            graphql_name("my_common_input_type")

            argument :email, String, required: true
            argument :newsletter_optin, Boolean, required: true
            ...
            argument :posts, [Types::Inputs::Post], required: true
        end
    end
end

& use it in some mutation with additional arguments like:

module Mutations
    class CreateUser < Mutations::BaseMutation

        argument :additional_arg_one, ID, required: true
        argument :additional_arg_two, String, required: false
        ...

        Types::Inputs::CommonInputType.arguments.each do |arg,properties|
          argument arg.to_sym, properties.graphql_definition.type
        end

    end
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM