graphql tools
I’ve been working with GraphQL for a few months now, but only recently began using Apollo’s graphql-tools library. After learning a few idioms, I am able to mock up a functional API quickly. This is largely due to its low-code, declarative approach to type definitions.
我已经使用GraphQL已有几个月了,但是直到最近才开始使用Apollo的graphql-tools库。 学习了一些习惯用法后,我便能够快速模拟出功能性的API。 这主要是由于其对类型定义的低代码声明性方法。
Apollo has an interactive LaunchPad web site, like the ones covered in my Swagger series. There are several example schemas you can use, and for this article I will use their Post and Authors schema. You can download or fork the code.
阿波罗(Apollo)有一个交互式LaunchPad网站,就像我的Swagger系列文章中介绍的网站一样。 您可以使用几种示例模式,在本文中,我将使用其Post and Authors模式 。 您可以下载或分叉代码。
I will be rearranging the project folders. For this post I’ll download and store it in Github, so I can branch and modify the code through each step. Along the way, I’ll link the branches to this post.
我将重新排列项目文件夹。 对于这篇文章,我将其下载并存储在Github中,因此我可以在每个步骤中分支和修改代码。 一路上,我将分支链接到该帖子。
declaring schema types
声明架构类型
In the Launchpad, you’ll see a typeDefs
template literal:
在启动板中,您将看到typeDefs
模板文字:
const typeDefs = `
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post] # the list of Posts by this author
}
type Post {
id: Int!
title: String
author: Author
votes: Int
}
# the schema allows the following query:
type Query {
posts: [Post]
author(id: Int!): Author
}
# this schema allows the following mutation:
type Mutation {
upvotePost (
postId: Int!
): Post
}
`;
There are two entities defined, Author
and Post
. In addition, there are two “magic” types: Query
and Mutation
. The Query type defines the root accessors
. In this case, there’s an accessor to fetch all Posts
, and another to fetch a single Author
by ID
.
定义了两个实体 , Author
和Post
。 此外,还有两种“魔术” 类型 : Query
和Mutation
。 查询类型定义根accessors
。 在这种情况下,有一个访问器来获取所有Posts
,另一个访问器是通过ID
获取单个Author
。
Note there is no way to directly query for a list of authors or for a single post. It is possible to add such queries later.
请注意,无法直接查询作者列表或单个帖子。 以后可以添加此类查询。
declaring resolvers
宣布解析器
Resolvers provide the necessary logic to support the schema. They are written as a JavaScript object with keys that match the types defined in the schema. The resolver
shown below operates against static data, which I’ll cover in a moment.
解析程序提供了支持架构的必要逻辑。 它们被编写为具有与模式中定义的类型相匹配的键JavaScript对象。 下面显示的resolver
针对静态数据进行操作,我将在稍后介绍。
const resolvers = {
Query: {
posts: () => posts,
author: (_, { id }) => find(authors, { id: id }),
},
Mutation: {
upvotePost: (_, { postId }) => {
const post = find(posts, { id: postId });
if (!post) {
throw new Error(`Couldn't find post with id ${postId}`);
}
post.votes += 1;
return post;
},
},
Author: {
posts: (author) => filter(posts, { authorId: author.id }),
},
Post: {
author: (post) => find(authors, { id: post.authorId }),
},
};
To link schema
and resolver
together, we’ll create an executable schema instance:
要将schema
和resolver
链接在一起,我们将创建一个可执行架构实例:
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
the data source
数据源
For this simple example, the data comes from two arrays of objects defined as constants: authors
and posts
:
对于此简单示例,数据来自定义为常量的两个对象数组: authors
和posts
:
const authors = [
{ id: 1, firstName: 'Tom', lastName: 'Coleman' },
{ id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
{ id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
];
const posts = [
{ id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
{ id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
{ id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
{ id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
];
the server
服务器
You can serve up the executable schema through graphql_express, apollo_graphql_express, or graphql-server-express. We see that in this example.
您可以通过graphql_express , apollo_graphql_express或graphql-server-express提供可执行模式。 我们在这个例子中看到了。
The important bits are:
重要的位是:
import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
import { schema, rootValue, context } from './schema';
const PORT = 3000;
const server = express();
server.use('/graphql', bodyParser.json(), graphqlExpress(request => ({
schema,
rootValue,
context: context(request.headers, process.env),
})));
server.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
}));
server.listen(PORT, () => {
console.log(`GraphQL Server is now running on
http://localhost:${PORT}/graphql`);
console.log(`View GraphiQL at
http://localhost:${PORT}/graphiql`);
});
Note that there are two pieces of GraphQL middleware in use:
请注意,有两个正在使用的GraphQL中间件:
graphqlExpress
graphqlExpress
the GraphQL server that handles queries and responses
处理查询和响应的GraphQL服务器
graphiqlExpress
graphiqlExpress
the interactive GraphQL web service that allows interactive queries through an HTML UI
交互式GraphQL Web服务,该服务允许通过HTML UI进行交互式查询
For large apps, we suggest splitting your GraphQL server code into 4 components: Schema, Resolvers, Models, and Connectors, which each handle a specific part of the work. (http://dev.apollodata.com/tools/graphql-tools/)
对于大型应用程序,我们建议将GraphQL服务器代码分成4个组件:架构,解析器,模型和连接器,它们分别处理工作的特定部分。 ( http://dev.apollodata.com/tools/graphql-tools/ )
Putting each type of component in its own file makes sense. I’ll go one better and put each set of components in a its own “domain” folder.
将每种类型的组件放在其自己的文件中是有意义的。 我会做得更好,并将每组组件放在一个自己的“域”文件夹中。
Domains are a convenient way to split up a large system into areas of operation. Within each domain there may be subdomains. In general, subdomains have a bounded context. Within a bounded context the entity names, properties, and processes have precise meaning.
域是将大型系统划分为多个操作区域的便捷方法。 在每个域中可能有子域。 通常,子域具有有限的上下文。 在有限的上下文中,实体名称,属性和过程具有精确的含义。
I find bounded contexts to be helpful during analysis, especially when talking to domain experts.
我发现有限的上下文在分析过程中会有所帮助,特别是在与领域专家交谈时。
The fly in the ointment is that GraphQL types occupy a single namespace, so naming conflicts can exist. More on that later.
美中不足的是,GraphQL类型仅占用一个名称空间,因此可能存在命名冲突。 以后再说。
I’ll call this domain authorposts, and put the related components in the authorposts folder
. Within that, I’ll create a file each for datasource
, resolvers
, and schema. Let’s also toss in an index.js
file to simplify importing. The original schema and server files will remain in the root folder, but the schema.js
code will be skeletal. The find
and filter
methods imported from lodash will be removed in favor of synonymous native ES6 methods. The resulting source is here.
我将这个域称为authorposts ,并将相关组件放入authorposts folder
authorposts folder
authorposts folder
。 在其中,我将分别为datasource
, resolvers
和schema创建一个文件。 让我们也将index.js
文件折腾以简化导入。 原始模式和服务器文件将保留在根文件夹中,但是schema.js
代码将是骨架的。 find
从lodash导入的filter
方法将被删除,以支持同义的本机ES6方法。 结果来源在这里 。
The main schema file has become simpler. It provides skeletal structure for further extension by schemas in our domains.
主模式文件变得更加简单。 它为我们的领域中的架构提供了进一步扩展的骨架结构。
A domain
schema is imported on lines 7–8, and the base
schema on lines 11–23. You’ll note there is a domain property. This is arbitrary but GraphQL, or graphql-tools, insists that one property be defined.
domain
模式在第7–8行导入, base
模式在第11–23行导入。 您会注意到有一个域属性。 这是任意的,但是GraphQL或graphql-tools坚持要定义一个属性。
The complete schema is constructed on line 26, and an executableSchema
instance is created given the schema
and resolvers
defined so far on lines 28–33. This is what is imported by the server.js code, which is largely unchanged from the original.
完整的架构在第26行上构建,并根据第28–33行到目前为止定义的schema
和resolvers
创建了一个executableSchema
实例。 这就是server.js代码导入的内容,与原始代码基本没有变化。
There is a trick to splitting up a schema this way. Let’s take a look:
有这样一种技巧可以拆分模式。 让我们来看看:
The first listing, authorpostResolvers.js
, is pretty much a cut’n’paste job from the original schema.js
source from Apollo’s example. Yet in the authorpostSchema.js
code, we extend the Query
and Mutator
definitions that are declared in the the base schema. If you don’t use the extend keyword, the executable schema builder will complain about two Query definitions.
第一个清单authorpostResolvers.js
是Apollo示例中原始schema.js
源代码中的一个“剪切”粘贴工作。 但是,在authorpostSchema.js
代码中,我们扩展了在基础架构中声明的Query
和Mutator
定义。 如果不使用extend关键字,则可执行模式构建器将抱怨两个查询定义。
This is a good start for organizing several schemas, one for each domain of interest (so long as you're mindful of the global namespace for types), but a complete schema, even for a single domain, can get huge. Fortunately, you can break down each schema even further, right down to the entity level, if necessary.
这是组织多个模式的一个良好的开始,一个模式用于每个感兴趣的域(只要您注意类型的全局名称空间),但是即使是单个域,一个完整的模式也会变得庞大。 幸运的是,您可以根据需要甚至进一步细分每个架构,直至实体级别 。
Here’s a modified directory structure, and listings of the new contents:
这是修改后的目录结构,并列出了新内容:
We can achieve granularity by defining two component files, then importing them into a domain schema.
我们可以通过定义两个组件文件,然后将它们导入域模式来实现粒度。
You don’t have to do one component per file. But you do want to be sure that the schema exports those components along with the schema itself as shown on line 20 of schema.js. Otherwise you’ll likely wind up missing a dependency further down the inclusion chain.
您不必为每个文件做一个组件。 但是您确实要确保该模式将这些组件与模式本身一起导出,如schema.js的第20行所示。 否则,您很可能最终会在包含链的下方错过一个依赖项。
Adding a new schema for a new domain is simple. Create a new domain folder and add dataSource, resolvers, schema, and index.js files. You can also add an optional component folder with component type definitions.
为新域添加新架构很简单。 创建一个新的域文件夹,并添加dataSource,解析器,架构和index.js文件。 您还可以添加带有组件类型定义的可选组件文件夹。
Finally, the root schema.js file must combine the schemas and resolvers from both domains:
最后,根schema.js文件必须结合两个域中的模式和解析器:
//...
import {
schema as myLittleTypoSchema,
resolvers as myLittleTypeResolvers
} from './myLittleDomain';
import {
merge
} from 'lodash';
//...
const schema = [...baseSchema, ...authorpostsSchema, ...myLittleTypoSchema]
const options = {
typeDefs: schema,
resolvers: merge(authorpostsResolvers, myLittleTypeResolvers)
}
Note that I had to include lodash
merge here because of the need for a deep merge of the two resolvers imports.
请注意,由于必须深度合并两个解析器 ,因此我必须在此处包括lodash
合并 进口。
If you are on a large project, you will encounter type name collisions. You might think that Account in one domain would mean the same as Account in another. Yet even if they do mean more or less similar things, chances are the properties and relationships will be different. So technically they are not the same type.
如果您在大型项目中,则会遇到类型名称冲突。 您可能会认为一个域中的帐户与另一个域中的帐户含义相同。 然而,即使它们确实或多或少地意味着相似的事物,属性和关系也有可能会不同。 因此,从技术上讲,它们不是同一类型。
At the time of this writing, GraphQL uses a single namespace for types.
在撰写本文时,GraphQL对类型使用单个名称空间。
How to work around this? Facebook apparently uses a naming convention for their 10,000 types. As awkward as that seems, it works for them.
如何解决这个问题? Facebook显然为其10,000种类型使用命名约定 。 看起来很尴尬,但对他们有用。
The Apollo graphql-tools stack appears to catch type name duplications. So you should be good there.
Apollo graphql-tools堆栈似乎捕获类型名称重复项。 所以你在那里应该很好。
There is an ongoing discussion on whether to include namespaces in GraphQL. It isn’t a simple decision . I remember the complexities caused by the introduction of XML Namespaces 10 years ago.
关于是否在GraphQL中包括名称空间的讨论正在进行中。 这不是一个简单的决定。 我记得10年前引入XML命名空间引起的复杂性。
This post only scratches the surface of how one might organize a large set of GraphQL schemas. The next post will be about mocking GraphQL resolvers, and how it’s possible to mix both real and mocked values in query responses.
这篇文章只是对如何组织一大套GraphQL模式的表述。 下一篇文章将关于模拟GraphQL解析器,以及如何在查询响应中混合使用真实值和模拟值。
翻译自: https://www.freecodecamp.org/news/declarative-graphql-with-graphql-tools-cd1645f94fc/
graphql tools