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。 這主要是由于其對類型定義的低代碼聲明性方法。
從他們的例子開始 (Starting with their example)
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中,因此我可以在每個步驟中分支和修改代碼。 一路上,我將分支鏈接到該帖子。
基礎 (The basics)
declaring schema types
聲明架構類型
In the Launchpad, you’ll see a typeDefs
template literal:
在啟動板中,您將看到typeDefs
模板文字:
const typeDefs = `type Author {id: Int!firstName: StringlastName: Stringposts: [Post] # the list of Posts by this author}type Post {id: Int!title: Stringauthor: Authorvotes: 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進行交互式查詢
改組 (Reorganizing)
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.
將每種類型的組件放在其自己的文件中是有意義的。 我會做得更好,并將每組組件放在一個自己的“域”文件夾中。
為什么要域名? (Why domains?)
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關鍵字,則可執行模式構建器將抱怨兩個查詢定義。
繼續… (Continuing…)
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行所示。 否則,您很可能最終會在包含鏈的下方錯過一個依賴項。
多個架構和解析器 (Multiple schemas and resolvers)
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
合并 進口。
處理命名空間沖突 (Dealing with Namespace Collisions)
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命名空間引起的復雜性。
然后去哪兒? (Where to go from here?)
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