Тот кто знаком с 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 сервер.
Если у вас имеются вопросы или пожелания, я с удовольствием отвечу в комментариях.
Спасибо за внимание.
Комментарии (11)
bagzon
03.05.2017 14:33В итоге всё красиво и удобно, но процесс разработки, прям чувствуется боль) Наверное это из-за недопонимания и порога вхождения…
nikitamarcius
03.05.2017 14:41Да, такая проблема у большинства в начале. Когда в голове все укладывается, GraphQL становится простым и понятным.
teknik2008
03.05.2017 14:41А как разделить права доступа?
Ну в к примеру Вася может просмотреть все данные пользователя (поля), а Маша только Контактные данные.
И Маша имеет доступ к платежам пользователей (отдельная таблица) и может их мутировать, а Петя только просмотреть, а Вася вообще не может. Где это задавать на уровне GraphQLSchema или есть др места разделения прав доступаnikitamarcius
03.05.2017 15:10На клиенте. На основание первичного запроса к серверу и полученных данных с категорией пользователя.
teknik2008
03.05.2017 15:28Я понимаю что можно перечислить поля запроса, но вы вроде не поняли сути. Я говорю о том что Маша не может просматривать поля пользователя, даже если она с клиента их перечислит. Как создавать такие ограничения? Как делать запреты просмотра(мутаций) схем отдельной категории пользователей? Все хорошо когда сайт обще доступен. Но если на нем есть ограничения, то как тогда строить схемы? Вот это интересно. Да спасибо что сделали обзор, но он не вышел за рамки других обзоров этой технологии.
nikitamarcius
03.05.2017 17:44Для ограничения mutations, в методе resolve можно проверять права пользователя и на основание прав выполнять или запрещать мутацию.
С queries сложней. На основание переданных аргументов в запросе, в методе resolve будет происходить поиск и получение данных, а возвращаемые данные упираются в описанные типы:
const TestType ={ // описанные тип _id: { type: GraphQLID }, username: { type: GraphQLString }, } const test = { type: TestType, // возвращаемые данные args: { }, // передаваемые аргументы с запросом resolve (root, args, options) { } // метод }
Возвращаемые поля, это контанта. C GraphQL нет возможности убрать из типа определенное поле. Его можно либо не указывать в запросе на стороне клиента, либо принудительно вернуть null в методе resolve.
punk423
03.05.2017 17:48Так же было бы очень интересно увидеть реализацию микросервисной архитектуры.
- Когда мы одним graphql запросом выбираем данные из несколько микросервисов.
- Когда есть вложенность микросервисов в запросе (пример трёх сервисов. Пользователи, посты, комментарии).
{ Users{ fisrtName Posts{ title Comments{ text Users{ firstName } } } } }
Yeah
Базовые штуки понятны, а как там со связями, например. Особенно интересует вопрос производительности
nikitamarcius
Если вопрос актуален, смогу написать отдельную статью.
Не смогу ответить, в эту сторону я не копал.