В предыдущей статье мы разворачивали Graphcool Prisma с нуля. На выходе кроме всего прочего мы получили веб-интерфейс, через который можно было добавлять новые топики и публиковать их.
Но это была простейшая система без каких-либо пользователей. База данных содержала одну основную таблицу - Post (топики)
Сегодня мы добавим пользователей, а так же связь Пользователь-Топики и проверку на владельца записей, чтобы публиковать черновики могли только их владельцы.
Добавим модель пользователя.
Прежде чем связать добавить связь топик-пользователь, просто добавим самостоятельную модель User (Пользователь). Для этого откроем файл server/database/datamodel.graphql
Сейчас там прописана только модель Post (Топик).
type Post {
id: ID! @unique
isPublished: Boolean!
title: String!
text: String!
}
Допишем ниже:
type User {
id: ID! @unique
email: String! @unique
password: String!
name: String!
}
Схема описана, теперь надо ее задеплоить, чтобы призма обновила структуру базы данных и сгенерировала необходимые методы для API и работы с базой данных. Для этого перейдем в папку server/ и выполним prisma deploy
Результат выполнения:
prisma deploy
Deploying service `hello-world` to stage `dev` on cluster `local` 52ms
Changes:
User (Type)
+ Created type `User`
+ Created field `id` of type `GraphQLID!`
+ Created field `email` of type `String!`
+ Created field `password` of type `String!`
+ Created field `name` of type `String!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `createdAt` of type `DateTime!`
Applying changes 1.1s
Hooks:
Writing database schema to `src/generated/prisma.graphql` 97ms
Теперь у нас в базе данных появилась таблица User
Запустим веб-консоль, чтобы посмотреть какие методы API у нас теперь имеются.
yarn start
yarn run v1.3.2
warning package.json: No license field
$ node src/index.js
Server is running on http://localhost:4000
Теперь у нас есть есть схема User. Но по-прежнему нет ни запросов (Queries), ни мутаций (Mutations).
query - это обычный запрос, то есть на получение данных. mutation - это запрос на изменение данных. На самом деле это все очень условно, так как GraphQL особо ничего не знает о выполняемых эти запросы/мутации резолверах (функции, обрабатывающие запросы), резолверы на query могут выполнять запросы на обновления, а мутации просто выборки данных. Но есть одна важная деталь: query за один раз можно выполнить сразу несколько и параллельно, в то время как mutation выполняется только поштучно и последовательно.
В данном случае у нас получается, что модель есть, но мы не можем с ней ничего делать, то есть через API мы не можем слать запросы ни на создание пользователей, ни на получение их списков, ничего.
Откроем файл server/src/schema.graphql и допишем в нем в Query users: [User!]! и в Mutation createUser(name: String!, email: String!, password: String!): User
итого получится
# import Post from "./generated/prisma.graphql"
type Query {
feed: [Post!]!
drafts: [Post!]!
post(id: ID!): Post
users: [User!]
}
type Mutation {
createDraft(title: String!, text: String): Post
deletePost(id: ID!): Post
publish(id: ID!): Post
createUser(name: String!, email: String!, password: String!): User
}
Обратите внимание на # import Post from "./generated/prisma.graphql"
Это не комментарий, это так прописана подгрузка типов нашего приложения. Редактировть тот файл нельзя, он генерируется призмой при деплое.
Перезапустим сервер приложения, чтобы вступила в силу новая схема (нажмем Ctrl+C и опять выполним yarn start) и обновим страницу.
Вот теперь у нас появились новые методы в схеме
Попробуем выполнить запрос на создание пользователя. Запрос выполняется, но результат пустой.
Это происходит потому что хотя у нас схема описана, не прописан резолвер на обработку этого запроса. То есть граф обрабатывает запрос, схема вся валидная, выполнение разрешено, но данных нет и не прописана функция на обработку запроса (возврат данных). В таком случае он просто возвращает пустое значение. А вот если бы мы прописали в Query users: [User!]! вместо users: [User!], то тут бы мы получили ошибку от графа, так как знак ! сигнализирует о запрете нулевого значения, то есть обязан быть ненулевой список, и как следует из указания User!, список этот не должен содержать нулевые значения пользователей.
Здесь на всякий случай еще раз объясню структуру запроса
mutation {
createUser(
name:"Test"
email: "test@local.host"
password:"123123"
){
id
name
email
password
}
}
mutation, логично, указывает на то, что это именно запрос из мутаций выполняется, а не просто query. Это в графе разные группы запросов.
createUser - это название конкретной операции, мы так назвали ее в схеме выше.
Все что в круглых скобках - это передаваемые параметры в запрос.
В фигурных - структура возвращаемых данных. То есть в результате выполнения, если будет создан пользователь, мы сразу получим в ответ указанные поля из данных этого пользователя.
Итак, допишем мутацию на создание пользователя. Для этого открываем файл server/src/index.js и в Mutation дописываем наш обработчик createUser.
async createUser(parent, { name, email, password }, ctx, info){
password = await bcrypt.hash(password, 10);
return ctx.db.mutation.createUser(
{ data: { name, email, password } },
info,
)
},
bcrypt я подключил в этом файле выше через const bcrypt = require('bcryptjs')
Так как в этой версии приложения пакет bcryptjs не был установлен, устанавливаем его через команду yarn add bcryptjs и после этого опять запускаем сервер.
Вот теперь пользователь был создан и на выходе мы получили пароль не в чистом виде, как его передавали, а сразу его хеш.
Остается только дописать запрос на получение пользователей в Query.
users(parent, args, ctx, info) {
return ctx.db.query.users({}, info)
},
Перезапускаем сервер, выполняем запрос и видим результат.
Обратите внимание, что мы не писали никаких запросов на непосредственную работу с базой данных. За нас все необходимые запросы создала призма при деплое новой схемы (когда выполняли prisma deploy). Напомню, что запросы эти пишутся в файл server/src/generated/prisma.graphql
А если еще более правильно выражаться, то там не запросы, а API-схемы для еще более низкого слоя всей этой системы - API-сервера призмы, что крутится на порту 4466. То есть получается, что наш проект крутится в своей папке, для него есть своя схема, через которую запросы транслируются на сервер призмы, которая в свою очередь работает с базой данных. При этом в призму мы деплоим изменения только в типах объектов схемы, запросы мы туда не деплоим, это уже наш локальный вопрос.
Авторизация пользователей.
Ну а теперь добавим непосредственно авторизацию пользователей. Зачем нам пользователи без этого?
В схему в Mutation допишем login(email: String!, password: String!): User
и там же ниже допишем еще одну модель.
type AuthPayload {
token: String!
user: User!
}
Это чтобы в ответ мы получали не только объект пользователя, но и токен.
И допишем в резолверы.
async createUser(parent, { name, email, password }, ctx, info){
password = await bcrypt.hash(password, 10);
return ctx.db.mutation.createUser(
{ data: { name, email, password } },
info,
)
},
Перезапускаем сервер, выполняем запрос на авторизацию и получаем ошибку.
Это потому что для работы авторизации требуется объявление произвольного секретного ключа.
Остановим сервер и запустим вот так: APP_SECRET="wefewfwefwef" yarn start
Вот теперь авторизация прошла успешно и мы получили не только объект пользователя, но и токен:
Запрос на получение текущего пользователя.
Теперь мы напишем такой запрос, который будет возвращать объект текущего пользователя в случае его идентификации.
Допишем в схему Query
me: User
И резолвер
me(parent, args, ctx, info) {
let id;
const Authorization = ctx.request.get('Authorization');
if (Authorization) {
const token = Authorization.replace('Bearer ', '')
const { userId } = jwt.verify(token, process.env.APP_SECRET);
id = userId;
}
else {
throw "Не был получен токен";
}
return ctx.db.query.user({ where: { id } }, info)
},
Теперь токен, полученный при авторизации укажем в заголовок запроса Authorization. Если все ОК, мы получим пользователя.
Обратите внимание на приставку Bearer, ее необходимо указывать.
Вот теперь у нас есть не только создание пользователей, но и авторизация.
Связываем пользователей и топики.
Ну и последний штрих: настроим связи Топик-Пользователь и научимся получать топики конкретных пользователей и авторов топиков. Для этого нам надо подправить наши схемы пользователя и топика.
Допишем в модель Post author: User @relation(name: "UserPosts"), а в User posts: [Post!]! @relation(name: "UserPosts"). Получится
type Post {
id: ID! @unique
isPublished: Boolean!
title: String!
text: String!
author: User @relation(name: "UserPosts")
}
type User {
id: ID! @unique
email: String! @unique
password: String!
name: String!
posts: [Post!]! @relation(name: "UserPosts")
}
Задеплоим нашу новую схему prisma deploy.
prisma deploy
Deploying service `hello-world` to stage `dev` on cluster `local` 128ms
Changes:
Post (Type)
+ Created field `author` of type `Relation`
User (Type)
+ Created field `posts` of type `[Relation!]!`
UserPosts (Relation)
+ Created relation between Post and User
Applying changes 1.0s
Hooks:
Writing database schema to `src/generated/prisma.graphql` 119ms
Что примечательно, призма не просто создала новую таблицу для хранения записей Топик-Пользователь, но даже настроила первичные-вторичные ключи.
Перезапустим сервер, обновим страницу и у нас уже есть возможность прописывать в запрос пользователя получение топиков.
Сейчас у нас список пустой, потому что мы не создавали еще топики от имени пользователя.
Для удобства получение ID текущего пользователя вынесем в отдельный метод.
function getUserId(ctx) {
const Authorization = ctx.request.get('Authorization')
if (Authorization) {
const token = Authorization.replace('Bearer ', '')
const { userId } = jwt.verify(token, process.env.APP_SECRET)
return userId
}
return null;
}
И с его использованием чуть перепишем мутацию создания топика.
createDraft(parent, { title, text }, ctx, info) {
const userId = getUserId(ctx)
const author = userId && {
connect: { id: userId },
} || undefined;
return ctx.db.mutation.createPost(
{ data: {
title,
text,
isPublished: false,
author,
} },
info,
)
}
Здесь мы просто дописали получение ID текущего пользователя и если был получен, то передаем в запрос создания топика объект с ID этого пользователя. При чем я специально оставил возможность передачи пустого объекта пользователя, чтобы оставить возможность публикации топиков и анонимно.
Вот теперь при создании топика, если пользователь авторизован, прописывается автор в топик.
А в списке топиков теперь выводятся авторы, если указаны.
А в списке пользователей видны теперь топики пользователей.
И совсем не сложно теперь в списке топиков у авторов получить все топики этих авторов.
Я уж не буду говорить, что сейчас доступны методы и на редактирование/удаление этих сущностей.
Вот так вот за вечер мы настроили себе платформу для регистрации/авторизации и публикации топиков, почти с нуля. На мой взгляд - очень неплохой результат.
Я не буду сейчас расписывать программирование фронта под все это (чтобы веб-морда для авторизации была и т.п.), это будет в следующем уроке. Скажу только что материал освоен и там не менее интересно, чем этот урок.
Исходники проекта лежат под тегом Lesson2: https://github.com/MODX-Club/prismagraphql-demo/tree/Lesson2