API creation with Rails + GraphQL

Each version

ruby: 2.7.1
rails: 6.0.3.4
graphql-ruby: 1.11.6

GraphQL Ruby

Official page

When dealing with GraphQL in Rails We will implement the API using the gem ↑.

graphiql-rails In addition, if you put graphiql-rails gem, GraphQL implemented on the browser You will be able to use an IDE that allows you to check: sparkles:

Image image graphiql-rails

: computer: Environment construction


Gemfile


gem 'graphql'
gem 'graphiql-rails' #This time I put it first

Once the gem is installed, run the rails generate graphql: install command to generate each file. The generated file is as follows ↓

$ rails generate graphql:install
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/app_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_argument.rb
      create  app/graphql/types/base_field.rb
      create  app/graphql/types/base_enum.rb
      create  app/graphql/types/base_input_object.rb
      create  app/graphql/types/base_interface.rb
      create  app/graphql/types/base_scalar.rb
      create  app/graphql/types/base_union.rb
      create  app/graphql/types/query_type.rb
add_root_type  query
      create  app/graphql/mutations
      create  app/graphql/mutations/.keep
      create  app/graphql/mutations/base_mutation.rb
      create  app/graphql/types/mutation_type.rb
add_root_type  mutation
      create  app/controllers/graphql_controller.rb
       route  post "/graphql", to: "graphql#execute"
     gemfile  graphiql-rails
       route  graphiql-rails

At this point, routes.rb looks like this:

Rails.application.routes.draw do

  # GraphQL
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
  end

  post '/graphql', to: 'graphql#execute'
end

: pencil: Implementation


Query creation

First of all, we have to define the Type corresponding to each table, so As an example, I would like to create a user_type that corresponds to the following users table.

create_table :users do |t|
  t.string :name, null: false
  t.string :email
  t.timestamps
end

Execute the following command to create user_type. (The specified type is the type for id whose ID is defined in GraphQL (actually String) Also, those with ! At the end are nullable types, and those without ! Are nullable. )

$ bundle exec rails g graphql:object User id:ID! name:String! email:String

[Supplement] If the table already exists in the DB, it seems that it will do it for you.

$ bundle exec rails g graphql:object User

↑ This was fine: sparkles:

The generated file graphql/type/user_type.rb looks like this:

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

Add the following to the already generated graphql/type/query_type.rb.

    field :users, [Types::UserType], null: false
    def users
      User.all
    end

If you throw the following query on http: // localhost: 3000/graphiql, you will get a response.

{
  users {
    id
    name
    email
  }
}

Creating Mutations

Next, I would like to create Mutations CreateUser to create users.

$ bundle exec rails g graphql:mutation CreateUser

graphql/mutations/create_user.rb will be created, so modify it as follows.

module Mutations
  class CreateUser < BaseMutation
    field :user, Types::UserType, null: true

    argument :name, String, required: true
    argument :email, String, required: false

    def resolve(**args)
      user = User.create!(args)
      {
        user: user
      }
    end
  end
end

Add the following to the already generated graphql/types/mutation_type.rb.

module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUser #Postscript
  end
end

User is created by executing the following on http: // localhost: 3000/graphiql.

mutation {
  createUser(
    input:{
      name: "user"
      email: "[email protected]"
    }
  ){
    user {
      id
      name 
      email
    }
  }
}

Association

--For a 1: 1 related table

For example, if Post is associated with Label 1: 1.

label_type.rb


module Types
  class LabelType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    ...
  end
end
module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
  end
end

You can define label as LabelType as in ↑. As an image of Query in this case

{
  posts {
    id
    label {
      id
      name
    }
  }
}

As mentioned above, you can query the required value with label as LabelType.

--1: For N related tables

For example, if User is Post and 1: N

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :label, LabelType, null: true
  end
end
module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
  end
end

As mentioned above, posts can be defined as[PostType], and as a Query

{
  user(id: 1234) {
    id
    posts {
      id
      label {
        id
        name
      }
    }
  }
}

You can call it like ↑.

graphql-batch

As explained in ↑, you can also fetch data from related tables of 1: 1 and 1: N. If it is left as it is, a large number of inquiries to the DB may occur. In the example when User is Post and 1: N, if there are 100 Posts, the query will occur 100 times each.

Therefore, I will introduce graphql-batch which is one of the solutions to collect multiple inquiries.

gem 'graphql-batch'

After installing Gem, create a loader. loader is an implementation of the" group multiple queries "part.

graphql/loaders/record_loader.rb


module Loaders
  class RecordLoader < GraphQL::Batch::Loader
    def initialize(model)
      @model = model
    end

    def perform(ids)
      @model.where(id: ids).each { |record| fulfill(record.id, record) }
      ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
    end
  end
end

Applying this when the previous Post is associated with Label 1: 1

module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
    def label
      Loaders::RecordLoader.for(Label).load(object.label_id)
    end
  end
end

You can write like this. If User is Post and 1: N, create a separate loader.

graphql/loaders/association_loader.rb


module Loaders
  class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)
      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      unless @model.reflect_on_association(@association_name)
        raise ArgumentError, "No association #{@association_name} on #{@model}"
      end
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end

If you write as follows, you will be able to make inquiries all at once.

module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
    def posts
      Loaders::AssociationLoader.for(User, :posts).load(object)
    end
  end
end

Document generation from schema file

I would like to automatically generate a nice document from the schema file defined at the end.

Can be mounted on routes.rb and automatically updates graphdoc with each deployment When I was looking for a convenient gem, there was a gem called graphdoc-ruby, so I will try it.

Add the following to Gemfile

gem 'graphdoc-ruby'

You also need the npm package @ 2fd/graphdoc. Install it in the Docker image in advance. (If you are not using Docker, you should install it in your local environment)

Example)

RUN set -ex \
    && wget -qO- https://deb.nodesource.com/setup_10.x | bash - \
    && apt-get update \
    && apt-get install -y \
                 ...
                 --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g yarn \
    && npm install -g @2fd/graphdoc #Install

Add the following to config/routes.rb

config/routes.rb


Rails.application.routes.draw do
  mount GraphdocRuby::Application, at: 'graphdoc'
end

Example)

GraphdocRuby.configure do |config|
  config.endpoint = 'http://0.0.0.0:3000/api/v1/graphql'
end

Restart Rails and it's okay if the documentation is generated at http: // localhost: 3000/graphdoc: sparkles:

graphdoc

: bomb: bad know-how


-- http: // localhost: 3000/graphiql If the following error occurs when accessing

```
Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
```

--Solution 1

Add the following to app/assets/config/manifest.js

    ```
    //= link graphiql/rails/application.css
    //= link graphiql/rails/application.js
    ```
    [AssetNotPrecompiled error with Sprockets 4.0 · Issue #75 · rmosolgo/graphiql-rails](https://github.com/rmosolgo/graphiql-rails/issues/75#issuecomment-546306742)

-> However, with this, I get a Sprockets :: FileNotFound: couldn't find file'graphiql/rails/application.css' error during Production and cannot use it ...

-** Solution 2 (Successful method) **

Lower to version 3.7.2 of gem'sprocket'

    ```ruby
    gem 'sprockets', '~> 3.7.2'  [#1098:  slowdev/knowledge/ios/Add Firebase with Carthage](/posts/1098) 
    ```

Add ↑ and bundle update How to use GraphQL in Rails 6 API mode (including error countermeasures) --Qiita

--The graphiql screen shows TypeError: Cannot read property'types' of undefined -> In my environment, it was cured by restarting Rails

--The graphiql screen displays SyntaxError: Unexpected token <in JSON at position 0 -> An error may have occurred, so look at the log and correct it.

: link: Helpful URL


-[Rails] Create API with graphql-ruby --Qiita --The story of introducing GraphQL in a project where REST API is the mainstream (server side) --Sansan Builders Blog -Thorough introduction to "GraphQL" ─ Comparison with REST, learning from implementation of both API and front --Engineer Hub | Think about the career of a young Web engineer! -Introduction of API specification-centered development using GraphQL and its effect --Kaizen Platform developer blog -GraphQL Ruby [class-based API] --Qiita -hawksnowlog: Getting Started with GraphQL in Ruby (Sinatra) --I added GraphQL API to an existing Rails project --Qiita -How to migrate from sprockets to Webpacker with Ruby on Rails and coexist those that cannot be migrated --Qiita -Reading: GraphQL for the first time-Basics of types | tkhm | note

Recommended Posts

API creation with Rails + GraphQL
How to build API with GraphQL and Rails
Rails API
[Rails 6] API development using GraphQL (Query)
[Rails] Initial data creation with seed
[Rails] Many-to-many creation
[Rails] Book search with Amazon PA API
Mutation with GraphQL
Rails6 [API mode] + MySQL5.7 environment construction with Docker
Compatible with Android 10 (API 29)
CLI creation with thor
[Rails 6] RuntimeError with $ rails s
Handle devise with Rails
Graph creation with JFreeChart
[Rails] Learning with Rails tutorial
[Rails] Test with RSpec
[Rails] Development with MySQL
Supports multilingualization with Rails!
Double polymorphic with Rails
Using PAY.JP API with Rails ~ Implementation Preparation ~ (payjp.js v2)
Build Rails (API) x MySQL x Nuxt.js environment with Docker
Using PAY.JP API with Rails ~ Card Registration ~ (payjp.js v2)
Java multi-project creation with Gradle
Create XML-RPC API with Wicket
Introduced graph function with rails
[Rails] Express polymorphic with graphql-ruby
Implement GraphQL with Spring Boot
Portfolio creation Ruby on Rails
[Rails] Upload videos with Rails (ActiveStorage)
GraphQL Client starting with Ruby
[Vue Rails] "Hello Vue!" Displayed with Vue + Rails
Use Bulk API with RestHighLevelClient
[Rails] Create API to download files with Active Storage [S3]
Preparation for developing with Rails
Run Rails whenever with docker
[Ruby on Rails] Implement login function by add_token_to_users with API
Rails reference type creation added
[Docker] Rails 5.2 environment construction with docker
Use multiple databases with Rails 6.0
Test GraphQL resolver with rspec
[Rails] Specify format with link_to
Login function implementation with rails
REST API testing with REST Assured
Link API with Spring + Vue.js
[Rails] Book search (asynchronous communication) with Amazon PA API v5.0
[Rails] New app creation --Notes--
[Docker] Use whenever with Docker + Rails
Rails 6 (API mode) + MySQL Docker environment creation by docker-compose (for Mac)
I implemented Rails API with TDD by RSpec. part2 -user authentication-
I implemented Rails API with TDD by RSpec. part3-Action implementation with authentication-
Create portfolio with rails + postgres sql
[Rails] Make pagination compatible with Ajax
Automatic API testing with Selenium + REST-Assured
Add & save from rails database creation
REST API test with REST Assured Part 2
Implemented mail sending function with rails
[Rails] Creating a new project with rails new
Minimal Rails with reduced file generation
Create pagination function with Rails Kaminari
Build environment with vue.js + rails + docker
Eliminate Rails FatModel with value object