Перейти к содержанию

Сервер 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
}

Ответ:

{
  "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:

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,
    },
  })
)
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
  }
}
{
  "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)