javascript

GraphQL сервер с Koa2 и MongoDB

  • среда, 3 мая 2017 г. в 03:14:41
https://habrahabr.ru/post/327816/
  • Node.JS
  • MongoDB
  • JavaScript


Привет Мир! Сервер — это сердце любого проекта. Сегодня я расскажу, как заставить его биться, чтобы вы смогли использовать в разных целях. Начиная от SPA, мобильный платформ Android + iOS и ограничиваясь лишь вашей фантазией.



Тот кто знаком с GraphQL, в этой статье не узнает ничего нового. Тот кто спешит, может сразу заглянуть в готовый репозиторий GitHub. Тот, кто заинтересован и располагает хотя бы часом времени, сможет с нулевыми знаниями создать свой полностью рабочий GraphQL сервер и что важнее — разобраться, как это все работает, даже если вы не дружите с node.js.

Введение в GraphQL


GraphQL — это язык запросов, который разработал Facebook и анонсировал в 2015 году. Он позволяет получать данные с сервера, добавлять, удалять, редактировать записи и создавать приложение реального времени буквально на коленках.

Подготовка компьютера


Кто знаком с основами node.js, может смело пропустить этот раздел.

В первую очередь на рабочем компьютере нам понадобится последняя версия Node.js 7.7.4+
По завершению установки откройте консоль и напишите команду:

node -v

Консоль вывела v7.7.4+ ? Отлично.

Чтобы не путаться в консольных командах, я составлю компактную таблицу, которая поможет ориентироваться:

npm  // пакетный менеджер, через который мы будем все устанавливать
i    // сокращенно от install
-g   // установка модуля глобально
init // ручное создание файла package.json
init -y   // автоматическое создание файла package.json
-D   // установка пакетов в раздел "devDependencies"
-S   // установка пакетов в раздел "dependencies"
package.json:
"devDependencies" - раздел с модулями используемых при разработке
"dependencies"    - раздел с модулями используемых в продакшене

Приступим к настройке проекта. Выполните по порядку команды в консоле:

// создаст рабочую директорию
mkdir test
// будет выполнять последующие команды из указанной директории
cd test
// создаст автоматически package.json в папке test
npm init -y

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

npm i -g nodemon

По умолчанию все пакеты устанавливаются локально. При глобальной установке, пакет будет доступен на любом проекте в пределах рабочего компьютера.

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


У вас создана папка проекта, в ней имеется файл package.json, глобально установлен nodemon , проект открыт в редакторе кода или IDE? Чудно, пока этого будет достаточно.

Откроем package.json в разделе ”scripts”, удалим строку “test” и добавим новую, чтобы получилось:

"scripts": {
  "dev": "nodemon ./src/main.js"
},

В корне проекта создайте папку srс , а в ней 3 файла: main.js, server.js, db.js .

В файле server.js будут храниться основные настройки сервера. main.js — точка входа проекта. db.js — отдельный файл с настройками подключения к базе данных.

При написание кода, будет использоваться синтаксис es2015 (es6), поэтому понадобится Babel для компиляции es6 в es5:

// babel-preset-es2015 компилирует es6 в es5
// babel-register компилировать на ходу
// babel-plugin-transform-runtime + babel-preset-stage-3 
// пригодятся позже
npm i -D babel-register babel-preset-es2015 babel-preset-stage-3 babel-plugin-transform-runtime

Создадим еще один файл в корне проекта .babelrc c кодом:

{
  "presets": [
    "es2015",
    "stage-3"
  ],
  "plugins": ["transform-runtime"]
}

.babelrc — это файл настроек для компилятора Babel, он используется автоматически. В main.js добавим:

// babel-register будет компилировать код из server.js
require('babel-register');
require('./server');

Установим пакет mongoose , который позволит взаимодействовать с MongoDB.

npm i -S mongoose

В файл db.js добавьте:

import mongoose from 'mongoose';
mongoose.Promise = global.Promise;
// если подключение к БД успешно, то в консоле увидим: 
// 'Connected to mongo server.'
mongoose.connection.on('open', function (ref) {
    console.log('Connected to mongo server.');
});
// если сервер не может подключится к БД, то выведет сообщение: 
// 'Could not connect to mongo server!' + ошибки
mongoose.connection.on('error', function (err) {
    console.log('Could not connect to mongo server!');
    console.log(err);
});
// пример подключения к MongoDB
// mongodb://username:password@host:port/myDataBase
mongoose.connect('mongodb://localhost:27017/test');

Напомню, mongoose всего лишь мост, между сервером и базой данных. Запустить MongoDB можно в облаке или на локальной машине. Поиск в Google: free mongodb cloud поможет найти бесплатное облачный хостинг. Также обратитесь к документации mongoose.

У себя я использую локальную БД: mongodb://localhost:27017/test .
В строке подключения нет username и password, в качестве host: localhost , порт: 27017, имя базы данных: test .

Наконец, мы подошли к последнему шагу настройки сервера. Вернемся к файлу server.js и установим требуемые пакеты:

// @next - это самая последняя версия пакета
npm i -S koa koa-router@next koa-bodyparser@next graphql-server-koa

В сам файл server.js скопируем код:

// koa - node.js фреймворк на базе которого запускается сервер
// koa-router - маршрутизация на сервере
// graphql-server-koa модуль связки, чтобы подружить Koa и GraphQL
import koa from 'koa'; // koa@2
import koaRouter from 'koa-router'; // koa-router@next
import koaBody from 'koa-bodyparser'; // koa-bodyparser@next
import {graphqlKoa, graphiqlKoa} from 'graphql-server-koa';
// знакомство с schema ждет нас впереди
// db.js - файл отвечающий за подключение к MongoDB
import schema from './data/schema'
import './db'
const app = new koa();
const router = new koaRouter();
const PORT = 3000;
// koaBody is needed just for POST.
app.use(koaBody());
// POST и GET запросы будут перенаправляться в схему GraphQL
router.post('/graphql', graphqlKoa({schema: schema}));
router.get('/graphql', graphqlKoa({schema: schema}));
// инструмент для тестирования запросов localhost:3000/graphiql
router.get('/graphiql', graphiqlKoa({endpointURL: '/graphql'}));
app.use(router.routes());
app.use(router.allowedMethods());
// запуск сервера
app.listen(PORT, () => {
    console.log('Server is running on', 'localhost:' + PORT);
    console.log('GraphiQL dashboard', 'localhost:' + PORT + '/graphiql');
});

Настройка сервера завершена. Если вы это осилили, значит хотите увидеть результат работы. В файле server.js закомментируем три строчки кода:

//import schema from './data/schema';
//router.post('/graphql', graphqlKoa({schema: schema}));
//router.get('/graphql', graphqlKoa({schema: schema}));

Мы еще не создавали схему GraphQL, поэтому так мы избежим ошибок и сможем насладиться рабочим сервером. Запустим его:

npm run dev

Если все сделали правильно и имеется подключение к MongoDB, в консоле увидим следующее:

> nodemon ./src/main.js
[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./src/main.js`
Sever is running on localhost:3000
GraphiQL dashboard localhost:3000/graphiql
Connected to mongo server.

В консоле ошибки? Ничего страшного. Откройте оригинальный проект на Github и сверьте файлы за исключением папок data и .idea .

Если получилось запустить без ошибок, откройте в браузере:

http://localhost:3000/graphiql

Вам будет доступен графический инструмент GraphiQL для тестирования запросов. Получилось? Отлично.

Пришло время сделать небольшой перерыв. Налейте себе кофе или чай, в зависимости от того, что вам больше нравится. Позже мы продолжим говорить о самом интересном.

2 + 2 = GraphQL


GraphQL — прост и я могу это доказать.

Недавно, вы возможно прислушались моего совета и сделали себе чай или кофе. Теперь представьте, что за вас это бы сделал некая Jane Dohe.

Так, вам нужно лишь ей сказать, что именно вы хотите: чай или кофе. Jane сможет сама приготовить и принести вам. Даже если вы предпочитаете сахар и молоко в напиток, Jane возьмет у соседки сахар, а за молоком дойдет до магазина. Сама все приготовит и уже в готовом виде принесет вам, в то самое место, где вы об этом ее попросили.

У Jane есть лишь один нюанс, сама она не знает, где брать сахар и молоко, поэтому ей нужно один раз объяснить и при любой повторной просьбе она будет это делать самостоятельно.

Именно так работает GraphQL на примере Jane Dohe. Вы отправляете запрос или запросы из любого места проекта. В тоже самое место, вы получаете ответ в формате JSON. Даже если запрашиваемые данные находятся в разных базах: MongoDB, MySQL, PostgreSQL. С одним нюансом, как и Jane, прежде чем делать запрос, нужно один раз объяснить GraphQL серверу, как готовить данные и откуда их нужно брать.

Помните, когда мы комментировали три строчки кода в server.js, чтобы запустить проект? Пора раскомментировать их обратно:

import schema from './data/schema';
router.post('/graphql', graphqlKoa({schema: schema}));
router.get('/graphql', graphqlKoa({schema: schema}));

В папке src создайте папку data . В папке data создайте файл schema.js и добавьте папку user в которой нам потребуются 3 файла: queries.js , mutations.js и models.js .

Вся схема взаимосвязи будет выглядеть следующим образом:



Прежде чем углубляться, давайте разберемся. У Jane Dohe один нюанс: ей нужно объяснить, где можно взять сахар и молоко. Так вот, когда нам потребуется получить с сервера данные, добавить, изменить, удалить, то каждый случай необходимо описать отдельно.

Например, для получения данных пользователя, мы создадим отдельное поле User, которое по определенным критериям найдет и вернет одного пользователя. То же самое для массива пользователей Users. Любые операции для вывода данных в GraphQL называются — queries и будут храниться в queries.js .

Аналогично queries, операция добавление, удаление и изменение данных, называются mutations и будут храниться в mutations.js . Каждой операции будет соответствовать конкретное поле: UserCreate, UserDelete, UserEdit.

Queries и mutations имеют много общего, помимо того, что они практически идентичны — у них общая модель.

models.js — это файл в котором мы описываем схему коллекции данных, определяем имя, описываем типы, отдельно для queries и mutations.

Типы очень похожи на схему коллекции, при этом имеет три явных преимущества:

  • Типы фильтруют входящие запросы. Вы можете их считать дополнительной защитой на пути к базе данных.
  • Из-за того, что запросы проверяются до модели, это увеличивает производительность сервера.
  • В типах можно ограничивать исходящие данные. Например, запретить получение пароля пользователя или другую важную информацию. Для этого достаточно их не указывать.

Именно из-за преимуществ, для queries и mutations будут отдельные типы.

GraphQL — Just do it!


Пришло время заполнить последние файлы с кодом.

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

npm i -S graphql

Схема — это ядро GraphQL сервера. Она может быть только одна и содержать только по 1 параметру queries и mutations.

Добавим код в schema.js :

import {
    GraphQLObjectType,
    GraphQLSchema
} from 'graphql';
// импортируем queries и mutations из папки user
import UserQueries from './user/queries'
import UserMutations from './user/mutations'
// создадим GraphQL схему и заполним параметрами
// каждый параметр может содержать только один GraphQLObjectType
export default new GraphQLSchema({
    query: new GraphQLObjectType({
        name: 'Query',      // произвольное имя для API библитеки
        fields: UserQueries // поля из файла queries.js
    }),
    mutation: new GraphQLObjectType({
        name: 'Mutation',
        fields: UserMutations
    })
});

От схемы перейдем к models.js :

import {
    GraphQLObjectType,
    GraphQLInputObjectType,
    GraphQLNonNull,
    GraphQLString,
    GraphQLID
} from 'graphql';
import mongoose from 'mongoose';
// схема коллекции
const schema = new mongoose.Schema({
    firstName: {
        type: String,
    },
    lastName: {
        type: String,
    }
});
// определяем коллекцию User и подключаем к ней схему
export let UserModel = mongoose.model('User', schema);
// тип для queries
export let UserType = new GraphQLObjectType({
    name: 'User',
    fields: {
        _id: {
            type: new GraphQLNonNull(GraphQLID)
        },
        firstName: {
            type: GraphQLString
        },
        lastName: {
            type: GraphQLString
        }
    }
});
// тип для mutations
export let UserInput = new GraphQLInputObjectType({
    name: "UserInput",
    fields: {
        firstName: {
            type: GraphQLString
        },
        lastName: {
            type: GraphQLString
        }
    }
});

Вспомните, мы ранее говорили о полях User и Users, для вывода пользователя и пользователей соответственно. Пора заполнить файл queries.js :

import {
    GraphQLID,
    GraphQLList,
    GraphQLNonNull
} from 'graphql';
// импортируем данные из models.js
import {UserModel, UserType, UserInput} from './models';
// создаем поле для получения одного пользователя
const User = {
    type: UserType,  // тип для получения данных пользователя
    args: {
        // аргумент для поиска пользователь
        id: {      
            name: 'id',
            type: new GraphQLNonNull(GraphQLID)
        }
    },
    // метод, в котором формируется запрос и возвращаются данные
    resolve (root, params, options) {
        return UserModel
            .findById(params.id)
            .exec();  // возвращаем JSON
    }
};
const Users = {
    type: new GraphQLList(UserType),
    args: {}, 
    resolve (root, params, options) {
        return UserModel
            .find()
            .exec();
    }
};
export default {
    User: User,
    Users: Users,
}

Mutations практически аналогичны queries. Queries выполняются асинхронно, а mutations последовательно, один за одним. Добавьте код в mutations.js .

import {
    GraphQLNonNull,
    GraphQLBoolean,
} from 'graphql';
import {UserModel, UserType, UserInput} from './models';
const UserCreate = {
    description: "Create new user",
    type: GraphQLBoolean,
    args: {
        data: {
            name: "data",
            type: new GraphQLNonNull(UserInput)
        }
    },
    async resolve (root, params, options) {
        const userModel = new UserModel(params.data);
        const newUser = await userModel.save();
if (!newUser) {
            throw new Error('Error adding new user');
        }
        return true;
    }
};
export default {
    UserCreate: UserCreate,
}

Я вас поздравляю! Вы создали свой GraphQL сервер с нуля. Осталось его протестировать.

npm run dev

Если в консоле ошибки, то обратитесь к рабочему репозиторию Github. При запущеном, рабочем сервере, перейдите по ссылке в браузере:

http://localhost:3000/graphiql

В открытом окне, напишите свои первые мутации:

mutation firstMutation{
  UserCreate(data: {firstName: "John", lastName: "Dohe"})
}
mutation secondMutation{
  UserCreate(data: {firstName: "Jane", lastName: "Dohe"})
}

При успешном результате вы получите:

{
  "data": {
    "UserCreate": true
  }
}

Последним шагом, выведем всех пользователей из MongoDB:

{
  Users{
    firstName
    lastName
  }
}

В ответ мы получим:

{
  "data": {
    "Users": [
      {
        "firstName": "John",
        "lastName": "Dohe"
      },
      {
        "firstName": "Jane",
        "lastName": "Dohe"
      }
    ]
  }
}

На этом мы закончили. Возможно, к этой статье вы пришли с базовыми знаниями или нашли для себя что-то новое. Так, начиная с простых вещей о node.js, создания сервера на Koa и подключением к MongoDB, полулось собрать полностью рабочий GraphQL сервер.

Если у вас имеются вопросы или пожелания, я с удовольствием отвечу в комментариях.

Спасибо всем за внимание.