Основы GraphQL

Jan 31, 2019

GraphQL - это язык; язык, который используется для взаимодействия с сервером для получений данных.
GraphQL - это среда (среда выполнения); GraphQL сервер.
Сайт: graphql.org

GraphQL работает следующим образом: вы указываете в запросе те данные, которые необходимо получить. Отличие от REST: в REST по запросу вы получаете весь объем данных, который как правило излишен.

Запрос в GraphQL:

    query {
        posts(id: 1) {
            id
            title
            author {
                firstname,
                lastname
            }
        }
    }

Как работает GraphQL

GraphQL сервер принимает запросы от клиента, взаимодействует с БД и отправляет ответ клиенту. GraphQL запрос это http-запрос, но в отличие от REST у GraphQL всего 1 маршрут, например:

HTTP POST https://mysite.com/graphql
// отправляем данные через get-запрос
fetch('https://mysite.com/graphql?query={posts(id: 1){...}}')
// отправляем данные через post-запрос
fetch('https://mysite.com/graphql', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            query: `
            {
                posts(id: 1) {
                    id
                    title
                    author {
                        firstname
                        lastname
                    }
                }
            }
            `
        })
    })

GraphQL не подменяет сервер: GraphQL имеет некий слой между клиентов и сервером. GraphQL может быть не очень удобен с точки зрения реализации, но с точки зрения клиента он очень удобен.

Термины

Запрос

Запрос - в запросе мы указываем то, что мы хотим получить от сервера.

    query {
        posts(id: 1) {
            id
            title
            author {
                firstname
                lastname
            }
        }
    }

Тип

Тип - типы данных. Например, перед отправкой запроса на сервер, серверу нужно объяснить, что такое post.

ID! - тип данных в GraphQL.

Author! - составные (пользовательские) типы данных; берутся от того, кто реализует сервер. Типы в GraphQL реализуются на стороне сервера.

    type Post {
        id: ID!
        title: String!
        content: String!
        author: Author!
        status: Status!
        comments: [Comment]!
    }
    type Author {
        id: ID!
        firstname: String!
        lastname: String!
    }
    enum Status {
        DRAFT
        PUBLISHED
        ARCHIVED
    }
    type Comment {
        id: ID!
        title: String!
        body: String!
    }

Изменение (Mutation)

Изменение (Mutation) - похожи на запросы, но если запрос получает данные, то изменения изменяют данные.

    mutation {
        createPost(input: {
            title: 'Заголовок'
            content: '...'
            status: DRAFT
            author: {
                firstname: 'John'
                lastname: 'Smith'
                status: DRAFT
            }
        })
    }

Язык GraphQL

Для практики будем использовать API GitHub - developer.github.com/v4/explorer/

GraphQL не работает с одинарными кавычками.

Ошибки в ответе

В отличие от rest в GraphQL не используются ответы с кодами в случае возникновения ошибок (404 etc.). Для описания ошибки используется свойство errors.

{
  "errors": [
    {
      "type": "MISSING_PAGINATION_BOUNDADRIES",
      "path": [
        "viewer",
        "gists"
      ],
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ],
      "message": "You must provide a `first` or `last` value to properly paginate the `gists` connection."
    }
  ]
}

Редактор GraphiQL

Мы будем работать с GraphQL посредством редактора GraphiQL.
GraphiQL 'понимает' какие поля доступны для запроса - нажмите ctrl + space.

Запрос через GraphiQL

В запросе ключевое слово query необязательно, если запрос один.

query {
    viewer {
        login
    }
}

// equal
{
    viewer {
        login
    }
}


// response
{
    "data": {
        "viewer": {
          "login": "userName"
        }
    }
}

Вложенные запросы

Вложенные свойства указываются как вложенные свойства в JSON-объекте.

{
    viewer {
        login
            gists {
                totalCount
            }
    }
}

Вложенный запрос с аргументом и его значением

У свойств могут быть аргументы - они указываются в скобках как пара (ключ: значение).

{
    viewer {
        login
            gists(first: 10) {
                totalCount
                nodes {
                    id
                    name
                }
            }
        }
}
{
    repository(name: "graphql", owner: "facebook") {
        name
    }
}

// response
{
  "data": {
    "repository": {
      "name": "graphql"
    }
  }
}

Алиасы

GraphQL позволяет в одном запросе вызывать несколько 'запросов':

// ЭТО НЕВЕРНО
query {
    repository(name: "graphql", owner: "facebook") {
        name
    }

    repository(name: "react", owner: "facebook") {
        name
    }
}

Но в JSON свойства должны быть уникальны в рамках одного объекта (то есть в данном ожидается, что вернется 2 одинаковых свойства - repository).

Данную проблему решают алиасы. Например, в нашем примере test это свойство, которое придет в ответе при запросе на test: repository(....

query {
    test: repository(name: "graphql", owner: "facebook") {
        name
    }

    repository(name: "react", owner: "facebook") {
        name
    }
}

// response:
{
    "data": {
        "test": {
            "name": "graphql"
        },
        "repository": {
            "name": "react"
        }
    }
}

Фрагменты

Чтобы не дублировать однотипные структуры в запросах (см. пример ниже) в GraphQL есть фрагменты.

query {
  graphql: repository(name: "graphql", owner: "facebook") {
    name
    description
    createdAt
    forks (first: 1) {
      edges {
        node {
          id
        }
      }
    }
  }

  react: repository(name: "react", owner: "facebook") {
    name
    description
    createdAt
    forks (first: 1) {
      edges {
        node {
          id
        }
      }
    }
  }
}

Фрагмент указывается через ключевое слово fragment далее идет название фрагменты, затем идет тип фрагмента (on тип-данных).

query {
  graphql: repository(name: "graphql", owner: "facebook") {
    ...repoDetails
  }

  react: repository(name: "react", owner: "facebook") {
    ...repoDetails
  }
}

fragment repoDetails on Repository {
  name
  description
  createdAt
  forks (first: 1) {
    edges {
      node {
        id
      }
    }
  }
}

Названия запросов

Запросу можно указать название. Для обозначения наименования запроса после ключевого слова query ставится имя запроса. Это удобно для отладки на стороне сервера.

query getRepository {
  graphql: repository(name: "graphql", owner: "facebook") {
    ...repoDetails
  }

  react: repository(name: "react", owner: "facebook") {
    ...repoDetails
  }
}

Переменные

Запросы в GraphQL могут принимать аргументы (по аналогии с обычными функциями javascript), и за это в GraphQL отвечают переменные. Для определения переменных используется знак $.
Через : указывается тип данных переменной.
! указывает, что переменная обязательна.

В редакторе GrapihQL внизу есть окошко (QUERY VARIABLES), где указываются значения для переменных. При этом переменные указываются в JSON-формате.

query getRepository($name: String!, $owner: String!) {
  graphql: repository(name: $name, owner: $owner) {
    name
    description
    createdAt
    forks (first: 1) {
      edges {
        node {
          id
        }
      }
    }
  }
}
{
  "name": "react",
  "owner": "facebook"
}

В post-запросе:

var dice = 3;
var sides = 6;
var query = `query RollDice($dice: Int!, $sides: Int) {
    rollDice(numDice: $dice, numSides: $sides)
}`;

fetch('/graphql', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    },
    body: JSON.stringify({
        query,
        variables: { dice, sides },
    })
})
    .then(r => r.json())
    .then(data => console.log('data returned:', data));

Еще один пример:

    $('button').click(function() {
      event.preventDefault();
      var entry = $('#entry').val();

      $.ajax({
          method: "POST",
          url: "https://api.github.com/graphql",
          contentType: "application/json",
          headers: {
            Authorization: "bearer ***********"
          },
          data: JSON.stringify({
            query: `query ($entry: String!) {repository(name: $entry,
            owner: "*******") { pullRequests(last: 100) {
              nodes {
                state
                headRepository {
                  owner {
                    login
                  }
                }
              }
            }
          }
        }`,
        variables: {
          "entry": entry
        }
      })
    })

get-запрос:

GET /graphql?query=query%20aTest(%24arg1%3A%20String!)%20%7B%20test(who%3A%20%24arg1)%20%7D&operationName=aTest&variables=me

// decode
/graphql?query=query aTest($arg1: String!) { test(who: $arg1) }&operationName=aTest&variables=me

Директивы

Директива используется в GraphQL в двух случаях:

  1. Хотим ли мы возвращать некоторое значение или нет по условию (@include).
  2. Для того чтобы пропустить какое-либо значение (@skip).

Например, объявим переменную includeForks, которая будет отвечать за то, следует ли включать свойство forks в запрос или нет. И применим ее в директиве @include:

query getRepository($name: String!, $owner: String!, $includeForks: Boolean!) {
  graphql: repository(name: $name, owner: $owner) {
    name
    description
    createdAt
    forks(first: 2) @include(if: $includeForks) {
      edges {
        node {
          createdAt
          id
        }
      }
    }
  }
}

Документация схемы

Документация схемы - в редакторе GrapiQL есть вкладка Docs, в которой прописаны типы, которые мы можем использовать.

При клике на Query мы увидим все доступные запросы с описанием всех типов, доступных переменных и т.д. которые используются в запросе.

При этом документация генерируется автоматически.

Сервер GraphQl в node.js

Настройка express сервера

// server.js
const express = require('express');
const server = express();


server.get('/', (req,  res) => {
    res.send('hello world!');
});

server.listen(3001, () => {
    console.log('test server on port 3001');
});

Отдельный модуль для API

Создадим отдельный модуль для работы API и импортируем его:

// api/index.js
const express = require('express');
const api = express();

api.all('/api', (req,  res) => {
    res.send('send api');
});

module.exports = api;
// server.js
server.get('/', (req,  res) => {
    res.send('hello world!');
});
server.use('/', api);
server.listen(3001, () => {
    console.log('test server on port 3001');
});

Библиотека graphql и express-graphql

npm i graphql

Свяжем GraphQL с express сервером при помощи express-graphql:

npm i express-graphql

Импортируем express-graphql в нашем апи модуле, при этом express-graphql будет отвечать за обработку запросво-ответов.

// api/index.js
const express = require('express');
const graphqlMiddleWare = require('express-graphql');

const api = express();

api.all('/api', graphqlMiddleWare({
    graphiql: true
}));

module.exports = api;

Схема

Перед тем как сервер сможет отдавать какие-либо данные клиенту ему нужно указать с какими данными придется работать. За описание данных отвечает схема.

// api/index.js
const express = require('express');
const graphqlMiddleWare = require('express-graphql');
// импортируем схему
const schema = require('./schema.js');


const api = express();

api.all('/api', graphqlMiddleWare({
    schema,
    graphiql: true
}));

module.exports = api;
// api/schema.js
const { buildSchema } = require('graphql');


// в type Query необходимо прописать запросы, которые сможет принимть сервер
// pet - запрос на сервере; String - отдаем, к примеру, тип String
module.exports = buildSchema(`
    type Query {
        pet: String
    }
`);

Далее при запросу на http://localhost:3001/api вы сможете увидеть редактор graphiql.

Резолверы

Нам требуется научить GraphQL отдавать данные при, например, запросе на pet. Для этого используются резолверы - реализация запросов в свойстве rootValue.

// api/index.js
const express = require('express');
const graphqlMiddleWare = require('express-graphql');
// импортируем схему в файле index.js
const schema = require('./schema.js');

const api = express();

api.all('/api', graphqlMiddleWare({
    schema,
    graphiql: true,
    rootValue: {
        // 1 - название запроса; 2 - ф-я (реализация схемы)
        pet: () => "test"
    }
}));

module.exports = api;

Теперь при запросе в graphiQL (если graphiql: true) на:

query {
  pet
}
// request:
{
  "data": {
    "pet": "test"
  }
}
http://localhost:3001/api?query=query%20%7B%0A%20%20pet%0A%7D

Типы в схеме

Для объекта pet нам нужно создать тип. Для это в файле schema.js:

// api/schema.js
module.exports = buildSchema(`
    type Step {
        title: String!,
        completed: Boolean!
    }

    type Pet {
        id: ID!,
        name: String!,
        species: String!,
        favFoods: [String],
        birthYear: Int,
        photo: String,
        steps: [Step]
    }

    type Query {
        pet: Pet!,
        pets: [Pet]
    }
`);
query {
  pets {
    id
    species
    favFoods
    birthYear
    name
    steps {
      title
      completed
    }
  }
}

Реализуем запрос с аргументом/ми

Воспользуемся аргументами, чтобы получить нужного питомца по id:

    // api/schema.js
    type Query {
        pet(id: ID!): Pet!,
    //...
// api/index.js
api.all('/api', graphqlMiddleWare({
    schema,
    graphiql: true,
    rootValue: {
        pet: ({id}) => {
            return pets.find(function (pet) {
                return pet.id == id;
            })
        },
        pets: () => pets
    }
}));
// request
query {
  pet(id: 2) {
    id
    species
    favFoods
    birthYear
    name
    steps {
      title
      completed
    }
  }
}

Изменения (обновление)

Для изменения данных используются изменения или mutation. В schema.js добавим еще один тип - mutation, в нем определим функции, которые будут изменять данные.

Данные (например, пришедшие на изменение) в GraphQL называют input.

    // НЕВЕРНО, так как **
    // api/schema.js
    type Mutation {
        createPet(input: Pet!): Pet
        updatePet(id: ID!, input: Pet!): Pet
        deletePet(id: ID!): ID
    }

Далее необходимо описать вышеприведенные функции как резолверы.

Для входных данных (а они в GraphQL называются словом input) нельзя** использовать типы данных, которые мы определили ранее, например, Pet и которые мы используем в type Query.

В input-ах мы можем указывать значения по умолчанию.

Для каждого входного input нужно определить свой тип данных через ключевой слово input:

module.exports = buildSchema(`
    type Step {
        title: String!,
        completed: Boolean!
    }

    type Pet {
        id: ID!,
        name: String!,
        species: String!,
        favFoods: [String],
        birthYear: Int,
        photo: String,
        steps: [Step]
    }

    type Query {
        pet(id: ID!): Pet!,
        pets: [Pet]
    }

    input StepInput {
        title: String!,
        completed: Boolean = false
    }

    input PetInput {
        name: String!,
        species: String!,
        birthYear: Int,
        steps: [StepInput]
    }

    type Mutation {
        createPet(input: PetInput!): Pet
        updatePet(id: ID!, input: PetInput!): Pet
        deletePet(id: ID!): ID
    }
`);

Реализуем функцию, которая будет срабатывать при запросе на createPet (mutation):

// api/index.js
api.all('/api', graphqlMiddleWare({
    schema,
    graphiql: true,
    rootValue: {
        // 1 - название запроса; 2 - ф-я (реализация схемы)
        pet: ({id}) => {
            return pets.find(function (pet) {
                return pet.id == id;
            })
        },
        pets: () => pets,
        createPet: ({ input }) => {
            let pet = { ...input, id: pets.length + 1 };
            pets.push(pet);
            return pet;
        },

Добавим задачу через интерфейс GraphiQL:

mutation  {
  createPet(input: {
    name: "Bobik",
    species: "dog",
    birthYear:  2019
  }) {
    id
    name
  }
}

// response
{
  "data": {
    "createPet": {
      "id": "6",
      "name": "Bobik"
    }
  }
}

Код с реализацией удаления, редактирования и добавления питомцев:

// api/index.js
const express = require('express');
const graphqlMiddleWare = require('express-graphql');
// импортируем схему в файле index.js
const schema = require('./schema.js');

let pets = require('./data').pets;
const api = express();

// класс Pet будет отвечать за корректную инициализацию экземпляра Pet
class Pet {
    constructor({ name, species = 'dog', steps = [], birthYear = 2019 } = {}) {
        this.name = name;
        this.species = species;
        this.birthYear = birthYear;
        this.steps = steps;
        this.id = pets.length + 1;
    }
}

api.all('/api', graphqlMiddleWare({
    schema,
    graphiql: true,
    rootValue: {
        // 1 - название запроса; 2 - ф-я (реализация схемы)
        pet: ({id}) => {
            return pets.find(function (pet) {
                return pet.id == id;
            })
        },
        pets: () => pets,
        createPet: ({ input }) => {
            let pet = new Pet(input);
            pets.push(pet);
            return pet;
        },
        updatePet: ({ id, input }) => {
            const pet = pets.find(function (pet) {
                return pet.id == id;
            });
            Object.assign(pet, input);
            return pet;
        },
        deletePet: ({ id }) => {
            const pet = pets.find(function (pet) {
                return pet.id == id;
            });
            pets = pets.filter(function (pet) {
                return pet.id != id;
            });
            return pet.id;

        },
    }
}));

module.exports = api;

Запросы в GrapiQL (getPets и updatePet это кастомные названия, чтобы GrapiQL мог выбрать query/mutation):

query getPets {
  pets {
    species
    favFoods
    birthYear
    name
  	id
  }
}

mutation updatePet {
  updatePet(id: 1, input: {
    name: "Bobik1",
    species: "dog1",
    birthYear:  2019,
    steps: [
      {
        title: "start",
        completed: true
      }
    ]
  }) {
    id
    name
    birthYear
  }

  deletePet(id: 1)
}

Добавим MongoDB

Для взаимодействия c MongoDB установим mongoose:

npm i mongoose

В папке api создадим файл model.js, в котором мы опишем модель для mongoose:

// api/model.js
const mongoose = require('mongoose');

const Pet = new mongoose.Schema({
    //id: - за id будет отвечать MongoDB
    name: {
        type: String,
        required: true
    },
    species: {
        type: String,
        required: true
    },
    favFoods: [String],
    birthYear: Number,
    photo: String,
    steps: [{
        title: String,
        completed: Boolean
    }]
});

module.exports = mongoose.model('Pet', Pet);

В файле index.js подключим mongoose, модель mongoose (реализованную ранее), подключимся к MongoDB и реализуем работу с запросами и mongoose:

// api/index.js
const express = require('express');
const graphqlMiddleWare = require('express-graphql');
// импортируем схему в файле index.js 
const schema = require('./schema.js');


const mongoose = require('mongoose');
mongoose.Promise = Promise;
mongoose.connect('mongodb://localhost:27017/graphql-intro');
mongoose.connection.once('open', () => console.log('connect to MongoDB'));

// импортируем модель (mongoose)
const Pet = require('./model');

const api = express();


api.all('/api', graphqlMiddleWare({
    schema,
    graphiql: true,
    rootValue: {
        // 1 - название запроса; 2 - ф-я (реализация схемы)
        pet: ({id}) => Pet.findById(id)
        ,
        pets: () => Pet.find({}),
        createPet: ({ input }) => {
            // метод mongoose create возвращает Promise
            return Pet.create(input)
        },
        updatePet: ({ id, input }) => {
            return Pet.findByIdAndUpdate( id, input, {
                new: true // то есть вернуть новый объект
            })
        },
        deletePet: ({ id }) => {
            return Pet.deleteOne({_id: id}).then(() => {
                return id;
            })

        },
    }
}));

module.exports = api;

Три запроса на создание, редактирование и удаление питомцев:

mutation createPet {
  createPet(input: {
    name: "Bobik",
    species: "dog",
    birthYear:  2019
  }) {
    id
    name
  }
}

mutation updatePet {
  updatePet(id: "5c55a389a45bfa12446b9fdf", input: {
    name: "12Bobik",
    species: "12dog",
    birthYear:  122019
  }) {
    id
    name
  }
}


mutation deletePet {
  deletePet(id: "5c55a389a45bfa12446b9fdf") 
}

Файл схемы

На данный момент схема описана через шаблонные строки в файле api/schema.js при вызове метода buildSchema. Но схему можно поместить в специальный файл - schema.graphql:

type Step {
    title: String!,
    completed: Boolean!
}

type Pet {
    id: ID!,
    name: String!,
    species: String!,
    favFoods: [String],
    birthYear: Int,
    photo: String,
    steps: [Step]
}

type Query {
    pet(id: ID!): Pet!,
    pets: [Pet]
}

input StepInput {
    title: String!,
    completed: Boolean = false
}

input PetInput {
    name: String!,
    species: String!,
    birthYear: Int,
    steps: [StepInput]
}

type Mutation {
    createPet(input: PetInput!): Pet
    updatePet(id: ID!, input: PetInput!): Pet
    deletePet(id: ID!): ID
}

Подключим вместо шаблонных строк наш файл schema.graphql:

const { buildSchema } = require('graphql');
const path =  require('path');
const fs = require('fs');

const schema = fs.readFileSync(path.resolve(__dirname, 'schema.graphql'), 'utf-8');

module.exports = buildSchema(schema);
Добавить комментарий