翻译 | 《JavaScript Everywhere》第8章 用户操作(^_^)
写在最前面
大家好呀,我是毛小悠,是一位前端开发工程师。正在翻译一本英文技术书籍。
为了提高大家的阅读体验,对语句的结构和内容略有调整。如果发现本文中有存在瑕疵的地方,或者你有任何意见或者建议,可以在评论区留言,或者加我的微信:code\_maomao,欢迎相互沟通交流学习。
(σ゚∀゚)σ..:\*☆哎哟不错哦
第8章 用户操作
想像你刚刚加入了一个俱乐部(还记得“超级酷人秘密俱乐部”吗?),当你第一次出现时,没有任何事可做。俱乐部是一个空旷的大房间,人们进进出出,无法与俱乐部的其他人产生互动。我有点内向,所以这听起来还不错,但是我不愿意为此支付会员费。
现在,我们的API
本质上是一个庞大的无用俱乐部。我们有创建数据的方法和用户登录的方法,但是没有任何一种方法允许用户拥有该数据。在本章中,我们将通过添加用户交互来解决这个问题。我们将编写代码,使用户能够拥有自己创建的笔记,限制可以删除或修改笔记的人员,并允许用户“收藏”他们喜欢的笔记。此外,我们将使API
用户能够进行嵌套查询,从而使我们的UI
可以编写将用户与笔记相关联的简单查询。
开始之前
在本章中,我们将对Notes
文件进行一些相当重要的更改。由于我们数据库中的数据量很少,可能你会发现从本地数据库中删除现有笔记更容易。这不是必需的,但是可以减少你在阅读本章时的困惑。
为此,我们将进入MongoDB shell
,确保引用了notedly
的数据库(数据库名称在.env
文件中),并使用MongoDB
的 .remove
()方法。
在终端中,键入以下内容:
$ mongo
$ use notedly
$ db.notes.remove({})
用户添加新笔记
在上一章中,我们更新了src/index.js
文件,以便当用户发出请求时检查JWT
。如果token
令牌存在,我们将对其进行解码并将当前用户添加到我们的GraphQL
上下文中。这使我们可以将用户信息发送到我们调用的每个解析器函数。我们将更新现有的GraphQL
请求以验证用户信息。为此,我们将利用Apollo Server
的AuthenticationError
和ForbiddenError
方法,这将允许我们引发适量的错误。这些将帮助我们调试开发过程以及向客户端发送适和的响应。
在开始之前,我们需要将mongoose
包导入我们的mutations.js
解析器文件中。这将使我们能够适当地分配交叉引用的MongoDB
对象ID
字段。更新src/resolvers/mutation.js
顶部的模块导入,如下所示:
const mongoose = require('mongoose');
现在,在我们的newNote
请求中,我们将用户设置为函数参数,然后检查是否将用户传递给函数。
如果找不到用户ID
,我们将抛出AuthenticationError
,因为必须登录到我们的服务才能创建新笔记。确认已通过身份验证的用户发出请求后,就可以在数据库中创建笔记。为此,我们现在将为作者分配传递给解析器的用户ID
。这将使我们能够从笔记本身中引用创建用户。
在src/resolvers/mutation.js
,添加以下内容:
// add the users context
newNote: async (parent, args, { models, user }) => {
// if there is no user on the context, throw an authentication error
if (!user) {
throw new AuthenticationError('You must be signed in to create a note');
}
return await models.Note.create({
content: args.content,
// reference the author's mongo id
author: mongoose.Types.ObjectId(user.id)
});
},
最后一步是将交叉引用应用于我们数据库中的数据。为此,我们将需要更新MongoDB notes
结构的author
字段。在/src/models/note.js
,如下更新作者字段:
author: { type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
有了此结构,所有新笔记都将准确记录并从请求的上下文中引用的作者。让我们在GraphQL Playground
中编写一个newNote
请求来尝试一下:
mutation {
newNote(content: "Hello! This is a user-created note") { id content
}
}
编写请求时,我们还必须确保在Authorization
标头中传递JWT
(请参见图8-1
):
{ "Authorization": "" }
如何检索JWT
如果你没有JWT
,可以执行signIn
修改来检索。
图8-1
。GraphQL Playground
中的newNote
请求。
目前,我们的API
不会返回作者信息,但是我们可以通过在MongoDB shell
中查找笔记来验证是否正确添加了作者。在终端窗口中,键入以下内容:
mongodb.notes.find({_id: ObjectId("A DOCUMENT ID HERE")})
返回的值应包括作者密钥,并带有对象ID
的值。
用户更新和删除权限
现在,我们还可以将用户检查添加到我们的deleteNote
和updateNote
请求中。这些将要求我们既检查是否将用户传递到上下文,又检查该用户是否是笔记的所有者。为此,我们将检查存储在数据库的author
字段中的用户ID
是否与传递到解析器上下文中的用户ID
相匹配。
在src/resolvers/mutation.js
,如下更新deleteNote
请求:
deleteNote: async (parent, { id }, { models, user }) => {
// if not a user, throw an Authentication Error
if (!user) {
throw new AuthenticationError('You must be signed in to delete a note');
}
// find the note
const note = await models.Note.findById(id);
// if the note owner and current user don't match, throw a forbidden error
if (note && String(note.author) !== user.id) {
throw new ForbiddenError("You don't have permissions to delete the note");
}
try {
// if everything checks out, remove the note
await note.remove();
return true;
} catch (err) {
// if there's an error along the way, return false
return false;
}
},
现在,也在src/resolvers/mutation.js
,如下更新updateNote
请求:
updateNote: async (parent, { content, id }, { models, user }) => { // if not a user, throw an Authentication Error
if (!user) { throw new AuthenticationError('You must be signed in to update a note');
} // find the note
const note = await models.Note.findById(id); // if the note owner and current user don't match, throw a forbidden error
if (note && String(note.author) !== user.id) { throw new ForbiddenError("You don't have permissions to update the note");
} // Update the note in the db and return the updated note
return await models.Note.findOneAndUpdate(
{
_id: id
},
{
$set: {
content
}
},
{ new: true }
);
},
用户查询
通过更新现有的请求包括用户检查,我们还添加一些特定于用户的查询。
为此,我们将添加三个新查询:
user
给定特定的用户名,返回用户的信息
users
返回所有用户的列表
me
返回当前用户的用户信息
在编写查询解析器代码之前,请将这些查询添加到GraphQL src/schema.js
文件,如下所示:
type Query {
...
user(username: String!): User
users: [User!]!
me: User!
}
现在在src/resolvers/query.js
文件,编写以下解析程序查询代码:
module.exports = {
// ...
// add the following to the existing module.exports object:
user: async (parent, { username }, { models }) => {
// find a user given their username
return await models.User.findOne({ username });
},
users: async (parent, args, { models }) => {
// find all users
return await models.User.find({});
},
me: async (parent, args, { models, user }) => {
// find a user given the current user context
return await models.User.findById(user.id);
}
}
让我们看看这些在我们的GraphQL Playground
中的结构。首先,我们可以编写一个用户查询来查找特定用户的信息。确保使用已经创建的用户名:
query {
user(username:"adam") {
username
email id }
}
这将返回一个数据对象,其中包含指定用户的用户名、电子邮件和ID
值(图8-2
)。
图8-2
。GraphQL Playground
中的用户查询
现在要查询数据库中的所有用户,我们可以使用users
查询,这将返回一个包含所有用户信息的数据对象(图8-3
):
query { users { username email
id
}
}
图8-3
。用户在GraphQL Playground
中查询
现在,我们可以使用传递给HTTP
标头的JWT
,通过me
查询来查找有关登录用户的信息。
首先,请确保在GraphQL Playground
的HTTP
标头部分中包含令牌:
{ "Authorization": "" }
现在,像这样执行me
查询(图8-4
):
query { me { username email
id
}
}
图8-4
。GraphQL Playground
中的me
查询
有了这些解析器后,我们现在可以查询API
以获得用户信息。
切换笔记收藏夹
我们还有最后一项功能可以添加到我们的用户交互中。你可能还记得我们的应用程序规范指出:“用户将能够收藏其他用户的笔记,并检索他们的收藏夹列表。类似于Twitter
的“心”和Facebook
的“喜欢”,我们希望用户能够将笔记标记(或取消标记)为收藏。为了实现此行为,我们将修改遵循GraphQL
模式的标准模式,然后是数据库模块,最后是resolver
函数。
首先,我们将在/src/schema.js
中更新GraphQL
模式。通过向我们的Note
类型添加两个新属性来实现。 favoriteCount
将跟踪笔记收到的“收藏夹”总数。 favoritedBy
将包含一组喜欢笔记的用户。
type Note {
// add the following properties to the Note type
favoriteCount: Int!
favoritedBy: [User!]
}
我们还将添加收藏夹列表到我们的用户类型:
type User {
// add the favorites property to the User type
favorites: [Note!]!
}
接下来,我们将/src/schema.js
在中添加一个请求,称为toggleFavorite
,它将添加或删除指定笔记的收藏夹。此请求以笔记ID
作为参数,并返回指定的笔记。
type Mutation {
// add toggleFavorite to the Mutation type
toggleFavorite(id: ID!): Note!
}
接下来,我们需要更新笔记模块,在数据库中包括favoriteCount
和favoritedBy
属性。 最喜欢的数字将是一个数字类型,默认值为0
。 最喜欢的数字将是一个对象数组,其中包含对我们数据库中用户对象ID
的引用。我们完整的/src/models/note.js
文件如下所示:
const noteSchema = new mongoose.Schema(
{
content: {
type: String,
required: true
},
author: {
type: String,
required: true
},
// add the favoriteCount property
favoriteCount: {
type: Number,
default: 0
},
// add the favoritedBy property
favoritedBy: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
]
},
{
// Assigns createdAt and updatedAt fields with a Date type
timestamps: true
}
);
随着我们的GraphQL
模式和数据库模块的更新,我们可以编写toggleFavorite
请求。此请求将收到一个笔记ID
作为参数,并检查用户是否已被列在“ favouritedBy
”数组中。如果列出了该用户,我们将通过减少favoriteCount
并从列表中删除该用户来删除收藏夹。如果用户尚未收藏该笔记,则我们将favouriteCount
增加1
,然后将当前用户添加到favouritedBy
数组中。为此,请将以下代码添加到src/resolvers/mutation.js
文件:
toggleFavorite: async (parent, { id }, { models, user }) => {
// if no user context is passed, throw auth error
if (!user) {
throw new AuthenticationError();
}
// check to see if the user has already favorited the note
let noteCheck = await models.Note.findById(id);
const hasUser = noteCheck.favoritedBy.indexOf(user.id);
// if the user exists in the list
// pull them from the list and reduce the favoriteCount by 1
if (hasUser >= 0) {
return await models.Note.findByIdAndUpdate(
id,
{
$pull: {
favoritedBy: mongoose.Types.ObjectId(user.id)
},
$inc: {
favoriteCount: -1
}
},
{
// Set new to true to return the updated doc
new: true
}
);
} else {
// if the user doesn't exist in the list
// add them to the list and increment the favoriteCount by 1
return await models.Note.findByIdAndUpdate(
id,
{
$push: {
favoritedBy: mongoose.Types.ObjectId(user.id)
},
$inc: {
favoriteCount: 1
}
},
{
new: true
}
);
}
},
使用此代码后,让我们测试一下在GraphQL Playground
中切换喜欢的笔记的功能。让我们用一个新创建的笔记来做到这一点。我们将首先编写一个newNote
请求,确保包含一个带有有效JWT
的Authorization
标头(图8-5
):
mutation {
newNote(content: "Check check it out!") {
content
favoriteCount id }
}
图8-5
。一个newNote
请求
你会注意到该新笔记的收藏夹计数自动设置为0
,因为这是我们在数据模块中设置的默认值。现在,让我们编写一个toggleFavorite
请求''以将其标记为收藏,将笔记的ID
作为参数传递。同样,请确保包括带有有效JWT
的Authorization HTTP
标头。
mutation {
toggleFavorite(id: "") {
favoriteCount
}
}
运行此修改后,笔记的favoriteCount
的值应为1
。如果重新运行该请求,则favoriteCount
将减少为0
(图8-6
)。
图8-6
。toggleFavorite
修改
用户现在可以在收藏夹中标记和取消标记笔记。更重要的是,我希望该功能可以演示如何向GraphQL
应用程序的API
添加新功能。
嵌套查询
GraphQL
的一大优点是我们可以嵌套查询,使我们可以编写单个查询来精确返回所需的数据,而不是用多个查询。
我们的用户类型的GraphQL
模式包括以数组格式列出作者的笔记列表,而我们的笔记类型包括对其作者的引用。所以,我们可用于从用户查询中提取笔记列表,或从笔记查询中获取作者信息。
这意味着我们可以编写如下查询:
query {
note(id: "5c99fb88ed0ca93a517b1d8e") {
id
content
# the information about the author note
author {
username
id
}
}
}
如果现在我们尝试运行类似于上一个查询的嵌套查询,则会收到错误消息。这是因为我们尚未编写用于对此信息执行数据库查找的解析程序代码。要启用此功能,我们将在src/resolvers
目录中添加两个新文件。
在src/resolvers/note.js
,添加以下内容:
module.exports = {
// Resolve the author info for a note when requested
author: async (note, args, { models }) => {
return await models.User.findById(note.author);
},
// Resolved the favoritedBy info for a note when requested
favoritedBy: async (note, args, { models }) => {
return await models.User.find({ _id: { $in: note.favoritedBy } });
}
};
在src/resolvers/user.js
,添加以下内容:
module.exports = {
// Resolve the list of notes for a user when requested
notes: async (user, args, { models }) => {
return await models.Note.find({ author: user._id }).sort({ _id: -1 });
},
// Resolve the list of favorites for a user when requested
favorites: async (user, args, { models }) => {
return await models.Note.find({ favoritedBy: user._id }).sort({ _id: -1 });
}
};
现在我们需要更新src/resolvers/index.js
导入和导出这些新的解析器模块。总体而言,src/resolvers/index.js
文件现在应如下所示:
const Query = require('./query');
const Mutation = require('./mutation');
const Note = require('./note');
const User = require('./user');
const { GraphQLDateTime } = require('graphql-iso-date');
module.exports = {
Query,
Mutation,
Note,
User,
DateTime: GraphQLDateTime
};
现在,如果我们编写一个嵌套的GraphQL
查询或修改,我们将收到我们期望的信息。你可以通过编写以下笔记查询来进行尝试:
query {
note(id: "") {
id
content
# the information about the author note
author {
username
id
}
}
}
该查询应使用作者的用户名和ID
正确解析。另一个实际的示例是返回有关“喜欢”笔记的用户的信息:
mutation {
toggleFavorite(id: "") {
favoriteCount
favoritedBy {
username
}
}
}
使用嵌套的解析器,我们可以编写精确的查询和修改,以精确返回所需的数据。
结论
恭喜你!在本章中,我们的API
逐渐成为一种用户可以真正与之交互的东西。该API
通过集成用户操作/添加新功能和嵌套解析器来展示GraphQL
的真正功能。我们还遵循了一种将真实的代码添加到项目中的尝试模式:首先编写GraphQL
模式,然后编写数据库模块,最后编写解析器代码以查询或更新数据。通过将过程分为三个步骤,我们可以向我们的应用程序添加各种功能。在下一章中,我们将介绍使API
产品准备就绪所需的最后步骤,包括分页和安全性。
如果有理解不到位的地方,欢迎大家纠错。如果觉得还可以,麻烦您点赞收藏或者分享一下,希望可以帮到更多人。