├── .gitignore
├── LICENSE
├── README.md
├── manuscript
├── Book.txt
├── Sample.txt
├── chapter-0.md
├── chapter-1.md
├── chapter-2.md
├── chapter-3.md
├── chapter-4.md
├── chapter-5.md
├── chapter-6.md
└── images
│ ├── addpin-page.png
│ ├── api.png
│ ├── business-logic.png
│ ├── client-server.png
│ ├── client.png
│ ├── current-file-structure.png
│ ├── database.png
│ ├── final-file-structure.png
│ ├── graphql-playground.png
│ ├── graphql-schema.png
│ ├── http.png
│ ├── login-page.png
│ ├── pinlist-page.png
│ ├── profile-page.png
│ ├── schema.png
│ ├── server-layers.png
│ ├── server.png
│ ├── subscriptions.png
│ ├── title_page.png
│ ├── types-resolvers.png
│ └── verify-page.png
├── package.json
└── styles
├── highlight.min.css
└── styles.css
/.gitignore:
--------------------------------------------------------------------------------
1 | WHITEBOARD.md
2 | .vscode
3 | scripts/hello-graphql/hello-graphql.sh
4 | *-error.log
5 | node_modules
6 | **/.DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Attribution-NonCommercial-ShareAlike 4.0 International
2 |
3 | =======================================================================
4 |
5 | Creative Commons Corporation ("Creative Commons") is not a law firm and
6 | does not provide legal services or legal advice. Distribution of
7 | Creative Commons public licenses does not create a lawyer-client or
8 | other relationship. Creative Commons makes its licenses and related
9 | information available on an "as-is" basis. Creative Commons gives no
10 | warranties regarding its licenses, any material licensed under their
11 | terms and conditions, or any related information. Creative Commons
12 | disclaims all liability for damages resulting from their use to the
13 | fullest extent possible.
14 |
15 | Using Creative Commons Public Licenses
16 |
17 | Creative Commons public licenses provide a standard set of terms and
18 | conditions that creators and other rights holders may use to share
19 | original works of authorship and other material subject to copyright
20 | and certain other rights specified in the public license below. The
21 | following considerations are for informational purposes only, are not
22 | exhaustive, and do not form part of our licenses.
23 |
24 | Considerations for licensors: Our public licenses are
25 | intended for use by those authorized to give the public
26 | permission to use material in ways otherwise restricted by
27 | copyright and certain other rights. Our licenses are
28 | irrevocable. Licensors should read and understand the terms
29 | and conditions of the license they choose before applying it.
30 | Licensors should also secure all rights necessary before
31 | applying our licenses so that the public can reuse the
32 | material as expected. Licensors should clearly mark any
33 | material not subject to the license. This includes other CC-
34 | licensed material, or material used under an exception or
35 | limitation to copyright. More considerations for licensors:
36 | wiki.creativecommons.org/Considerations_for_licensors
37 |
38 | Considerations for the public: By using one of our public
39 | licenses, a licensor grants the public permission to use the
40 | licensed material under specified terms and conditions. If
41 | the licensor's permission is not necessary for any reason--for
42 | example, because of any applicable exception or limitation to
43 | copyright--then that use is not regulated by the license. Our
44 | licenses grant only permissions under copyright and certain
45 | other rights that a licensor has authority to grant. Use of
46 | the licensed material may still be restricted for other
47 | reasons, including because others have copyright or other
48 | rights in the material. A licensor may make special requests,
49 | such as asking that all changes be marked or described.
50 | Although not required by our licenses, you are encouraged to
51 | respect those requests where reasonable. More considerations
52 | for the public:
53 | wiki.creativecommons.org/Considerations_for_licensees
54 |
55 | =======================================================================
56 |
57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
58 | Public License
59 |
60 | By exercising the Licensed Rights (defined below), You accept and agree
61 | to be bound by the terms and conditions of this Creative Commons
62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License
63 | ("Public License"). To the extent this Public License may be
64 | interpreted as a contract, You are granted the Licensed Rights in
65 | consideration of Your acceptance of these terms and conditions, and the
66 | Licensor grants You such rights in consideration of benefits the
67 | Licensor receives from making the Licensed Material available under
68 | these terms and conditions.
69 |
70 |
71 | Section 1 -- Definitions.
72 |
73 | a. Adapted Material means material subject to Copyright and Similar
74 | Rights that is derived from or based upon the Licensed Material
75 | and in which the Licensed Material is translated, altered,
76 | arranged, transformed, or otherwise modified in a manner requiring
77 | permission under the Copyright and Similar Rights held by the
78 | Licensor. For purposes of this Public License, where the Licensed
79 | Material is a musical work, performance, or sound recording,
80 | Adapted Material is always produced where the Licensed Material is
81 | synched in timed relation with a moving image.
82 |
83 | b. Adapter's License means the license You apply to Your Copyright
84 | and Similar Rights in Your contributions to Adapted Material in
85 | accordance with the terms and conditions of this Public License.
86 |
87 | c. BY-NC-SA Compatible License means a license listed at
88 | creativecommons.org/compatiblelicenses, approved by Creative
89 | Commons as essentially the equivalent of this Public License.
90 |
91 | d. Copyright and Similar Rights means copyright and/or similar rights
92 | closely related to copyright including, without limitation,
93 | performance, broadcast, sound recording, and Sui Generis Database
94 | Rights, without regard to how the rights are labeled or
95 | categorized. For purposes of this Public License, the rights
96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar
97 | Rights.
98 |
99 | e. Effective Technological Measures means those measures that, in the
100 | absence of proper authority, may not be circumvented under laws
101 | fulfilling obligations under Article 11 of the WIPO Copyright
102 | Treaty adopted on December 20, 1996, and/or similar international
103 | agreements.
104 |
105 | f. Exceptions and Limitations means fair use, fair dealing, and/or
106 | any other exception or limitation to Copyright and Similar Rights
107 | that applies to Your use of the Licensed Material.
108 |
109 | g. License Elements means the license attributes listed in the name
110 | of a Creative Commons Public License. The License Elements of this
111 | Public License are Attribution, NonCommercial, and ShareAlike.
112 |
113 | h. Licensed Material means the artistic or literary work, database,
114 | or other material to which the Licensor applied this Public
115 | License.
116 |
117 | i. Licensed Rights means the rights granted to You subject to the
118 | terms and conditions of this Public License, which are limited to
119 | all Copyright and Similar Rights that apply to Your use of the
120 | Licensed Material and that the Licensor has authority to license.
121 |
122 | j. Licensor means the individual(s) or entity(ies) granting rights
123 | under this Public License.
124 |
125 | k. NonCommercial means not primarily intended for or directed towards
126 | commercial advantage or monetary compensation. For purposes of
127 | this Public License, the exchange of the Licensed Material for
128 | other material subject to Copyright and Similar Rights by digital
129 | file-sharing or similar means is NonCommercial provided there is
130 | no payment of monetary compensation in connection with the
131 | exchange.
132 |
133 | l. Share means to provide material to the public by any means or
134 | process that requires permission under the Licensed Rights, such
135 | as reproduction, public display, public performance, distribution,
136 | dissemination, communication, or importation, and to make material
137 | available to the public including in ways that members of the
138 | public may access the material from a place and at a time
139 | individually chosen by them.
140 |
141 | m. Sui Generis Database Rights means rights other than copyright
142 | resulting from Directive 96/9/EC of the European Parliament and of
143 | the Council of 11 March 1996 on the legal protection of databases,
144 | as amended and/or succeeded, as well as other essentially
145 | equivalent rights anywhere in the world.
146 |
147 | n. You means the individual or entity exercising the Licensed Rights
148 | under this Public License. Your has a corresponding meaning.
149 |
150 |
151 | Section 2 -- Scope.
152 |
153 | a. License grant.
154 |
155 | 1. Subject to the terms and conditions of this Public License,
156 | the Licensor hereby grants You a worldwide, royalty-free,
157 | non-sublicensable, non-exclusive, irrevocable license to
158 | exercise the Licensed Rights in the Licensed Material to:
159 |
160 | a. reproduce and Share the Licensed Material, in whole or
161 | in part, for NonCommercial purposes only; and
162 |
163 | b. produce, reproduce, and Share Adapted Material for
164 | NonCommercial purposes only.
165 |
166 | 2. Exceptions and Limitations. For the avoidance of doubt, where
167 | Exceptions and Limitations apply to Your use, this Public
168 | License does not apply, and You do not need to comply with
169 | its terms and conditions.
170 |
171 | 3. Term. The term of this Public License is specified in Section
172 | 6(a).
173 |
174 | 4. Media and formats; technical modifications allowed. The
175 | Licensor authorizes You to exercise the Licensed Rights in
176 | all media and formats whether now known or hereafter created,
177 | and to make technical modifications necessary to do so. The
178 | Licensor waives and/or agrees not to assert any right or
179 | authority to forbid You from making technical modifications
180 | necessary to exercise the Licensed Rights, including
181 | technical modifications necessary to circumvent Effective
182 | Technological Measures. For purposes of this Public License,
183 | simply making modifications authorized by this Section 2(a)
184 | (4) never produces Adapted Material.
185 |
186 | 5. Downstream recipients.
187 |
188 | a. Offer from the Licensor -- Licensed Material. Every
189 | recipient of the Licensed Material automatically
190 | receives an offer from the Licensor to exercise the
191 | Licensed Rights under the terms and conditions of this
192 | Public License.
193 |
194 | b. Additional offer from the Licensor -- Adapted Material.
195 | Every recipient of Adapted Material from You
196 | automatically receives an offer from the Licensor to
197 | exercise the Licensed Rights in the Adapted Material
198 | under the conditions of the Adapter's License You apply.
199 |
200 | c. No downstream restrictions. You may not offer or impose
201 | any additional or different terms or conditions on, or
202 | apply any Effective Technological Measures to, the
203 | Licensed Material if doing so restricts exercise of the
204 | Licensed Rights by any recipient of the Licensed
205 | Material.
206 |
207 | 6. No endorsement. Nothing in this Public License constitutes or
208 | may be construed as permission to assert or imply that You
209 | are, or that Your use of the Licensed Material is, connected
210 | with, or sponsored, endorsed, or granted official status by,
211 | the Licensor or others designated to receive attribution as
212 | provided in Section 3(a)(1)(A)(i).
213 |
214 | b. Other rights.
215 |
216 | 1. Moral rights, such as the right of integrity, are not
217 | licensed under this Public License, nor are publicity,
218 | privacy, and/or other similar personality rights; however, to
219 | the extent possible, the Licensor waives and/or agrees not to
220 | assert any such rights held by the Licensor to the limited
221 | extent necessary to allow You to exercise the Licensed
222 | Rights, but not otherwise.
223 |
224 | 2. Patent and trademark rights are not licensed under this
225 | Public License.
226 |
227 | 3. To the extent possible, the Licensor waives any right to
228 | collect royalties from You for the exercise of the Licensed
229 | Rights, whether directly or through a collecting society
230 | under any voluntary or waivable statutory or compulsory
231 | licensing scheme. In all other cases the Licensor expressly
232 | reserves any right to collect such royalties, including when
233 | the Licensed Material is used other than for NonCommercial
234 | purposes.
235 |
236 |
237 | Section 3 -- License Conditions.
238 |
239 | Your exercise of the Licensed Rights is expressly made subject to the
240 | following conditions.
241 |
242 | a. Attribution.
243 |
244 | 1. If You Share the Licensed Material (including in modified
245 | form), You must:
246 |
247 | a. retain the following if it is supplied by the Licensor
248 | with the Licensed Material:
249 |
250 | i. identification of the creator(s) of the Licensed
251 | Material and any others designated to receive
252 | attribution, in any reasonable manner requested by
253 | the Licensor (including by pseudonym if
254 | designated);
255 |
256 | ii. a copyright notice;
257 |
258 | iii. a notice that refers to this Public License;
259 |
260 | iv. a notice that refers to the disclaimer of
261 | warranties;
262 |
263 | v. a URI or hyperlink to the Licensed Material to the
264 | extent reasonably practicable;
265 |
266 | b. indicate if You modified the Licensed Material and
267 | retain an indication of any previous modifications; and
268 |
269 | c. indicate the Licensed Material is licensed under this
270 | Public License, and include the text of, or the URI or
271 | hyperlink to, this Public License.
272 |
273 | 2. You may satisfy the conditions in Section 3(a)(1) in any
274 | reasonable manner based on the medium, means, and context in
275 | which You Share the Licensed Material. For example, it may be
276 | reasonable to satisfy the conditions by providing a URI or
277 | hyperlink to a resource that includes the required
278 | information.
279 | 3. If requested by the Licensor, You must remove any of the
280 | information required by Section 3(a)(1)(A) to the extent
281 | reasonably practicable.
282 |
283 | b. ShareAlike.
284 |
285 | In addition to the conditions in Section 3(a), if You Share
286 | Adapted Material You produce, the following conditions also apply.
287 |
288 | 1. The Adapter's License You apply must be a Creative Commons
289 | license with the same License Elements, this version or
290 | later, or a BY-NC-SA Compatible License.
291 |
292 | 2. You must include the text of, or the URI or hyperlink to, the
293 | Adapter's License You apply. You may satisfy this condition
294 | in any reasonable manner based on the medium, means, and
295 | context in which You Share Adapted Material.
296 |
297 | 3. You may not offer or impose any additional or different terms
298 | or conditions on, or apply any Effective Technological
299 | Measures to, Adapted Material that restrict exercise of the
300 | rights granted under the Adapter's License You apply.
301 |
302 |
303 | Section 4 -- Sui Generis Database Rights.
304 |
305 | Where the Licensed Rights include Sui Generis Database Rights that
306 | apply to Your use of the Licensed Material:
307 |
308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right
309 | to extract, reuse, reproduce, and Share all or a substantial
310 | portion of the contents of the database for NonCommercial purposes
311 | only;
312 |
313 | b. if You include all or a substantial portion of the database
314 | contents in a database in which You have Sui Generis Database
315 | Rights, then the database in which You have Sui Generis Database
316 | Rights (but not its individual contents) is Adapted Material,
317 | including for purposes of Section 3(b); and
318 |
319 | c. You must comply with the conditions in Section 3(a) if You Share
320 | all or a substantial portion of the contents of the database.
321 |
322 | For the avoidance of doubt, this Section 4 supplements and does not
323 | replace Your obligations under this Public License where the Licensed
324 | Rights include other Copyright and Similar Rights.
325 |
326 |
327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability.
328 |
329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
339 |
340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
349 |
350 | c. The disclaimer of warranties and limitation of liability provided
351 | above shall be interpreted in a manner that, to the extent
352 | possible, most closely approximates an absolute disclaimer and
353 | waiver of all liability.
354 |
355 |
356 | Section 6 -- Term and Termination.
357 |
358 | a. This Public License applies for the term of the Copyright and
359 | Similar Rights licensed here. However, if You fail to comply with
360 | this Public License, then Your rights under this Public License
361 | terminate automatically.
362 |
363 | b. Where Your right to use the Licensed Material has terminated under
364 | Section 6(a), it reinstates:
365 |
366 | 1. automatically as of the date the violation is cured, provided
367 | it is cured within 30 days of Your discovery of the
368 | violation; or
369 |
370 | 2. upon express reinstatement by the Licensor.
371 |
372 | For the avoidance of doubt, this Section 6(b) does not affect any
373 | right the Licensor may have to seek remedies for Your violations
374 | of this Public License.
375 |
376 | c. For the avoidance of doubt, the Licensor may also offer the
377 | Licensed Material under separate terms or conditions or stop
378 | distributing the Licensed Material at any time; however, doing so
379 | will not terminate this Public License.
380 |
381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
382 | License.
383 |
384 |
385 | Section 7 -- Other Terms and Conditions.
386 |
387 | a. The Licensor shall not be bound by any additional or different
388 | terms or conditions communicated by You unless expressly agreed.
389 |
390 | b. Any arrangements, understandings, or agreements regarding the
391 | Licensed Material not stated herein are separate from and
392 | independent of the terms and conditions of this Public License.
393 |
394 |
395 | Section 8 -- Interpretation.
396 |
397 | a. For the avoidance of doubt, this Public License does not, and
398 | shall not be interpreted to, reduce, limit, restrict, or impose
399 | conditions on any use of the Licensed Material that could lawfully
400 | be made without permission under this Public License.
401 |
402 | b. To the extent possible, if any provision of this Public License is
403 | deemed unenforceable, it shall be automatically reformed to the
404 | minimum extent necessary to make it enforceable. If the provision
405 | cannot be reformed, it shall be severed from this Public License
406 | without affecting the enforceability of the remaining terms and
407 | conditions.
408 |
409 | c. No term or condition of this Public License will be waived and no
410 | failure to comply consented to unless expressly agreed to by the
411 | Licensor.
412 |
413 | d. Nothing in this Public License constitutes or may be interpreted
414 | as a limitation upon, or waiver of, any privileges and immunities
415 | that apply to the Licensor or You, including from the legal
416 | processes of any jurisdiction or authority.
417 |
418 | =======================================================================
419 |
420 | Creative Commons is not a party to its public
421 | licenses. Notwithstanding, Creative Commons may elect to apply one of
422 | its public licenses to material it publishes and in those instances
423 | will be considered the “Licensor.” The text of the Creative Commons
424 | public licenses is dedicated to the public domain under the CC0 Public
425 | Domain Dedication. Except for the limited purpose of indicating that
426 | material is shared under a Creative Commons public license or as
427 | otherwise permitted by the Creative Commons policies published at
428 | creativecommons.org/policies, Creative Commons does not authorize the
429 | use of the trademark "Creative Commons" or any other trademark or logo
430 | of Creative Commons without its prior written consent including,
431 | without limitation, in connection with any unauthorized modifications
432 | to any of its public licenses or any other arrangements,
433 | understandings, or agreements concerning use of licensed material. For
434 | the avoidance of doubt, this paragraph does not form part of the
435 | public licenses.
436 |
437 | Creative Commons may be contacted at creativecommons.org.
438 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Полностековый GraphQL (русский перевод)
2 |
3 | Книга о фулстек-разработке GraphQL с помощью React и Node.
4 |
5 | https://graphql.college/fullstack-graphql.
6 |
7 |
8 |
9 |
10 |
11 | ## Структура
12 |
13 | Каждая глава написана в файлах формата Markdown в каталоге `manuscript`.
14 |
15 | Каждое изображение книги находится в `manuscript/images`.
16 |
17 | Порядок глав определяется файлом `Book.txt`.
18 |
19 | `Sample.txt` — содержимое для образца главы книги.
20 |
21 | ## Участие в проекте
22 |
23 | Все типы участия приветствуются! Вот небольшой указатель, как можно внести свой вклад.
24 |
25 | ### Баги/опечатки
26 |
27 | Если вы увидели баг или опечатку, пожалуйста, откройте ишью в этом репозитории на GitHub.
28 |
29 | ### Исправления
30 |
31 | Для исправления примера, пожалуйста, пересоздайте код примера в Glitch и создайте пулреквест, ссылающиеся на исправленный URL-адрес в Glitch.
32 |
33 | ### Новые главы
34 |
35 | Пожалуйста, создайте ишью для предложения новых глав. Предложения новых глав всегда приветствуются! Но, пожалуйста, не создавайте пулреквест с новой главой до её обсуждения, поскольку она может быть не принята. Время — золото, используйте его с умом!
36 |
37 | ### Вопросы
38 |
39 | Если у вас есть какие-либо вопросы относительно книги или GraphQL в целом, пожалуйста, создайте переписку в https://spectrum.chat/graphql вместо ишью на GitHub.
40 |
41 | ## Лицензия
42 |
43 | 
44 | 
45 | 
46 | 
47 |
48 | Данная книга распространяется на условиях лицензии [Creative Commons Attribution Non Commercial Share Alike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/).
49 |
--------------------------------------------------------------------------------
/manuscript/Book.txt:
--------------------------------------------------------------------------------
1 | chapter-0.md
2 | chapter-1.md
3 | chapter-2.md
4 | chapter-3.md
5 | chapter-4.md
6 | chapter-5.md
7 | chapter-6.md
--------------------------------------------------------------------------------
/manuscript/Sample.txt:
--------------------------------------------------------------------------------
1 | chapter-0.md
2 | chapter-1.md
--------------------------------------------------------------------------------
/manuscript/chapter-0.md:
--------------------------------------------------------------------------------
1 | # Предисловие
2 |
3 | GraphQL — революционное решение в клиент-серверном взаимодействии. Эта технология позволяет лучше документировать API, облегчает управление данными в HTTP-клиентах и оптимизирует использование сети.
4 |
5 | Одно из основных преимуществ GraphQL — улучшение контакта между API и его пользователями. Кроме этого, технология позволяет улучшить работу в команде разработчиков, предоставляя им информацию о методах API. Он также обеспечивает лучшее взаимодействие со сторонними пользователями API, поскольку сервисам GraphQL не требуется конфигурация для API-документации.
6 |
7 | Учитывая вышесказанное, GraphQL расширяет возможности клиентов, давая им полный контроль над получением данных. GraphQL позволяет клиентам запрашивать только те данные, которые им нужны, — ни больше ни меньше. Технология также предоставляет возможность запрашивать и вложенные данные, используя один запрос и избегая нескольких простых запросов, как это принято в RESTful API. REST порождает тенденцию к сложности использования API для клиентов.
8 |
9 | Как следствие, GraphQL оптимизирует использование сети за счёт уменьшения отправляемых данных и количества HTTP-запросов. Данное преимущество особенно заметно на мобильных клиентах, где интернет-трафик играет важную роль.
10 |
11 | ## Итак, что же такое GraphQL?
12 |
13 | GraphQL — типизированный предметно-ориентированный язык для проектирования и запросов данных.
14 |
15 | Предметно-ориентированный язык или DSL — это язык, созданный для единственной предметной области приложения; он противоположен языкам общего назначения, таким как JavaScript, Ruby, Python или C, которые применяются для решения самых разных задач. В настоящее время существует множество популярных DSL: CSS — это DSL для оформления (стилизации), а HTML — это DSL для разметки. И вот GraphQL — это DSL для данных.
16 |
17 | GraphQL — типизированный язык, а это значит, что он использует типы для определения ресурсов, добавляет типы в поля каждого ресурса. Он также использует типы для статической проверки ошибок. Как упоминалось, это типизированный язык, поэтому он является источником самых больших достоинств GraphQL, таких как автоматическая интроспекция API и документация.
18 |
19 | Предметная область GraphQL — это данные. Он может использоваться для разработки схемы, представляющей данные, а также для запроса определённых полей данных.
20 |
21 | Разработка серверов и клиентов API — основной вариант использования GraphQL. Бэкенд-разработчики могут использовать GraphQL для моделирования своих данных, в то время как фронтенд-разработчики могут использовать GraphQL для формулирования запросов на получения определённых частей данных.
22 |
23 | Даже несмотря на то, что сервисы обычно включают GraphQL через HTTP-уровень, тем не менее GraphQL не привязан строго к HTTP или какому-либо другому протоколу передачи данных.
24 |
25 | В конце концов, GraphQL — это спецификация. Это означает, как он должен работать, позволяя любому реализовать его на любом языке программирования. Существует официальная реализация на JavaScript под названием `graphql-js`, но есть также множество других реализаций на разных языках программирования, таких как Ruby, Elixir и т.д.
26 |
27 | ## Организация книги
28 |
29 | В этой книге вы узнаете, как с нуля разработать готовое клиент-серверное приложение, использующее GraphQL. Вы научитесь получать данные от клиента, проектировать эти данные на сервере, разрабатывать GraphQL-серверы на Node.js и, наконец, как создавать клиенты на React с использованием GraphQL.
30 |
31 | Первые две главы покажут, как получать данные с помощью GraphQL. В первой главе вы познакомитесь с тем, как создавать данные. Во второй главе изучите, как проектировать схемы. Вы узнаете эти чистые концепции GraphQL, не задумываясь про серверы или клиенты HTTP. GraphQL — это абстракция, позволяющая думать о данных, не беспокоясь о том, как они будут передаваться. Вы создадите схему, запросы и мутации с помощью JavaScript и нескольких библиотек GraphQL.
32 |
33 | Остальные главы будут посвящены созданию серверов и клиентов GraphQL.
34 |
35 | В третьей главе, API GraphQL, вы узнаете, как создавать HTTP-серверы GraphQL с использованием серверов Node.js и Apollo. Вы сделаете доступной GraphQL-схему через HTTP, подключитесь к базе данных, реализуете аутентификацию и авторизацию, а также узнаете, как лучше организовать файловую структуру.
36 |
37 | В четвёртой главе, Клиенты GraphQL, вы научитесь разрабатывать GraphQL-клиенты с помощью клиента React и Apollo. Вы узнаете, как запрашивать и создавать данные с использованием Apollo-компонентов `Query` и `Mutation`, а также как обрабатывать аутентификацию.
38 |
39 | В пятой главе вы освоите добавление функциональности, работающей в режиме реального времени к вашим приложениям GraphQL с помощью подписок. Подписки предоставляют API-интерфейсам GraphQL возможность передавать данные клиентам.
40 |
41 | Наконец, в шестой главе вы познакомитесь с тестированием API и GraphQL-клиентов.
42 |
43 | ## Пример приложения
44 |
45 | В ходе этой книги вы узнаете, как создать клон сервииса Pinterest под названием Pinapp с использованием GraphQL, Node.js, React и Apollo-клиента.
46 |
47 | Для Pinapp определены следующие требования:
48 |
49 | * Вход с помощью магических ссылок
50 | * Выход из приложения
51 | * Добавление пина (изображения со ссылкой на URL-адрес)
52 | * Возможность поиска пинов и пользователей
53 | * Просмотр списка пинов
54 | * Просмотр новых пинов без обновления браузера
55 |
56 | Вы создадите это приложение слоями. Начнёте со слоя данных, затем напишите бизнес-логику, после чего перейдёте к транспортному уровню HTTP, а затем соедините всё это с уровнем базы данных и, наконец, реализуете HTTP-клиент.
57 |
58 | ## Окружение для разработки
59 |
60 | Нет никаких требований к окружению, чтобы посмотреть вживую примеры из этой книги, кроме наличия браузера и подключения к интернету. На каждом шаге приложения есть работающая и доступна для клонирования онлайн-версия на glitch.com. Glitch — потрясающий сервис для создания приложений, созданное людьми, которые разработали Stack Overflow и Trello.
61 |
--------------------------------------------------------------------------------
/manuscript/chapter-1.md:
--------------------------------------------------------------------------------
1 | # 1. Чтение и запись
2 |
3 | В этой главе вы узнаете, как использовать GraphQL с точки зрения разработчика. Объясняется, как использовать запросы[^queries_note] и мутации для чтения и записи данных сервера GraphQL.
4 |
5 | 
6 |
7 | По мере изучения возможностей GraphQL, вы поймете, что это та технология, которая делает жизнь намного проще для фронтенд-разработчиков. GraphQL предоставляет полный контроль над данными, которые можно получить с сервера.
8 |
9 | Сделать жизнь проще для клиента — это одна из главных задач создателей GraphQL. Результат эволюции языка демонстрирует следующий доклад: [Client-Driven Development](https://youtu.be/vQkGO5q52uE).
10 |
11 | ## 1.1 Запросы и мутации
12 |
13 | В самом простом виде GraphQL предоставляет возможность запрашивать набор полей (fields).
14 |
15 | Сам язык GraphQL определяет, как именно взаимодействовать с сервером. Запросы, в эталонном их виде, служат для получения данных, в то время как мутации требуются для того, чтобы эти данные записывать. Запросы служат той же цели, что и GET-запросам в старом добром REST, а мутации аналогичны методам POST, PUT, PATCH и DELETE.
16 |
17 | В этой главе вы узнаете о следующих возможностях синтаксиса GraphQL:
18 |
19 | * Простой запрос
20 | * Запрос вложенных полей
21 | * Запрос нескольких полей
22 | * Наименование операции
23 | * Аргументы
24 | * Псевдонимы
25 | * Фрагменты
26 | * Переменные
27 | * Директивы
28 | * Переменные по умолчанию
29 | * Мутации
30 | * Встроенные фрагменты
31 | * Метаинформация
32 |
33 | Все концепции, о которых вы узнаете, имеют примеры, реализованные с помощью [`graphql-js`](https://github.com/graphql/graphql-js). GraphQL JS — эталонная реализация GraphQL, написанная на JavaScript. Сама библиотека предоставляет функцию `graphql, позволяющая передать запрос в схему GraphQL.
34 |
35 | Примеры в этой главе уже содержат базовую реализацию схемы GraphQL. Не волнуйтесь, если вы еще не понимаете, как всё это работает. В этой главе мы сосредоточимся на самих GraphQL-запросах, в то время как следующая будет посвящена созданию схемы. Обратите внимание, что приведённая схема содержит очень простую реализацию, поэтому не ожидайте большего, кроме случайных чисел или классический «Hello world» в качестве ответов сервера. Следующая глава научит вас правильно проектировать эту схему.
36 |
37 | Несмотря на то, что GraphQL предназначен для использования HTTP-клиентами и работает с HTTP-сервером, выполнение запросов с использованием JavaScript-консоли поможет понять основы языка без каких-либо накладных расходов, т.е. без настройки сервера и прочего.
38 |
39 | Му воспользуемся функцией `graphql`, которая экспортируется из `graphql-js`. В простейшем виде она принимает два аргумента и возвращает объект Promise. Первый аргумент — это объект схемы GraphQL. Второй аргумент — это строка, содержащая запрос GraphQL. Все примеры в этой главе расскажут вам, как написать этот запрос. Если же вы хотите узнать чуть больше, то всегда можно воспользоваться [документацией по API `graphql-js`](http://graphql.org/graphql-js/graphql/#graphql).
40 |
41 | ```js
42 | const { graphql } = require("graphql");
43 |
44 | const schema = require("../schema");
45 |
46 | const query = ``;
47 |
48 | graphql(schema, query).then(result =>
49 | console.log(JSON.stringify(result, null, 1))
50 | );
51 | ```
52 |
53 | Воспользуйтесь [копированием этого примера в сервис Glitch](https://glitch.com/edit/#!/remix/pinapp-queries-mutations), чтобы получить возможность сразу же запустить и посмотреть в действии код, показанный в этой главе. Это даст вам полную свободу над проектом. Вы можете модифицировать его по своему усмотрению, запускать скрипты с помощью консоли и даже экспортировать его в GitHub.
54 |
55 | После копирования этого проекта вы можете запустить любой из скриптов в папке `queries`. Далее откройте консоль, нажав «Logs», а затем «Console», и выполните `node query/1-query.js` для просмотра выходных данных первого скрипта.
56 |
57 | Теперь у вас есть все необходимое, чтобы начать изучение синтаксиса GraphQL. Давайте уже приступим к отправке основных запросов.
58 |
59 | You have everything you need to start learning GraphQL query syntax. Let's start by sending basic queries.
60 |
61 | Как мы уже говорили в начале этой главы, GraphQL предоставляет возможность запрашивать конкретные поля объектов. Запрос определяет, какие именно поля будет иметь ответ GraphQL в формате JSON[^chapter1-json]. Синтаксис языка запросов очень похож на формат объектов в JSON с перечислением ключей без их значений. Например, если вы хотите получить список пользователей, каждый из которых имеет поле электронной почты, вы можете написать следующий запрос:
62 |
63 | [^chapter1-json]: GraphQL не накладывает никаких ограничений на формат ответов сервера, однако JSON является самым популярным и довольно читаемым вариантом.
64 |
65 | ```graphql
66 | {
67 | users {
68 | email
69 | }
70 | }
71 | ```
72 |
73 | Вы можете передать этот запрос вместе со схемой в функцию `graphql`. Помните, что именно второй аргумент, передаваемый в функцию `graphql`, является строкой запроса. Давайте посмотрим пример скрипта.
74 |
75 | `queries/1-query.js`
76 |
77 | ```js
78 | const { graphql } = require("graphql");
79 |
80 | const schema = require("../schema");
81 |
82 | const query = `
83 | {
84 | users {
85 | email
86 | }
87 | }
88 | `;
89 |
90 | graphql(schema, query).then(result =>
91 | console.log(JSON.stringify(result, null, 1))
92 | );
93 | ```
94 |
95 | Запуск приведённого выше кода в консоли вернёт ответ с корневым ключом `"data"`, в котором есть все запрошенные в запросе поля. Внутри поля `"data"` вы увидите структуру, которая точно соответствует отправленному запросу: вы увидите ключ `"users"`, который в свою очередь содержит массив объектов с ключом `"email"`.
96 |
97 | ```bash
98 | $ node queries/1-query.js
99 | {
100 | "data": {
101 | "users": [
102 | {
103 | "email": "Hello World"
104 | },
105 | {
106 | "email": "Hello World"
107 | }
108 | ]
109 | }
110 | }
111 | ```
112 |
113 | ## 1.3 Вложенные поля
114 |
115 | Используя GraphQL можно запросить вложенные поля. Одно из больших преимуществ GraphQL по сравнению с REST — это выборка вложенных ресурсов в одном запросе. Вы можете запросить ресурс, например пользователей, а также список вложенных ресурсов, например, пинов[^pins], в одном запросе. Чтобы сделать это с помощью REST, вам потребуется запрашивать пользователей и пины в отдельных HTTP-запросах.
116 |
117 | [^pins]: Под _пином_, как правило, подразумевается изображение, у которого необязательно может быть название, описание, ссылка (источник изображения). Название пришло из сервиса Pinterest, в котором пользователь может создавать пины.
118 |
119 | Обратите внимание, что только поля с типом `Object` могут иметь вложенные поля. Вы не можете запрашивать вложенные поля из других типов, таких как `String`, `Int` и др.
120 |
121 | ```js
122 | const { graphql } = require("graphql");
123 |
124 | const schema = require("../schema");
125 |
126 | const query = `
127 | {
128 | users {
129 | email
130 | pins {
131 | title
132 | }
133 | }
134 | }
135 | `;
136 |
137 | graphql(schema, query).then(result =>
138 | console.log(JSON.stringify(result, null, 1))
139 | );
140 | ```
141 |
142 | Приведенный выше пример показывает, насколько легко получить вложенные ресурсы. Как вы понимаете, выполнение предыдущего примера вернет JSON-объект с точно такими же ключами, которые были указаны в запросе. Убедитесь в этом сами, запустив команду `node query/2-fields.js` в консоли из-под директории проекта.
143 |
144 | ```bash
145 | $ node queries/2-fields.js
146 | {
147 | "data": {
148 | "users": [
149 | {
150 | "email": "Hello World",
151 | "pins": [
152 | {
153 | "title": "Hello World"
154 | },
155 | {
156 | "title": "Hello World"
157 | }
158 | ]
159 | },
160 | {
161 | "email": "Hello World",
162 | "pins": [
163 | {
164 | "title": "Hello World"
165 | },
166 | {
167 | "title": "Hello World"
168 | }
169 | ]
170 | }
171 | ]
172 | }
173 | }
174 | ```
175 |
176 | ## 1.4 Несколько полей
177 |
178 | GraphQL позволяет получить несколько полей в одном запросе. В предыдущем примере вы видели, что можно запросить вложенные ресурсы, но также возможно запросить совершенно не связанные с друг другом ресурсы в одной и той же операции.
179 |
180 | ```js
181 | const { graphql } = require("graphql");
182 |
183 | const schema = require("../schema");
184 |
185 | const query = `
186 | {
187 | users {
188 | email
189 | }
190 | pins {
191 | title
192 | }
193 | }
194 | `;
195 |
196 | graphql(schema, query).then(result =>
197 | console.log(JSON.stringify(result, null, 1))
198 | );
199 | ```
200 |
201 | Теперь вы понимаете, что GraphQL-запросы на самом деле представляют из себя получение конкретных полей объектов. Если вы выполните `node queries/3-multiple-fields.js`, то получите объект с двумя ключами: `users` и `pins`.
202 |
203 | ```bash
204 | $ node queries/3-multiple-fields.js
205 | {
206 | "data": {
207 | "users": [
208 | {
209 | "email": "Hello World"
210 | },
211 | {
212 | "email": "Hello World"
213 | }
214 | ],
215 | "pins": [
216 | {
217 | "title": "Hello World"
218 | },
219 | {
220 | "title": "Hello World"
221 | }
222 | ]
223 | }
224 | }
225 | ```
226 |
227 | ## 1.5 Название операции
228 |
229 | До сих пор мы использовали сокращенный синтаксис запросов GraphQL, хотя вместе с ним есть более длинный синтаксис, который предоставляет больше возможностей. Более длинный синтаксис включает ключевое слово `query` и имя операции. Неоднократно, если не всегда, вы будете использовать именно такой синтаксис, поскольку он позволяет указывать переменные или использовать различные операции, такие как мутации или подписки, которые мы рассмотрим в оставшейся части книги.
230 |
231 | Вот как выглядит запрос с именем операции `GetUsers`:
232 |
233 | ```js
234 | const { graphql } = require("graphql");
235 |
236 | const schema = require("../schema");
237 |
238 | const query = `
239 | query GetUsers {
240 | users {
241 | email
242 | pins {
243 | title
244 | }
245 | }
246 | }
247 | `;
248 |
249 | graphql(schema, query).then(result =>
250 | console.log(JSON.stringify(result, null, 1))
251 | );
252 | ```
253 |
254 | Вы можете запустить предыдущий запрос, набрав в консоли `node queries/4-operation-name.js`. Обратите внимание, что приведенная выше версия запроса работает аналогично, как и сокращенная версия.
255 |
256 | ```bash
257 | $ node queries/4-operation-name.js
258 | {
259 | "data": {
260 | "users": [
261 | {
262 | "email": "Hello World",
263 | "pins": [
264 | {
265 | "title": "Hello World"
266 | },
267 | {
268 | "title": "Hello World"
269 | }
270 | ]
271 | },
272 | {
273 | "email": "Hello World",
274 | "pins": [
275 | {
276 | "title": "Hello World"
277 | },
278 | {
279 | "title": "Hello World"
280 | }
281 | ]
282 | }
283 | ]
284 | }
285 | }
286 | ```
287 |
288 | ## 1.6 Аргументы
289 |
290 | У всех полей могут быть аргументы, которые можно использовать аналогично, как и аргументы функции. Вы можете рассматривать GraphQL-поля как функции, а не свойства. Представление их в виде функций дает более четкое представление о том, что вы можете сделать с полями, передав им аргументы.
291 |
292 | Давайте предположим, например, что вы хотите запросить пин по идентификатору, запросив поле `pinById`. Можно получить пин с идентификатором `1`, передав в запрос соответствующий именованный аргумент подобным образом:
293 |
294 | ```js
295 | const { graphql } = require("graphql");
296 |
297 | const schema = require("../schema");
298 |
299 | const query = `
300 | query {
301 | pinById(id: "1") {
302 | title
303 | }
304 | }
305 | `;
306 |
307 | graphql(schema, query).then(result =>
308 | console.log(JSON.stringify(result, null, 1))
309 | );
310 | ```
311 |
312 | Выполнение `node queries/5-arguments.js` в консоли приведёт к следующему выводу:
313 |
314 | ```bash
315 | $ node queries/5-arguments.js
316 | {
317 | "data": {
318 | "pinById": {
319 | "title": "Hello World"
320 | }
321 | }
322 | }
323 | ```
324 |
325 | ## 1.7 Псевдонимы
326 |
327 | Что произойдет, если вы захотите запросить одно и то же поле дважды в одном запросе? Ну... вы можете достичь этого с помощью псевдонимов. Псевдонимы позволяют связать имя с полем, так что в ответе будет указан псевдоним вместо имя ключа.
328 |
329 | Псевдоним поля — это также просто, как и добавление имени поля с желаемым псевдонимом и двоеточием (:).
330 |
331 | Псевдонимы особенно полезны при запросе одного и того же поля, но с разными аргументами. Следующий запрос дважды запрашивает поле `pinById`, используя псевдонимы: первое поле будет иметь псевдоним `firstPin`, а второе поле — `secondPin`.
332 |
333 | ```js
334 | const { graphql } = require("graphql");
335 |
336 | const schema = require("../schema");
337 |
338 | const query = `
339 | query {
340 | firstPin: pinById(id: "1") {
341 | title
342 | }
343 | secondPin: pinById(id: "2") {
344 | title
345 | }
346 | }
347 | `;
348 |
349 | graphql(schema, query).then(result =>
350 | console.log(JSON.stringify(result, null, 1))
351 | );
352 | ```
353 |
354 | Как вы видите, ответ содержит псевдонимы вместо имени поля. Проверьте сами, выполнив команду `node queries/6-aliases.js`.
355 |
356 | ```bash
357 | $ node queries/6-aliases.js
358 | {
359 | "data": {
360 | "firstPin": {
361 | "title": "Hello World"
362 | },
363 | "secondPin": {
364 | "title": "Hello World"
365 | }
366 | }
367 | }
368 | ```
369 |
370 | ## 1.8 Фрагменты
371 |
372 | Синтаксис GraphQL предоставляет возможность многократного использования набора полей с использованием ключевого поля `fragment`. Это язык, предназначенный для запросов полей, поэтому вполне естественно ожидать вариант для многократного использования полей в разных частях запроса.
373 |
374 | Для повторного использования поля, сначала нужно определить фрагмент, а затем поместить фрагмент в разные части запроса.
375 |
376 | Определите фрагменты, используя следующий синтаксис: `fragment [fragmentName] on [Type] { field anotherField }`. Используйте фрагменты, поместив `...[fragmentName]` в то место запроса, где вы указали бы поле.
377 |
378 | Любой примере лучше всего продемонстрирует какую-либо возможность. В следующем примере определяется фрагмент с именем `pinFields`, а затем используется дважды в запросе.
379 |
380 | ```js
381 | const { graphql } = require("graphql");
382 |
383 | const schema = require("../schema");
384 |
385 | const query = `
386 | query {
387 | pins {
388 | ...pinFields
389 | }
390 | users {
391 | email
392 | pins {
393 | ...pinFields
394 | }
395 | }
396 | }
397 | fragment pinFields on Pin {
398 | title
399 | }
400 | `;
401 |
402 | graphql(schema, query).then(result =>
403 | console.log(JSON.stringify(result, null, 1))
404 | );
405 | ```
406 |
407 | Выполните запрос выше с помощью команды `node queries/7-fragments.js`. Попробуйте поиграть с этим фрагментом, изменив список запрашиваемых полей, и посмотрите, как измениться вывод скрипта.
408 |
409 | ```bash
410 | $ node queries/7-fragments.js
411 | {
412 | "data": {
413 | "pins": [
414 | {
415 | "title": "Hello World"
416 | },
417 | {
418 | "title": "Hello World"
419 | }
420 | ],
421 | "users": [
422 | {
423 | "email": "Hello World",
424 | "pins": [
425 | {
426 | "title": "Hello World"
427 | },
428 | {
429 | "title": "Hello World"
430 | }
431 | ]
432 | },
433 | {
434 | "email": "Hello World",
435 | "pins": [
436 | {
437 | "title": "Hello World"
438 | },
439 | {
440 | "title": "Hello World"
441 | }
442 | ]
443 | }
444 | ]
445 | }
446 | }
447 | ```
448 |
449 | ## 1.9 Переменные
450 |
451 | Так же, как фрагменты позволяют вам повторно использовать наборы полей, переменные дают повторно использовать запросы. Используя переменные, можно указать, какие части запроса являются настраиваемыми, так что вы можете использовать запрос несколько раз, изменяя значения переменных. Используя переменные, можно создавать динамические запросы.
452 |
453 | Давайте посмотрим, как вы можете добавить переменные в пример запроса пинов по идентификатору. Можно определить переменную с именем `$id`, указать ее тип `String` и пометить ее как обязательную, поставив восклицательный знак (`!`) после названия типа.
454 |
455 | Следующий фрагмент определяет переменную `$id` в своем запросе и отправляет ее вместе со схемой и списком переменных в `graphql`. Функция `graphql` получает список переменных в качестве пятого аргумента.
456 |
457 | ```js
458 | const { graphql } = require("graphql");
459 |
460 | const schema = require("../schema");
461 |
462 | const query = `
463 | query ($id: String!) {
464 | pinById(id: $id) {
465 | title
466 | }
467 | }
468 | `;
469 |
470 | graphql(schema, query, undefined, undefined, {
471 | id: "1"
472 | }).then(result =>
473 | console.log(JSON.stringify(result, null, 1))
474 | );
475 | ```
476 |
477 | Результат выполнения `node query / 8-variables.js` довольно ясен.
478 |
479 | ```bash
480 | $ node queries/8-variables.js
481 | {
482 | "data": {
483 | "pinById": {
484 | "title": "Hello World"
485 | }
486 | }
487 | }
488 | ```
489 |
490 | ## 1.10 Директивы
491 |
492 | По аналогии как переменные дают возможность создавать динамические запросы путем изменения аргументов, директивы позволяют конструировать динамические запросы, которые изменяют структуру и форму их результата.
493 |
494 | Директивы возможно прикрепить к полям или фрагментам. Все директивы начинаются с символа `@`.
495 |
496 | GraphQL-серверы могут предоставлять любое количество директив, которое они пожелают, однако в спецификации GraphQL определены две обязательные директивы: `@include(if: Boolean)` и `@skip(if: Boolean)`. Первая директива включает поле только если `if` равно true, а вторая, напротив, пропускает поле, когда `if` равно true.
497 |
498 | Следующий пример показывает директивы в действии. Директива `@include` помещается в поле `pins` и параметризует значение, используя переменную с именем `$withPins`.
499 |
500 | ```js
501 | const { graphql } = require("graphql");
502 |
503 | const schema = require("../schema");
504 |
505 | const query = `
506 | query ($withPins: Boolean!) {
507 | users {
508 | email
509 | pins @include(if: $withPins) {
510 | title
511 | }
512 | }
513 | }
514 | `;
515 |
516 | graphql(schema, query, undefined, undefined, {
517 | withPins: true
518 | }).then(result =>
519 | console.log(JSON.stringify(result, null, 1))
520 | );
521 | ```
522 |
523 | Двигайтесь дальше, выполните предыдущий пример через команду `node queries/9-directives.js`. Измените `withPins` на false, что увидеть, как изменится структура результата.
524 |
525 | ```bash
526 | $ node queries/9-directives.js
527 | {
528 | "data": {
529 | "users": [
530 | {
531 | "email": "Hello World",
532 | "pins": [
533 | {
534 | "title": "Hello World"
535 | },
536 | {
537 | "title": "Hello World"
538 | }
539 | ]
540 | },
541 | {
542 | "email": "Hello World",
543 | "pins": [
544 | {
545 | "title": "Hello World"
546 | },
547 | {
548 | "title": "Hello World"
549 | }
550 | ]
551 | }
552 | ]
553 | }
554 | }
555 | ```
556 |
557 | ## 1.11 Переменные по умолчанию
558 |
559 | Синтаксис GraphQL даёт определять значения по умолчанию для переменных. Это возможно, если добавить знак равенства (=) после типа переменной.
560 |
561 | Давайте рассмотрим пример, добавив параметр по умолчанию `true` к предыдущему примеру с директивами. Значение по умолчанию для переменной позволяет вызвать функцию `graphql` в примере, не указывая значение для `withPins` в аргументе списка переменных.
562 |
563 | ```js
564 | const { graphql } = require("graphql");
565 |
566 | const schema = require("../schema");
567 |
568 | const query = `
569 | query ($withPins: Boolean = true) {
570 | users {
571 | email
572 | pins @include(if: $withPins) {
573 | title
574 | }
575 | }
576 | }
577 | `;
578 |
579 | graphql(schema, query).then(result =>
580 | console.log(JSON.stringify(result, null, 1))
581 | );
582 | ```
583 |
584 | Выполните в консоли: `node queries/10-default-variables.js`. Обратите внимание, вывод выглядит точно так же, как и вызов `graphql` со значением `withPins`, равным `true`.
585 |
586 | ```bash
587 | $ node queries/10-default-variables.js
588 | {
589 | "data": {
590 | "users": [
591 | {
592 | "email": "Hello World",
593 | "pins": [
594 | {
595 | "title": "Hello World"
596 | },
597 | {
598 | "title": "Hello World"
599 | }
600 | ]
601 | },
602 | {
603 | "email": "Hello World",
604 | "pins": [
605 | {
606 | "title": "Hello World"
607 | },
608 | {
609 | "title": "Hello World"
610 | }
611 | ]
612 | }
613 | ]
614 | }
615 | }
616 | ```
617 |
618 | ## 1.12 Встроенные фрагменты
619 |
620 | Встроенные фрагменты предусматривают способ указать список встроенных полей. В отличие от обычных фрагментов, которые должны быть определены с помощью ключевого слова `fragment`, встроенные фрагменты не нужно где-либо определять.
621 |
622 | Эти типы фрагментов полезны при запросе полей с типом `Union` или `Interface`. Эти поля могут возвращать объекты с различными полями, в зависимости от типа объекта. Вы можете использовать фрагменты, чтобы указать, какие поля возвращать, основываясь на типе объекта.
623 |
624 | Отличный случай использования встроенных фрагментов — поисковый запрос, который может возвращать объекты разных типов. В следующем фрагменте показано, как можно использовать встроенные фрагменты, чтобы получить разный набор полей из запроса `search`. Если возвращаемый объект — это `Person`, значит нужно вернуть его `email`, а если этот объект — `Pin`, то вернуть его `title`.
625 |
626 | ```js
627 | const { graphql } = require("graphql");
628 |
629 | const schema = require("../schema");
630 |
631 | const query = `
632 | query ($text: String!) {
633 | search(text: $text) {
634 | ... on Person {
635 | email
636 | }
637 | ... on Pin {
638 | title
639 | }
640 | }
641 | }
642 | `;
643 |
644 | graphql(schema, query, undefined, undefined, {
645 | text: "Hello world"
646 | }).then(result =>
647 | console.log(JSON.stringify(result, null, 1))
648 | );
649 | ```
650 |
651 | Запустите этот пример, используя `node queries/11-inline-fragments.js`.
652 |
653 | ```bash
654 | $ node queries/11-inline-fragments.js
655 | {
656 | "data": {
657 | "search": [
658 | {
659 | "title": "Hello World"
660 | },
661 | {
662 | "email": "Hello World"
663 | }
664 | ]
665 | }
666 | }
667 | ```
668 |
669 | ## 1.13 Метаполя
670 |
671 | Запросы могут запрашивать метаполя, представляющие собой специальные поля, которые содержат информацию о схеме.
672 |
673 | GraphQL позволяет получать имена типов объектов, запрашивая метаполе с именем `__typename`.
674 |
675 | Данное метаполе полезно в тех же ситуациях, где удобны встроенные фрагменты, то есть в запросах, которые могут возвращать несколько типов полей, таких как `Union` или `Interface`.
676 |
677 | Следующий фрагмент кода добавляет поле `__typename` к примеру запроса `search` из объяснения встроенных фрагментов.
678 |
679 | ```js
680 | const { graphql } = require("graphql");
681 |
682 | const schema = require("../schema");
683 |
684 | const query = `
685 | query ($text: String!) {
686 | search(text: $text) {
687 | __typename
688 | ... on Person {
689 | email
690 | }
691 | ... on Pin {
692 | title
693 | }
694 | }
695 | }
696 | `;
697 |
698 | graphql(schema, query, undefined, undefined, {
699 | text: "Hello world"
700 | }).then(result =>
701 | console.log(JSON.stringify(result, null, 1))
702 | );
703 | ```
704 |
705 | Запустите предыдущий скрипт, выполнив в командной строке `node queries/12-meta-fields.js`. Вы увидите, что ответ содержит поле `__typename` в каждом объекте.
706 |
707 | ```bash
708 | $ node queries/12-meta-fields.js
709 | {
710 | "data": {
711 | "search": [
712 | {
713 | "__typename": "Admin",
714 | "email": "Hello World"
715 | },
716 | {
717 | "__typename": "Pin",
718 | "title": "Hello World"
719 | }
720 | ]
721 | }
722 | }
723 | ```
724 |
725 | ## 1.14 Мутации
726 |
727 | Синтаксис GraphQL позволяет создавать данные с использованием ключевого слова `mutation`. Оно работает аналогично ключевому слову `query`. Поддерживаются переменные, поэтому возможно запросить конкретные поля в ответе, а также всё то, что мы разбирали ранее. В отличие от запросов, мутации не имеют сокращенных форм написания, так что, они всегда начинаются с ключевого слова `mutation`.
728 |
729 | Даже если мутации означают изменения данных, это всего лишь соглашение. Нет никакой гарантии, что серверы будут изменять данные внутри мутаций. Точно так же нельзя утверждать наверняка, что запросы содержат только изменения данных. Это соглашение похоже на соглашения из REST, предписывающие GET-запросам не иметь побочных эффектов, либо рекомендуют использовать POST-запросы для создания ресурсов. Данное соглашение не соблюдается тем или иным образом, однако вам следует придерживаться его, чтобы не преподносить неожиданные сюрпризы пользователям вашего API.
730 |
731 | Давайте посмотрим, как мутации работают на практике, отправив мутацию с именем `addPin`, представленную на примере схемы, которую мы использовали в этой главе.
732 |
733 | Вы заметите, что написание мутаций действительно похоже на написание запросов. Единственное отличие заключается в разном ключевом слове и, конечно, то, это операция предполагает изменение данных.
734 |
735 | ```js
736 | const { graphql } = require("graphql");
737 |
738 | const schema = require("../schema");
739 |
740 | const query = `
741 | mutation AddPin($pin: PinInput!) {
742 | addPin(pin: $pin) {
743 | id
744 | title
745 | link
746 | image
747 | }
748 | }
749 | `;
750 |
751 | graphql(schema, query, undefined, undefined, {
752 | pin: {
753 | title: "Hello world",
754 | link: "Hello world",
755 | image: "Hello world"
756 | }
757 | }).then(result =>
758 | console.log(JSON.stringify(result, null, 1))
759 | );
760 | ```
761 |
762 | Запустите этот пример мутации, набрав в консоли команду `node queries/13-mutations.js`. Помните, что наша схема работает с фиктивными данными, она не имеет реальной реализации, поэтому не ожидайте каких-либо изменений данных, вызванных данной мутацией.
763 |
764 | ```bash
765 | $ node queries/13-mutations.js
766 | {
767 | "data": {
768 | "addPin": {
769 | "id": "Hello World",
770 | "title": "Hello World",
771 | "link": "Hello World",
772 | "image": "Hello World"
773 | }
774 | }
775 | }
776 | ```
777 |
778 | Если вы запросите список пинов после последней мутации, вы заметите, что выполненная последняя мутация не создала никаких данных. Это происходит потому, что запросы в этой главе выполняются на имитированной схеме.
779 |
780 | ## 1.15 Резюме
781 |
782 | GraphQL упрощает фронтенд-разработку, предоставляя мощные возможности запросов. Это облегчает получение нескольких вложенных ресурсов в одном запросе. Извлечение минимального набора полей из ресурса также является встроенной возможностью.
783 |
784 | В следующей главе, «Моделирование данных», вы с нуля разработаете схему, которую использовали в этой главе. По сравнению с используемой схемой текущей главы, в следующей главе будет использоваться база данных в оперативной памяти для хранения схемы, и у нее не будет фиктивных значений.
785 |
786 |
787 | [^queries_note]: Термины **queries**, **mutations** и **subscriptions**, используемые в рамках этой книги, помимо основного значения являются и самостоятельными определениями, которые обозначают типы запросов на сервер: запросы, мутации и подписки, соответственно. — Здесь и далее прим. пер.
--------------------------------------------------------------------------------
/manuscript/chapter-2.md:
--------------------------------------------------------------------------------
1 | # 2. Моделирование данных
2 |
3 | В предыдущей главе вы узнали, как читать и записывать данные, отправляя запросы к схеме с использованием языка запросов GraphQL. В этой главе вы научитесь, как моделировать данные, лежащие в основе запросов, используя схемы и типы. Для создания такой схемы, вы будете использовать язык определения схемы (Schema Definition Language) GraphQL (также называемый SDL, чтобы не путать с LSD).
4 |
5 | В то время как предыдущая глава была посвящена взаимодействию клиентов с серверами, используя GraphQL, в этой главе будет рассказано, как представить модель данных, которую могут использовать клиенты.
6 |
7 | Помните клон Pinterest, о котором шла речь во введении? Изучив концепции схем и типов GraphQL, к концу главы вы разработаете модель данных.
8 |
9 | ## 2.1 Схема, типы и резолверы
10 |
11 | Серверы GraphQL предоставляют свою схему для того, чтобы клиенты знали, какие доступны запросы и мутации. Для определения того, как выглядит схема, вам нужно объявить типы всех полей. Чтобы определить, как ведет себя схема, вам нужно создать функцию, которую сервер будет запускать, когда клиент запрашивает поле, — такая функция называется резолвером. Схема нуждается как в определениях типов, так и в резолверах.
12 |
13 | 
14 |
15 | Поскольку GraphQL — это спецификация, реализованная на многих языках программирования, она предоставляет собственный язык для разработки схем, называемый SDL. Вы пишете определения типов на SDL, хотя создавать резолверы можно на любом языке, который реализует спецификацию GraphQL. Эта книга посвящена JavaScript-экосистеме GraphQL, поэтому вам предстоит все резолверы на данном языке.
16 |
17 | Схема, которую вы создадите, — это нечто большее, чем просто пример, иллюстрирующий, как писать SDL-код. Это первый шаг к разработке PinApp, пример приложения данной книги. Создаваемая схема будет отражать большинство возможностей в готовом приложении:
18 |
19 | * Авторизация с помощью магических ссылок
20 | * Возможность аутентифицированным пользователям добавлять пины
21 | * Поиск по пинам и пользователям
22 | * Список пинов
23 |
24 | Создайте собственную копию этого примера с помощью следующей ссылки:
25 |
26 | [Клонировать пример схемы](https://glitch.com/edit/#!/remix/pinapp-schema)
27 |
28 | > После клонирования внимательно следуйте инструкциям в `README.md`. Файл README этого проекта сообщает вам создать переменные окружения в специальном для этого файле `.env`.
29 |
30 | Имейте в виду, что эта схема не распространяется через протокол HTTP. Она доступна со скриптами, использующими `graphql-js`. Следующая глава покажет вам, как добавить HTTP-слой в эту схему с помощью Apollo Server.
31 |
32 | В следующем разделе вы поймете, как создавать схемы, используя функцию `makeExecutableSchema`.
33 |
34 | ## 2.2 Схемы
35 |
36 | Создание схемы происходит путём сочетания определений типов и резолверов. Существует удобный пакет [`graphql-tools`](https://github.com/apollographql/graphql-tools), предоставляющий функцию `makeExecutableSchema`. Предыдущая глава содержала много вызовов `graphql(query, schema)`. Все существующие примеры отправляют запросы согласно схемы, сгенерированной с помощью функции `makeExecutableSchema`.
37 |
38 | Откройте файл `schema.js` в примере проекта, который вы только что склонировали, чтобы узнать, как можно создать схему.
39 |
40 | ```js
41 | const { makeExecutableSchema } = require("graphql-tools");
42 | const { importSchema } = require("graphql-import");
43 |
44 | const typeDefs = importSchema("schema.graphql");
45 | const resolvers = require("./resolvers");
46 |
47 | const schema = makeExecutableSchema({
48 | typeDefs,
49 | resolvers
50 | });
51 |
52 | module.exports = schema;
53 | ```
54 |
55 | Как видите, этот файл создает схему с типами из файла `schema.graphql` и резолверами из файла `resolvers.js`. В следующих двух разделах вы научитесь, как создавать определения типов и резолверы.
56 |
57 | ## 2.3 Определения типов
58 |
59 | В этом разделе вы научитесь тому, как писать типы GraphQL, используя SDL. Тип — это просто представление объекта в вашей схеме. Объекты, как и во многих других языках программирования, могут иметь много полей.
60 |
61 | > Все примеры в этом разделе можно найти в файле `schema.graphql`
62 |
63 | Определить тип объекта можно подобным образом:
64 |
65 | ```graphql
66 | type Pin {
67 | title: String!
68 | link: String!
69 | image: String!
70 | id: String!
71 | user_id: String!
72 | }
73 | ```
74 |
75 | Как видно, можно определить тип полей после имени поля. В случае `Pin`, у всех его полей указан тип `String` и все они являются обязательными, поскольку заканчиваются восклицательным знаком (!).
76 |
77 | GraphQL определяет два специальных типа объектов — `Query` и `Mutation`. Они особенные, потому что они определяют точки входа в схему. Быть точкой входа в схему означает, что клиенты GraphQL должны начинать свои запросы с одного или нескольких полей из `Query`.
78 |
79 | ```graphql
80 | type Query {
81 | pins: [Pin]
82 | pinById(id: String!): Pin
83 | users: [User]
84 | me: User
85 | search(text: String): [SearchResult]
86 | }
87 | ```
88 |
89 | Как вы, наверное, заметили, у типов объектов могут быть аргументы. Каждое поле имеет базовую функцию (называемую резолвером), которая выполняется перед возвратом собственного значения, поэтому имеет смысл рассматривать аргументы поля по аналогии с тем, как мы размышляем над аргументами функции.
90 |
91 | Еще один новый элемент в предыдущем `Query` — это модификатор типа List. Возможно заключить поля в квадратные скобки, чтобы указать их как списки.
92 |
93 | Спецификация GraphQL определяет, что все схемы должны иметь тип `Query`, а также необязательно тип `Mutation`. Вот так выглядит тип `Mutation` в PinApp:
94 |
95 | ```graphql
96 | type Mutation {
97 | addPin(pin: PinInput!): Pin
98 | sendShortLivedToken(email: String!): Boolean
99 | createLongLivedToken(token: String!): String
100 | }
101 | ```
102 |
103 | Обратите внимание, что поле `addPin` имеет аргумент `pin` с указанным типом `PinInput`, а у двух остальных полей есть аргументы типа `String`. Вы не можете передавать аргументы типа `Object` в качестве аргументов, разрешено передавать только скалярные типы, а также типы `Input`.
104 |
105 | Скалярные типы не могут иметь вложенные поля, они представляют собой листья[^leaves] схемы. Это встроенные скалярные типы в GraphQL:
106 | [^leaves]: Элементы самого нижнего уровня в древовидном представлении иерархии
107 |
108 | * `Int`
109 | * `Float`
110 | * `String`
111 | * `Boolean`
112 | * `ID`
113 |
114 | Некоторые реализации GraphQL позволяют определять пользовательские скалярные типы. Это означает, что есть возможность создавать собственные скаляры, такие как `Date` или `JSON`.
115 |
116 | Вы можете определить специальный вид скаляров, используя перечислимые типы (`enum`). Перечисления — специальные скалярные типы, потому что они ограничены фиксированным набором значений.
117 |
118 | Вот таким образом выглядит перечислимый тип:
119 |
120 | ```graphql
121 | enum PinStatus {
122 | DELETED
123 | HIDDEN
124 | VISIBLE
125 | }
126 | ```
127 |
128 | Типы `Input` работает почти так же, как объекты. Внутри них могут быть поля, но разница в том, что такие поля не могут иметь аргументов и также тип `Object`.
129 |
130 | Вот как определяется нестандартный тип `PinInput`:
131 |
132 | ```graphql
133 | input PinInput {
134 | title: String!
135 | link: String!
136 | image: String!
137 | }
138 | ```
139 |
140 | GraphQL позволяет также определять типы `Interface` и `Union`. Они полезны, когда вам нужно вернуть объект, который может состоять из нескольких разных типов.
141 |
142 | Можно воспользоваться интерфейсами, когда есть разные типы, имеющие общие поля между собой. Типичный случай использования будет представлять тип `User`.
143 |
144 | ```graphql
145 | interface Person {
146 | id: String!
147 | email: String!
148 | pins: [Pin]
149 | }
150 |
151 | type User implements Person {
152 | id: String!
153 | email: String!
154 | pins: [Pin]
155 | }
156 |
157 | type Admin implements Person {
158 | id: String!
159 | email: String!
160 | pins: [Pin]
161 | }
162 | ```
163 |
164 | В случае, если вам нужен тип, который представляет разные типы без общих полей между собой, в вашем распоряжении есть тип `Union`. Типичная операция, которая возвращает такой тип, — это поиск:
165 |
166 | ```graphql
167 | union SearchResult = User | Admin | Pin
168 |
169 | type Query {
170 | # ...
171 | search(text: String): [SearchResult]
172 | }
173 | ```
174 |
175 | Вот как выглядит полная версия `schema.graphql`:
176 |
177 | ```graphql
178 | type Pin {
179 | title: String!
180 | link: String!
181 | image: String!
182 | id: String!
183 | user_id: String!
184 | }
185 |
186 | input PinInput {
187 | title: String!
188 | link: String!
189 | image: String!
190 | }
191 |
192 | interface Person {
193 | id: String!
194 | email: String!
195 | pins: [Pin]
196 | }
197 |
198 | type User implements Person {
199 | id: String!
200 | email: String!
201 | pins: [Pin]
202 | }
203 |
204 | type Admin implements Person {
205 | id: String!
206 | email: String!
207 | pins: [Pin]
208 | }
209 |
210 | union SearchResult = User | Admin | Pin
211 |
212 | type Query {
213 | pins: [Pin]
214 | pinById(id: String!): Pin
215 | users: [User]
216 | me: User
217 | search(text: String): [SearchResult]
218 | }
219 |
220 | type Mutation {
221 | addPin(pin: PinInput!): Pin
222 | sendShortLivedToken(email: String!): Boolean
223 | createLongLivedToken(token: String!): String
224 | }
225 | ```
226 |
227 | Как вы узнали из предыдущего раздела, схема состоит из определений типов и резолверов. И поскольку сейчас, когда вы знаете, как выглядят определения типов, пришло время узнать про резолверы.
228 |
229 | ## 2.4 Резолверы
230 |
231 | Резолверы — это функции, которые запускаются каждый раз, когда запрос запрашивает поле. Когда реализация GraphQL получает запрос, она выполняет резолвер для каждого поля. Если резолвер возвращает поле типа `Object`, то GraphQL запускает резолвер-функцию этого поля. Когда все резолверы возвращают скалярные значения, цепочка замыкается, и запрос получает готовый JSON-результат.
232 |
233 | Поскольку GraphQL не привязан к какой-либо технологии баз данных, поэтому реализация резолвера полностью зависит от вас. Все функции в файле `resolvers.js` используют обычный JS-объект, служащий базой данных в оперативной памяти, однако уже в следующей главе вы узнаете, как выполнить миграцию в базу данных Postgres.
234 |
235 | Вы можете организовать резолверы любым способом, в зависимости от ваших потребностей. Примеры этой книги стремятся держать резолвер-функции простыми, а также разделить доступ к базе данных с помощью бизнес-логики. Это простой случай применения старого доброго принципа [разделения ответственности](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8).
236 |
237 | Давайте посмотрим, как выглядит `resolvers.js`:
238 |
239 | ```js
240 | const {
241 | addPin,
242 | createShortLivedToken,
243 | sendShortLivedToken,
244 | createLongLivedToken,
245 | createUser
246 | } = require("./business-logic");
247 |
248 | const database = {
249 | users: {},
250 | pins: {}
251 | };
252 |
253 | const resolvers = {
254 | Query: {
255 | pins: () => Object.values(database.pins),
256 | users: () => Object.values(database.users),
257 | search: (_, { text }) => {
258 | return [
259 | ...Object.values(database.pins).filter(pin =>
260 | pin.title.includes(text)
261 | ),
262 | ...Object.values(database.users).filter(user =>
263 | user.email.includes(text)
264 | )
265 | ];
266 | }
267 | },
268 | Mutation: {
269 | addPin: async (_, { pin }, { user }) => {
270 | const {
271 | user: updatedUser,
272 | pin: createdPin
273 | } = await addPin(user, pin);
274 | database.pins[createdPin.id] = createdPin;
275 | database.users[user.id] = updatedUser;
276 | return createdPin;
277 | },
278 | sendShortLivedToken: (_, { email }) => {
279 | let user;
280 | const userExists = Object.values(database.users).find(
281 | u => u.email === user.email
282 | );
283 | if (userExists) {
284 | user = userExists;
285 | } else {
286 | user = createUser(email);
287 | database.users[user.id] = user;
288 | }
289 | const token = createShortLivedToken(user);
290 | return sendShortLivedToken(email, token);
291 | },
292 | createLongLivedToken: (_, { token }) => {
293 | return createLongLivedToken(token);
294 | }
295 | },
296 | Person: {
297 | __resolveType: person => {
298 | if (person.admin) {
299 | return "Admin";
300 | }
301 | return "User";
302 | }
303 | },
304 | User: {
305 | pins({ id }) {
306 | return Object.values(database.pins).filter(
307 | pin => pin.user_id === id
308 | );
309 | }
310 | },
311 | SearchResult: {
312 | __resolveType: searchResult => {
313 | if (searchResult.admin) {
314 | return "Admin";
315 | }
316 | if (searchResult.email) {
317 | return "User";
318 | }
319 | return "Pin";
320 | }
321 | }
322 | };
323 |
324 | module.exports = resolvers;
325 | ```
326 |
327 | В этом случае не все поля `Query` и `Mutation` имеют соответствующий резолвер. Если у поля нет резовлера, оно будет иметь значение `null`. Конечно, это только для демонстрационных целей. Ваши клиенты API не будут очень довольны запросами, которые всегда возвращают неопределённое значение.
328 |
329 | Можно заметить, что большая часть логики в полях `Query` и `Mutations` состоит из функций, определенных в файле `business-logic.js`. Содержимое функций — чаще всего доступ к данным и вызовы методов из модуля бизнес-логики.
330 |
331 | Некоторые из типов в файле `resolvers.js` имеют методы `__resolveType`. Это метод, который использует `makeExecutableSchema` из `graphql-tools`. Он определяет тип объектов типа, которые имеют `Union` или `Interface`.
332 |
333 | Вы можете попробовать схему этого примера, если откроете консоль склонированного примера и запустив в ней `node queries.js`. Этот скрипт имитирует пользователя, который сначала создает токен аутентификации и отправляет его, чтобы можно добавить новый пин.
334 |
335 | ```bash
336 | $ node queries
337 | API Token:
338 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
339 | {
340 | "data": {
341 | "addPin": {
342 | "id": "f5220ee1-bfeb-48a0-be9f-c63d055b8139",
343 | "title": "Hello world",
344 | "link": "http://graphql.college/fullstack-graphql",
345 | "image": "http://graphql.college/fullstack-graphql",
346 | "user_id": "75c16079-b3ef-43f0-a352-ae03f2488baa"
347 | }
348 | }
349 | }
350 | ```
351 |
352 | Не бойтесь экспериментировать, изменяя различные резолвер-функции и наблюдая за тем, как изменяется конечный результат. Вы также можете создавать различные запросы, теперь, когда вы уже знаете, какие запросы и мутации предоставляет ваша схема.
353 |
354 | ## 2.5 Резюме
355 |
356 | Вы узнали, как создавать GraphQL-схемы. Вы написали определения типов с использованием SDL, а также резолверы, используя JavaScript. Схема, созданная в рамках этой главы, доступна скриптами, использующими `graphql-js`.
357 |
358 | В следующей главе вы узнаете, как создавать HTTP API с помощью GraphQL. Вы добавите различные слои, составляющие сервер GraphQL, поверх схемы GraphQL. Этот API будет иметь несколько дополнительных уровней, включая HTTP, базу данных и аутентификацию.
359 |
360 |
--------------------------------------------------------------------------------
/manuscript/chapter-3.md:
--------------------------------------------------------------------------------
1 | # 3. API-интерфейсы GraphQL
2 |
3 | HTTP-сервер — наиболее распространённый способ предоставить схему GraphQL. Создание API-интерфейсов GraphQL — это гораздо больше, чем просто проектирование схем. В этой главе вы узнаете, как создавать надежные, многоуровневые API-интерфейсы GraphQL.
4 |
5 | 
6 |
7 | Вы узнаете, как предоставлять GraphQL-схемы с помощью Apollo Server. Как подключить ваши резолверы к базе данных. Вы добавите аутентификацию по электронной почте в собственный API. Наконец, вы научитесь, как организовать исходный код по функциональным возможностям.
8 |
9 | 
10 |
11 | Все этапы этой главы имеют соответствующий проект, который можно склонировать, чтобы попробовать на практике.
12 |
13 | Давайте начнем с изучения того, как создать API, используя Apollo Server.
14 |
15 | ## 3.1 Сервер
16 |
17 | Apollo Server — это сервер с открытым исходном кодом, совместимый со спецификацией GraphQL. Это готовый к работе в продакшене, простой в настройке способ представления GraphQL-схем, так чтобы HTTP-клиенты могли их использовать.
18 |
19 | 
20 |
21 | Вы уже создали схему с использованием `graphql-tools` в предыдущей главе, поэтому публично представить её с помощью Apollo Server не составляет никакого труда.
22 |
23 | ```js
24 | const { ApolloServer } = require("apollo-server");
25 |
26 | const schema = require("./schema");
27 |
28 | const server = new ApolloServer({ schema });
29 |
30 | server.listen().then(({ url }) => {
31 | console.log(`🚀 Server ready at ${url}`);
32 | });
33 | ```
34 |
35 | Действительно просто! Достаточно вызова функции `server.listen()` и у вас есть работающий API для GraphQL. Склонируйте следующий пример проекта, чтобы у вас была собственная копия проекта.
36 |
37 | [Клонировать пример сервера](https://glitch.com/edit/#!/remix/pinapp-server)
38 |
39 | Нажмите кнопку «Show» в левом верхнем углу экрана для открытия включенного в комплект GraphQL-клиента — [GraphQL Playground](https://github.com/prismagraphql/graphql-playground). Это больше, чем просто клиент GraphQL, скорее что-то вроде IDE. У этого инструмента есть автозаполнение запроса, документация GraphQL-схемы, а также хранение всех выполненных запросов, что позволяет использовать в будущем.
40 |
41 | 
42 |
43 | Теперь, когда вы развернули собственный API-сервер GraphQL, пришло время добавить сохранение данных, используя базу данных.
44 |
45 | ## 3.2 База данных
46 |
47 | API-интерфейсы GraphQL могут быть работать с любым источником данных. Они могут использовать базы данных SQL, NoSQL, базы данных в оперативной памяти или даже использовать конечные точки HTTP.
48 |
49 | 
50 |
51 | В этой главе вы подключите PinApp к базе данных SQLite, используя коннектор базы данных [Knex](http://knexjs.org). Knex — это построитель SQL-запросов, который может взаимодействовать со многими базами данных SQL, такими как SQLite3, MySQL, Postgres и др.
52 |
53 | Склонируйте текущую итерацию приложения PinApp, чтобы вы могли следовать по тексту этого раздела, работая с собственной копией проекта.
54 |
55 | [Клонировать пример базы данных](https://glitch.com/edit/#!/remix/pinapp-database)
56 |
57 | > Не забывайте следовать инструкциям по началу работы в файле README проекта
58 |
59 | Поскольку все взаимодействия с базами данных происходят в файле `resolvers.js`, давайте начнем с этого файла.
60 |
61 | Получить набор записей с помощью `select()`. Например, получить список пинов можно следующим образом:
62 |
63 | ```js
64 | pins: () => database("pins").select(),
65 | ```
66 |
67 | Вы можете объединить в цепочку вызовов функции Knex. Например, можно отфильтровать результаты из `select()`, добавив его вызов с функцией `where()`.
68 |
69 | ```js
70 | search: (_, { text }) => {
71 | return Promise.all([
72 | database("users")
73 | .select()
74 | .where("email", "like", `%${text}%`),
75 | database("pins")
76 | .select()
77 | .where("title", "like", `%${text}%`)
78 | ]);
79 | };
80 | ```
81 |
82 | Еще одна полезная функция, которую предоставляет Knex, — это `insert()`. Она позволит создавать объекты в базе данных. Вот так выглядит мутация `addPin` при использовании `insert`.
83 |
84 | ```js
85 | addPin: async (_, { pin }, { token }) => {
86 | const [user] = await authorize(token);
87 | const { user: updatedUser, pin: createdPin } = await addPin(user, pin);
88 | await database("pins").insert(createdPin);
89 | return createdPin;
90 | },
91 | ```
92 |
93 | Файл `database.js` создает экземпляр Knex и экспортирует в качестве модуля. Это простой файл, который создает экземпляр Knex с конфигурационными значениями из файла с именем `knexfile.js`.
94 |
95 | ```js
96 | const database = require("knex")(require("./knexfile"));
97 |
98 | module.exports = database;
99 | ```
100 |
101 | У Knex имеется интерфейс командной строки, предоставляющий несколько утилит для создания миграций, заполнения данными баз данных и многое другое. Утилита CLI считывает конфигурацию из `knexfile.js`. Файл конфигурации PinApp выглядит так:
102 |
103 | ```js
104 | module.exports = {
105 | client: "sqlite3",
106 | connection: {
107 | filename: ".data/database.sqlite"
108 | }
109 | };
110 | ```
111 |
112 | > [Glitch](https://glitch.com) позволяет сохранять данные в директории `.data`. Именно по этой причине файл `database.sqlite` находится в этой директории.
113 |
114 | Последний шаг, необходимый для использования SQL-файла, — создание схемы базы данных. Knex может генерировать файлы миграции, используя собственный инструмент командной строки.
115 |
116 | Выполнение `npx knex migrate:make create_users_table` создаст файл с именем `[date]_create_users_table.js` в директории `.migrations`. Этот файл экспортирует два метода — `up` и `down`. Эти методы являются своего рода заполнителями, которые необходимо заполнить согласно ваших конкретным потребностям. В данном случае таблица пользователей будет иметь два поля: `id` и `email`. У обоих полей указан тип `string`, а поле `id` задано как первичный ключ.
117 |
118 | ```js
119 | exports.up = function(knex) {
120 | return knex.schema.createTable("users", function(table) {
121 | table.string("id").primary();
122 | table.string("email");
123 | });
124 | };
125 |
126 | exports.down = function(knex) {
127 | return knex.schema.dropTable("users");
128 | };
129 | ```
130 |
131 | В склонированном проекте есть еще одна миграция — `[date]_create_pins_migration`. Эта миграция определяет пять строковых (`string`) полей: `id`, `title`, `link`, `image` и `pin_id`.
132 |
133 | ```js
134 | exports.up = function(knex) {
135 | return knex.schema.createTable("pins", function(table) {
136 | table.string("id").primary();
137 | table.string("title");
138 | table.string("link");
139 | table.string("image");
140 | table
141 | .string("user_id")
142 | .references("id")
143 | .inTable("users")
144 | .onDelete("CASCADE")
145 | .onUpdate("CASCADE");
146 | });
147 | };
148 |
149 | exports.down = function(knex) {
150 | return knex.schema.dropTable("pins");
151 | };
152 | ```
153 |
154 | Выполните `npm run setup-db` для применения всех миграций базы данных. Этот скрипт определен в ключе `scripts` файла `package.json`:
155 |
156 | ```json
157 | "setup-db": "knex migrate:latest"`
158 | ```
159 |
160 | Изучение SQL выходит за рамки книги: если вы хотите правильно изучить этот язык, лучше всего прочитать соответствующую книгу. Построитель запросов Knex отлично работает с базами данных SQL, а еще у него есть отличная [документация](http://knexjs.org). Обратитесь к ней, если вы хотите узнать больше про данный инструмент.
161 |
162 | ## 3.3 Аутентификация
163 |
164 | Один из самых часто задаваемых вопросов при создании API-интерфейсов с использованием GraphQL: «Куда поместить аутентификацию и авторизацию?». Это должно быть в слое GraphQL? Или на уровне базы данных? А может быть в бизнес-логики? Несмотря на то, что ответ зависит от контекста того, какой API вы создаете, распространенный способ решения этой проблемы — поместить аутентификацию и авторизацию на бизнес-уровне. Размещение кода, связанного с аутентификацией, на бизнес-уровне — это [подход Facebook](https://dev-blog.apollodata.com/graphql-at-facebook-by-dan-schafer-38d65ef075af).
165 |
166 | 
167 |
168 | Вы можете реализовать аутентификацию несколькими способами, что полностью соответствует вашим потребностям. Этот раздел покажет, как добавить аутентификацию по электронной почте в PinApp.
169 |
170 | Аутентификация на основе электронной почты состоит из поля для ввода электронной почты. Как только пользователи укажут электронную почту, им придёт ссылка на приложение, в которой будет содержаться аутентификационный временный токен. Если пользователи перейдут в приложение с использованием действительного токена, то значит такому пользователю можно доверять (предполагаем, что почта пользователя подтверждена). А далее действительный токен будет заменен на токен аутентификации с более продолжительным сроком действия.
171 |
172 | Самое большое преимущество подобной системы аутентификации, в отличие от старой доброй аутентификации по паролю, заключается в том, что вам вообще не нужно заниматься паролями. То, что мы не храним пароли также означает, что не требуется принимать крайних мер безопасности для их защиты. Еще это значит, что вашим пользователям не нужно заходить на очередной сайт, который просит их создать новый пароль.
173 |
174 | ```gherkin
175 | Feature: Email based authentication
176 |
177 | Scenario: Send magic link
178 | Given a user enters his email address
179 | When he/she submits the form
180 | Then he/she should receive an email with a link to the app that contains his token
181 |
182 | Scenario: Verify email
183 | Given a user receives an email with a magic link
184 | When he/she clicks the link
185 | Then he/she should see a loading screen
186 | When the app verifies the token
187 | Then he/she should see an email confirmation message
188 | ```
189 |
190 | Говоря в терминах GraphQL, оба эти действия (сценария) отражаются на мутациях в соответствующей GraphQL-схеме — `sendShortLivedToken` и `createLongLivedToken`. Первое действие получает в качестве аргумента электронное письмо типа `String`. Второе действие получает действительный токен, а возвращает новый токен.
191 |
192 | ```graphql
193 | type Mutation {
194 | # ...
195 | sendShortLivedToken(email: String!): Boolean
196 | createLongLivedToken(token: String!): String
197 | }
198 | ```
199 |
200 | Склонируйте этот проект для того, чтобы следить за ходом внедрения аутентификации по электронной почте.
201 |
202 | [Склонировать пример аутентификации по электронной почте](https://glitch.com/edit/#!/remix/pinapp-email-authentication)
203 |
204 | Теперь давайте проанализируем, как выглядят резолверы, связанные с электронной почтой.
205 |
206 | Резолвер `sendShortLivedToken` должен проверить, существует ли пользователь, соответствующий электронной почте. Если он не существует, он должен вставить его в базу данных. После этого он должен создать токен с небольшим сроком действия и отправить его на электронную почту пользователя.
207 |
208 | ```js
209 | sendShortLivedToken: async (_, { email }) => {
210 | let user;
211 | const userExists = await database("users")
212 | .select()
213 | .where({ email });
214 | if (userExists.length) {
215 | user = userExists[0];
216 | } else {
217 | user = createUser(email);
218 | await database("users").insert(user);
219 | }
220 | const token = createShortLivedToken(user);
221 | return sendShortLivedToken(email, token);
222 | };
223 | ```
224 |
225 | Этот резолвер использует две функции из файла `business-logic.js` — `createShortLivedToken` и `sendShortLivedToken`.
226 |
227 | Первый создает токен, используя функцию `jsonwebtoken` из NPM-пакета [`sign`](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback). У этого токена срок действия ограничен пятью минутами.
228 |
229 | ```js
230 | const createShortLivedToken = ({ email, id }) => {
231 | return jsonwebtoken.sign(
232 | { id, email },
233 | process.env.SECRET,
234 | {
235 | expiresIn: "5m"
236 | }
237 | );
238 | };
239 | ```
240 |
241 | Вторая функция `sendShortLivedToken` использует функцию, определенную в файле `email.js`, под названием `sendMail`.
242 |
243 | ```js
244 | const sendShortLivedToken = (email, token) => {
245 | return sendMail({
246 | from: '"Julian" ',
247 | to: email,
248 | text: `${process.env.APP_URL}/verify?token=${token}`,
249 | html: `Authenticate`,
252 | subject: "Auth token"
253 | });
254 | };
255 | ```
256 |
257 | Для отправки почты воспользуемся пакетом `nodemailer`. Эта библиотека позволяет отправлять электронные письма через SMTP-сервер. Самый простой способ создать почтовый сервер для разработки — [Ethereal](https://ethereal.email/messages. Это фиктивный почтовый сервис, разработанный создателями Nodemailer, является очень простым способом создания SMTP-службы для разработки. Разумеется, если вы хотите отправлять реальные электронные письма, вам следует использовать реальный сервис SMTP. [Sendgrid](https://sendgrid.com/) предлагает отличный бесплатный план.
258 |
259 | ```js
260 | const nodemailer = require("nodemailer");
261 |
262 | const transporter = nodemailer.createTransport({
263 | host: "smtp.ethereal.email",
264 | port: 587,
265 | auth: {
266 | user: process.env.MAIL_USER,
267 | pass: process.env.MAIL_PASSWORD
268 | }
269 | });
270 |
271 | function sendMail({ from, to, subject, text, html }) {
272 | const mailOptions = {
273 | from,
274 | to,
275 | subject,
276 | text,
277 | html
278 | };
279 | return new Promise((resolve, reject) => {
280 | transporter.sendMail(mailOptions, (error, info) => {
281 | if (error) {
282 | return reject(error);
283 | }
284 | resolve(info);
285 | });
286 | });
287 | }
288 |
289 | module.exports = sendMail;
290 | ```
291 |
292 | Реализация `createLongLivedToken` намного проще, чем `sendShortLivedToken`. Первый резолвер использует функцию из файла `business-logic.js`, проверяющая токен, переданный в качестве аргумента. Если токен действителен, функция создает токен с окончанием срока действия, равный тридцать дней.
293 |
294 | ```js
295 | const createLongLivedToken = token => {
296 | try {
297 | const { id, email } = jsonwebtoken.verify(
298 | token,
299 | process.env.SECRET
300 | );
301 | const longLivedToken = jsonwebtoken.sign(
302 | { id, email },
303 | process.env.SECRET,
304 | { expiresIn: "30 days" }
305 | );
306 | return Promise.resolve(longLivedToken);
307 | } catch (error) {
308 | console.error(error);
309 | throw error;
310 | }
311 | };
312 | ```
313 |
314 | Двигайтесь дальше и сконфигурируйте склонированный проект в соответствии с вашей учетной записью Ethereal. Как только всё будет настроено, зайдите в GraphQL Playground, нажав кнопку «Show», и попробуйте авторизоваться, используя собственную электронную почту (или любую другую, так как Ethereal перехватывает все отправленные письма :D).
315 |
316 | ## 3.4 Организация файлов
317 |
318 | В этом разделе вы узнаете, как использовать `graphql-import` для организации API-интерфейсов Node.js GraphQL по функциональным возможностям. Импорт GraphQL позволяет импортировать и экспортировать определения типов в GraphQL SDL.
319 |
320 | На текущий момент все файлы в образце репозитория сгруппированы по ролям, но подобный не очень хорошо подходит для больших проектов. Сейчас все резолверы определены в файле `resolvers.js`, определения типов — в файле `schema.graphql`, а вся бизнес-логика — в файле b`business-logic.js`. По мере того, как проекты становятся все больше и больше, эти три файла станут слишком большими и неуправляемыми.
321 |
322 | Текущая структура файла проекта выглядит таким образом:
323 |
324 | 
325 |
326 | Вы собираетесь разделить `schema.graphql`, `resolvers.js` и `business-logic.js` на три функциональные возможности: `authentication`, `pins` и `search`. Окончательная структура каталогов будет такой, как показано ниже:
327 |
328 | 
329 |
330 | Склонируйте проект, если хотите посмотреть на окончательную версию.
331 |
332 | [Клонировать пример организации файлов](https://glitch.com/edit/#!/remix/pinapp-files)
333 |
334 | Основной точкой входа схемы GraphQL по-прежнему будет файл `schema.graphql`. Разница в том, что в нем не будут содержаться какие-либо определения типов, а вместо этого импортировать все типы из файла `schema.graphql` в каждой директории функциональной возможности. Основная схема будет импортировать остальные схемы, используя оператор `import`, предоставленный `graphql-import`. Его синтаксис: `# import * from "module-name.graphql"`.
335 |
336 | ```graphql
337 | # import * from "authentication/schema.graphql"
338 | # import * from "pins/schema.graphql"
339 | # import * from "search/schema.graphql"
340 | ```
341 |
342 | Такой способ импорта GraphQL SDL возможен, поскольку файл `schema.js` загружает `schema.graphql` следующим фрагментом кода:
343 |
344 | ```js
345 | const { importSchema } = require("graphql-import");
346 |
347 | const typeDefs = importSchema("schema.graphql");
348 | // ...
349 | ```
350 |
351 | Подобное схеме, главная точка входа для всех резолверов останется прежней. Это по-прежнему будет `resolvers.js`. Node.js уже предоставляет способ импорта и экспорта файлов с помощью `require`, поэтому вам не потребуется использовать дополнительную библиотеку. Даже если этот файл не нуждается в дополнительной библиотеке для импорта модулей, он использует библиотеку для объединения импортируемых резолверов. Можно писать на чистом JavaScript-коде, чтобы достичь аналогичного результата, однако `lodash.merge` — это отличный способ объединить множества JavaScript-объектов.
352 |
353 | ```js
354 | const merge = require("lodash.merge");
355 |
356 | const searchResolvers = require("./search/resolvers.js");
357 | const authenticationResolvers = require("./authentication/resolvers.js");
358 | const pinsResolvers = require("./pins/resolvers.js");
359 |
360 | const resolvers = merge(
361 | searchResolvers,
362 | authenticationResolvers,
363 | pinsResolvers
364 | );
365 |
366 | module.exports = resolvers;
367 | ```
368 |
369 | В завершение изменения файловой структуры, разделите `business-logic.js`, `resolvers.js` и `schema.graphql`. Разделите файл `business-logic.js` на `authentication/index.js` и `pins/index.js`. Разделите `resolvers.js` на`authentication/resolvers.js`, `pins/resolvers.js` и `search/resolvers.js`. И напоследок разделите `schema.graphql` на три новые директории.
370 |
371 | Всё! Использование файловой структуры, основанной на функциональных возможностях — это масштабируемый способ организации кода. Такой подход может быть излишним для небольших проектов, но он с лихвой окупается в больших проектах.
372 |
373 | ## 3.5 Резюме
374 |
375 | Вы узнали, как создать API на GraphQL, используя Apollo Server. Начав с простой GraphQL-схемы, вы узнали, как обернуть эту схему HTTP-слоем при помощи ApolloServer. Вы добавили слой базы данных, воспользовавшись Knex, а также аутентификацию по электронной почты с применением Nodemailer. Наконец, вы организовали свой проект по функциональным возможностям с использованием GraphQL Import.
376 |
377 | В следующей главе вы научитесь, как создавать GraphQL-клиенты на примере Apollo Client, а также сделаете фронтенд на React, который взаимодействует с только что созданным API GraphQL.
378 |
379 |
--------------------------------------------------------------------------------
/manuscript/chapter-4.md:
--------------------------------------------------------------------------------
1 | # 4. GraphQL clients
2 |
3 | In this chapter you will learn how to build a GraphQL client using React and Apollo GraphQL.
4 |
5 | 
6 |
7 | You will start with a bare-bones React application, and transform it step-by-step into a GraphQL powered Pinterest clone. The first step will be making an app with all state stored in the client. After that you will learn how to add a GraphQL library called Apollo Client. Finally, you will learn how to easily fetch and store data in the GraphQL server using React Apollo's `Query` and `Mutation` components.
8 |
9 | Let's start with the initial version of PinApp.
10 |
11 | ## 4.1 Initial React client
12 |
13 | The first version of PinApp's client consists of a single `App` component that renders a `div` that says "PinApp". The project's setup is pretty standard, since it's based on the super popular [`create-react-app`](https://github.com/facebook/create-react-app). Here is the complete source code for `src/App.js`:
14 |
15 | ```js
16 | import React from "react";
17 |
18 | export default class App extends React.Component {
19 | render() {
20 | return
PinApp
;
21 | }
22 | }
23 | ```
24 |
25 | Remix this project to create your own copy of it:
26 |
27 | [Remix initial React client](https://glitch.com/edit/#!/remix/pinapp-initial)
28 |
29 | The next section will teach you how to create a client side version of PinApp.
30 |
31 | ## 4.2 Client side state
32 |
33 | This section will teach you how to create a working, client-side only version of PinApp. To achieve this, you will use a library called [`pinapp-components`](https://github.com/GraphQLCollege/pinapp-components). This library exports a `Container` component, a `Nav` component and also five Page components, `PinListPage`, `LoginPage`, `VerifyPage`, `AddPinPage` and `ProfilePage`. To get to know all of these components, see them live in [the playground](https://graphqlcollege.github.io/pinapp-components).
34 |
35 | These components expose a well defined API via their props. This customization enables you to use them to create a version of PinApp using only local state and passing it via props. Using the same components, but passing different props, you will create a version of PinApp backed by the GraphQL API you created in the previous chapter.
36 |
37 | Following is a brief description of all components.
38 |
39 | `Container` sets up PinApp's styling and routing configuration. Render it as the first in your component hierarchy. It only receives children as props.
40 |
41 | `Nav` renders a list of actions as the app's footer. Actions are links to `/`, `/upload-pin` and `/profile`.
42 |
43 | The rest of the components define a page each.
44 |
45 | `PinListPage` renders a list of pins in the `/` URL. It receives just one argument, `pins`. `pins` is an array that contains a list of pins. Pins must be object with `title`, `image` and `link` properties. All three properties must be Strings.
46 |
47 | 
48 |
49 | `LoginPage` sets up the `/login` URL. It receives only one prop, `authenticate`. It is a function that will run when the user clicks "login". This function receives a string as an argument, which contains an email that the user entered.
50 |
51 | 
52 |
53 | `VerifyPage` sets up a screen in the `/verify?token={token}` URL. This component receives one prop called `verify`. It is a function that will run when the component mounts. This function passes a string argument which contains the token that the user entered in the URL's query string.
54 |
55 | 
56 |
57 | `AddPinPage` renders a form to create a pin in the `/upload-pin` URL. This component receives two props, `authenticated` and `addPin`. `authenticated` is a boolean. `addPin` is a function that will run when the user clicks "Save". It receives a pin object as argument.
58 |
59 | 
60 |
61 | The last page component is `ProfilePage`. It shows the users' profile in the `/profile` URL. It receives three props, `user`, `authenticated` and `logout`. `user` is an object with an `email` property. `authenticated` is a boolean. `logout` is a function that will run when the user clicks "Logout". It does not receive any arguments.
62 |
63 | 
64 |
65 | Now let's talk about how state management. The app will have three keys in state. It will hold an array of `pins`, a boolean called `authenticated` and a `user` object. The `App` component will have three functions that directly modify this state, called `addPin`, `verify` and `logout`. To simulate authentication, it will have an `authenticate` function that always authenticates the user. This `App` component will pass down its state and functions as props to the components from `pinapp-components`.
66 |
67 | The first thing you need to do is add `"pinapp-components": "^1.0.1"` to the `"dependencies"` key in your `package.json`.
68 |
69 | After adding `pinapp-components` as a dependency, modify `src/App.js` to look like this:
70 |
71 | ```js
72 | import React from "react";
73 | import {
74 | Container,
75 | Nav,
76 | PinListPage,
77 | AddPinPage,
78 | LoginPage,
79 | VerifyPage,
80 | ProfilePage
81 | } from "pinapp-components";
82 |
83 | export default class App extends React.Component {
84 | state = { pins: [], authenticated: false, user: null };
85 | addPin = pin => {
86 | this.setState(({ pins }) => ({
87 | pins: pins.concat([pin])
88 | }));
89 | };
90 | verify = () => {
91 | return success().then(token =>
92 | this.setState({
93 | authenticated: true,
94 | user: { email: "name@example.com" }
95 | })
96 | );
97 | };
98 | authenticate = () => {
99 | return Promise.resolve({});
100 | };
101 | logout = () => {
102 | this.setState({ authenticated: false, user: null });
103 | };
104 | render() {
105 | return (
106 |
107 |
108 |
112 |
113 |
114 |
119 |
120 |
121 | );
122 | }
123 | }
124 |
125 | function success() {
126 | return wait(1000).then(() => "long-lived-token");
127 | }
128 |
129 | function wait(time) {
130 | return new Promise((resolve, reject) => {
131 | setTimeout(resolve, time);
132 | });
133 | }
134 | ```
135 |
136 | [Remix this step's application](https://glitch.com/edit/#!/remix/pinapp-client-side-state) if you got stuck in some way.
137 |
138 | Congratulations! You have got a working version of PinApp. Too bad that all pins get lost when you refresh the application. This happens because the app stores everything in-memory. The next couple of sections will teach you how to connect PinApp with your GraphQL API.
139 |
140 | ## 4.3 Apollo Client
141 |
142 | In this section you will add [Apollo Client](https://www.apollographql.com/client) to PinApp's frontend. You will setup Apollo Client, point it to the API you created in the previous chapter, and send a query to that endpoint.
143 |
144 | Apollo Client is a GraphQL Client that provides advanced data loading to JavaScript applications. It provides wrappers to many popular JavaScript frameworks, like React, React Native, Angular, Vue.js, it also provides Native iOS and Android versions.
145 |
146 | To install it, you need to add three dependencies to `package.json`. These dependencies are `apollo-boost",`, `graphql` and `graphql-tag`. This is what your `"dependencies"` key should look like:
147 |
148 | ```js
149 | "dependencies": {
150 | "react-scripts": "^1.1.4",
151 | "react": "^16.3.2",
152 | "react-dom": "^16.3.2",
153 | "pinapp-components": "^1.0.1",
154 | "apollo-boost": "^0.1.6",
155 | "graphql": "^0.13.2",
156 | "graphql-tag": "^2.9.2"
157 | },
158 | ```
159 |
160 | Now it's time to setup Apollo Client in `src/App.js`. Import `apollo-boost` as `ApolloClient` and import `graphql-tag` as `gql`.
161 |
162 | After that, create a new instance of `ApolloClient`, pass it an object with an `uri` key pointing to your GraphQL API URL, and assign it to a variable called `client`.
163 |
164 | ```js
165 | import ApolloClient from "apollo-boost";
166 | import gql from "graphql-tag";
167 |
168 | const client = new ApolloClient({
169 | uri: process.env.REACT_APP_API_URL
170 | });
171 | ```
172 |
173 | Remember to add `REACT_APP_API_URL=https://pinapp-files.glitch.me` to the `.env` file, pointing to your API URL.
174 |
175 | Once you have created an instance of Apollo Client, you can use it to send queries and mutations to your API. Let's use the `query` method from Apollo Client to fetch the list of pins from the API. This method receives an object with a `query` key. Inside that key you can send queries using the `gql` function from `graphql-tag`.
176 |
177 | The `gql` function receives a string written in SDL and transforms it into a JavaScript object. `client.query` accepts this JavaScript object. Note that `client.query` throws an error if you pass a string directly, without using `gql`.
178 |
179 | Another handy feature of `gql` is that many IDEs add syntax highlighting to `gql` calls. Unfortunately, at the moment Glitch does not support this feature. But who knows, maybe in the future it supports `gql` syntax highlighting. And maybe you are reading this in the future and enjoying beautiful GraphQL queries in our Glitch's code examples.
180 |
181 | Add the following `componentDidMount` function to the `App` component:
182 |
183 | ```js
184 | componentDidMount() {
185 | client
186 | .query({
187 | query: gql`
188 | {
189 | pins {
190 | title
191 | image
192 | link
193 | }
194 | }
195 | `
196 | })
197 | .then(result => this.setState({ pins: result.data.pins }));
198 | }
199 | ```
200 |
201 | You can click [here](https://glitch.com/edit/#!/remix/pinapp-apollo-client) to remix this step's version of PinApp. Remember to edit `REACT_APP_API_URL` in `.env`.
202 |
203 | As you can see, it's really easy to use Apollo Client to communicate with GraphQL APIs. But Apollo Client provides an even better way of connecting your React application with a GraphQL API. It provides a library called React Apollo, which lets you collocate components with data by placing queries alongside your components. The next section will teach you how to setup React Apollo to interact with PinApp's API.
204 |
205 | ## 4.4 React Apollo
206 |
207 | React Apollo is a library that lets you declaratively specify your component's data requirements. This means that your components can specify what data they need, instead of how to fetch that data. You achieve this by placing GraphQL queries in your components, and delegating how to fetch that data to Apollo Client.
208 |
209 | ## 4.5 Query component
210 |
211 | A component that React Apollo provides is called `Query`. It receives one prop, called `query`, which accepts a GraphQL query. This component also receives a function as a child. Functions as child is a React pattern that lets you pass data from parents to children. Many of React Apollo's component use this pattern to pass down information. You can read more about passing functions to component in [React's official documentation](https://reactjs.org/docs/faq-functions.html).
212 |
213 | The `Query` component passes down an object to its children. It contains three keys, `loading`, `error` and `data`. `loading` is a boolean that is true when the component started fetching data, but has not finished yet. `error` is an object that contains GraphQL errors, if any. `data` is an object that contains the GraphQL API's response.
214 |
215 | Let's use this `Query` component. Create a file called `src/PinListPage.js`. If `loading` is true, it will return the `Spinner` from `pinapp-components`. If error is defined, it will display a `div` with the text "Error". And if `data` is defined, it will return `PinListPage` from `pinapp-components`.
216 |
217 | ```js
218 | import React from "react";
219 | import { Query } from "react-apollo";
220 | import { PinListPage, Spinner } from "pinapp-components";
221 |
222 | import { LIST_PINS } from "./queries";
223 |
224 | class PinListPageContainer extends React.Component {
225 | render() {
226 | return (
227 |
228 | {({ loading, error, data }) => {
229 | if (loading) {
230 | return (
231 |
235 | );
236 | }
237 | if (error) {
238 | return
Error
;
239 | }
240 | return ;
241 | }}
242 |
243 | );
244 | }
245 | }
246 |
247 | export default PinListPageContainer;
248 | ```
249 |
250 | You may have noticed that `PinListPage.js` references `LIST_PINS` from `queries`. Create this new file called `src/queries.js`. It will contain all queries that PinApp needs. Paste the following code in this new file.
251 |
252 | ```js
253 | import gql from "graphql-tag";
254 |
255 | export const ADD_PIN = gql`
256 | mutation AddPin($pin: PinInput!) {
257 | addPin(pin: $pin) {
258 | title
259 | link
260 | image
261 | }
262 | }
263 | `;
264 |
265 | export const LIST_PINS = gql`
266 | {
267 | pins {
268 | id
269 | title
270 | link
271 | image
272 | user_id
273 | }
274 | }
275 | `;
276 |
277 | export const CREATE_LONG_LIVED_TOKEN = gql`
278 | mutation CreateLongLivedToken($token: String!) {
279 | createLongLivedToken(token: $token)
280 | }
281 | `;
282 |
283 | export const CREATE_SHORT_LIVED_TOKEN = gql`
284 | mutation CreateShortLivedToken($email: String!) {
285 | sendShortLivedToken(email: $email)
286 | }
287 | `;
288 |
289 | export const ME = gql`
290 | {
291 | me {
292 | email
293 | }
294 | }
295 | `;
296 | ```
297 |
298 | ## 4.6 Apollo Provider
299 |
300 | In order to use React Apollo's `Query` component, you need to wrap your application with a component named `ApolloProvider`. It receives an instance of `ApolloClient`, and provides querying capabilities to `Query` components inside the component hierarchy.
301 |
302 | ```js
303 | import { ApolloProvider } from "react-apollo";
304 | ```
305 |
306 | Remove `PinListPage` from the list of imports from `pinapp-components` and import it from the file you just created.
307 |
308 | ```js
309 | import PinListPage from "./PinListPage";
310 | ```
311 |
312 | Put `ApolloProvider` as the first component in `App`'s `render`. Also remove `pins` prop from `PinListPage` because it does not need it anymore.
313 |
314 | ```js
315 |
316 |
317 |
318 | {/* ... */}
319 |
320 |
321 | ```
322 |
323 | Finally, you can now delete the `componentDidMount` function, because you load the list of pins using `Query`.
324 |
325 | You can start seeing the benefits of declarative fetching using React Apollo, compared to fetching data in React's lifecycle methods, like `componentDidMount`.
326 |
327 | ## 4.7 Mutation component
328 |
329 | React Apollo provides a component called `Mutation` that allows you to collocate mutations with React components.
330 |
331 | It works similarly to `Query`, it receives a query as a prop and it also receives a function as its child.
332 |
333 | It receives a GraphQL query, in this case that property is called `mutation` instead of `query`.
334 |
335 | Similarly to `Query`, it receives a function as its child. The main difference is that this component passes down a function as its first argument, instead of an object with `data`, `loading` and `error`. It actually passes that object as a second argument, it will contain the data returned by the mutation. Calling this function will send a mutation to the GraphQL API configured in `ApolloProvider`.
336 |
337 | Let's use React Apollo's `Mutation`. Create a file called `src/LoginPage.js`. Inside it, create a React class Component that returns `Mutation` in its `render` method. Place a function as a child of `Mutation`. This function will receive an argument called `createShortLivedToken`. `LoginPage` will receive a function in its `authenticate` property that calls `createShortLivedToken` when the user clicks "Login".
338 |
339 | ```js
340 | import React from "react";
341 | import { Mutation } from "react-apollo";
342 | import { LoginPage } from "pinapp-components";
343 |
344 | import { CREATE_SHORT_LIVED_TOKEN } from "./queries";
345 |
346 | class LoginPageContainer extends React.Component {
347 | render() {
348 | return (
349 |
350 | {createShortLivedToken => (
351 |
353 | createShortLivedToken({
354 | variables: { email }
355 | })
356 | }
357 | />
358 | )}
359 |
360 | );
361 | }
362 | }
363 |
364 | export default LoginPageContainer;
365 | ```
366 |
367 | To use this component, remove `LoginPage` from the list of `pinapp-components` import in `src/App.js` and import it from the file you just created.
368 |
369 | After sending a mutation to the server, you usually want to update the data in your app. For example, after adding a pin, you want to update the list of pins in your application. An easy way to achieve this is using another property from `Mutation`, called `refetchQueries`. It receives an array that contains the list of GraphQL queries to send after the mutation finishes.
370 |
371 | To see `refetchQueries` in use, create a file called `src/AddPinPage.js` with the following contents:
372 |
373 | ```js
374 | import React from "react";
375 | import { Mutation } from "react-apollo";
376 |
377 | import { AddPinPage } from "pinapp-components";
378 | import { ADD_PIN, LIST_PINS } from "./queries";
379 |
380 | class AddPinPageContainer extends React.Component {
381 | render() {
382 | return (
383 |
384 | {addPin => (
385 |
388 | addPin({
389 | variables: { pin },
390 | refetchQueries: [{ query: LIST_PINS }]
391 | })
392 | }
393 | />
394 | )}
395 |
396 | );
397 | }
398 | }
399 |
400 | export default AddPinPageContainer;
401 | ```
402 |
403 | Another useful prop that `Mutation` receives is called `update`. It is a function that gets called once your mutation finishes. You can use it to update React Apollo's internal state after a mutation finishes. In this case you are going to use it to get access to the token that the `createLongLived` query returns.
404 |
405 | Create a file called `src/VerifyPage.js` and place the following code in it.
406 |
407 | ```js
408 | import React from "react";
409 | import { Mutation } from "react-apollo";
410 | import { VerifyPage } from "pinapp-components";
411 |
412 | import { CREATE_LONG_LIVED_TOKEN, ME } from "./queries";
413 |
414 | class VerifyPageContainer extends React.Component {
415 | render() {
416 | return (
417 | {
420 | if (data && data.createLongLivedToken) {
421 | this.props.onToken(data.createLongLivedToken);
422 | }
423 | }}
424 | >
425 | {createLongLivedToken => (
426 |
428 | createLongLivedToken({
429 | variables: {
430 | token: shortLivedToken
431 | },
432 | refetchQueries: [{ query: ME }]
433 | })
434 | }
435 | />
436 | )}
437 |
438 | );
439 | }
440 | }
441 |
442 | export default VerifyPageContainer;
443 | ```
444 |
445 | The `App` component will use `onToken` to update its state with the authentication token.
446 |
447 | ```js
448 | {
450 | localStorage.setItem("token", token);
451 | this.setState({ token });
452 | }}
453 | />
454 | ```
455 |
456 | The final file you need to create is `src/ProfilePage.js`. This file does not contain any new API that you need to know. It uses `Query`, just like `PinListPage`. It uses the `ME` query from `queries.js`. This is what this file looks like:
457 |
458 | ```js
459 | import React from "react";
460 | import { Query } from "react-apollo";
461 | import { ProfilePage } from "pinapp-components";
462 |
463 | import { ME } from "./queries";
464 |
465 | class ProfilePageContainer extends React.Component {
466 | render() {
467 | if (!this.props.authenticated) {
468 | return (
469 |
474 | );
475 | }
476 | return (
477 |
478 | {({ loading, error, data }) => {
479 | return (
480 |
488 | );
489 | }}
490 |
491 | );
492 | }
493 | }
494 |
495 | export default ProfilePageContainer;
496 | ```
497 |
498 | The last step you need to take before reaching the final version of PinApp is replacing `src/App.js` with the following code:
499 |
500 | ```js
501 | import React from "react";
502 | import ApolloClient from "apollo-boost";
503 | import { ApolloProvider } from "react-apollo";
504 | import { Container, Nav } from "pinapp-components";
505 |
506 | import PinListPage from "./PinListPage";
507 | import LoginPage from "./LoginPage";
508 | import VerifyPage from "./VerifyPage";
509 | import AddPinPage from "./AddPinPage";
510 | import ProfilePage from "./ProfilePage";
511 |
512 | const client = new ApolloClient({
513 | uri: process.env.REACT_APP_API_URL,
514 | request: operation => {
515 | if (this.state.token) {
516 | operation.setContext({
517 | headers: { Authorization: this.state.token }
518 | });
519 | }
520 | }
521 | });
522 |
523 | export default class App extends React.Component {
524 | state = {
525 | token: null
526 | };
527 | componentDidMount() {
528 | const token = localStorage.getItem("token");
529 | if (token) {
530 | this.setState({ token });
531 | }
532 | }
533 | logout = () => {
534 | localStorage.removeItem("token");
535 | this.setState({ token: null });
536 | };
537 | render() {
538 | return (
539 |
540 |
541 |
542 |
543 |
544 | {
546 | localStorage.setItem("token", token);
547 | this.setState({ token });
548 | }}
549 | />
550 |
554 |
555 |
556 |
557 | );
558 | }
559 | }
560 | ```
561 |
562 | [Remix](https://glitch.com/edit/#!/remix/pinapp-react-apollo) the final version of PinApp's client to see what it looks like.
563 |
564 | ## 4.5 Summary
565 |
566 | You created a version of PinApp using only local state, learned how to use Apollo Client and React Apollo to connect this app with a GraphQL API. You achieved this using React Apollo's `Query` and `Mutation` to easily send queries and mutations.
567 |
568 | The next chapter will teach you how to add real time functionality to apps using GraphQL Subscriptions, which let GraphQL APIs push data to clients.
569 |
570 |
--------------------------------------------------------------------------------
/manuscript/chapter-5.md:
--------------------------------------------------------------------------------
1 | # 5. Subscriptions
2 |
3 | GraphQL servers can provide a way for clients to fetch data in response to server-sent events. This enables GraphQL powered applications to push data to users in response to events.
4 |
5 | For example, you could use Subscriptions to send notifications to users when another user creates new pins.
6 |
7 | 
8 |
9 | This chapter will teach you how to implement subscriptions, both on your GraphQL API and frontend.
10 |
11 | ## 5.1 Server side subscriptions
12 |
13 | Subscriptions are implemented as a persisting connection between server and client, as opposed to queries and mutations, which are implemented as request/response actions. This means that Subscriptions use Websockets as their transport layer, instead of HTTP.
14 |
15 | To implement server-side subscriptions you need to declare a top-level Subscription type, implement a resolver for each of its fields, and finally wire up a PubSub system to handle events.
16 |
17 | ## 5.2 PubSub systems
18 |
19 | GraphQL subscriptions implementations require you to setup a PubSub adapter, but you are not tied to any particular system. You can use an in memory PubSub, Redis, RabbitMQ, Postgres and more. This is a list of the different [PubSub implementations](https://github.com/apollographql/graphql-subscriptions#pubsub-implementations).
20 |
21 | Using Postgres as a PubSub system is a great way of keeping your API simple. Postgres can serve both as a database, and as an event system.
22 |
23 | Since we are already using Knex to handle database interactions, migrating from SQLite3 to Postgres will be straightforward. Let's prepare your API for subscriptions by migrating its database to Postgres.
24 |
25 | The migration consists on creating a Postgres database, and pointing knex configuration to its URL, instead of pointing it to a SQLite file.
26 |
27 | Install the `pg` library by adding it to `package.json`'s `dependencies`.
28 |
29 | ```js
30 | "dependencies": {
31 | // ...
32 | "pg": "^7.4.3"
33 | }
34 | ```
35 |
36 | Replace all the code in `knexfile.js` with this:
37 |
38 | ```js
39 | const pg = require("pg");
40 |
41 | // Hack required to support connection strings and ssl in knex
42 | // https://github.com/tgriesser/knex/issues/852#issuecomment-229502678
43 | pg.defaults.ssl = true;
44 |
45 | let connection = process.env.DATABASE_URL;
46 |
47 | module.exports = {
48 | client: "pg",
49 | connection
50 | };
51 | ```
52 |
53 | Now, create a Postgres database. We recommend creating a [Heroku Postgres](https://www.heroku.com/postgres) databases, because they are easy to create. But you can create a Postgres instance in the way that you feel most comfortable.
54 |
55 | To create a Postgres database using Heroku, you need to create an account. After that, go to `https://dashboard.heroku.com/apps` and create a new app. Once you create the app, provision an add-on called `Heroku Postgres` by navigating to your app's resources page. You need to copy this database's URL by going to "Settings" -> "Config Vars" and copying the value of `DATABASE_URL`.
56 |
57 | Once you have the database url, place it inside your project's `.env` as `DATABASE_URL=url_that_you_just_copied`. Run `npm run setup-db` inside your project's console to verify your database connection and set up all migrations.
58 |
59 | [Remix this step's project](https://glitch.com/edit/#!/remix/pinapp-postgres) if you got stuck somewhere along the way.
60 |
61 | Database migration finished! You are now ready to implement server side subscriptions using Postgres as a PubSub system.
62 |
63 | ## 5.3 Implementing server side Subscriptions
64 |
65 | You will implement a way for clients to receive events every time a new pin is added. This event allows clients features like updating the pins list in real time, or showing a notification every time another user creates a pin.
66 |
67 | Add a new `Subscription` type to `pins/schema.graphql`. It will have a single field called `pinAdded`, which will return a `Pin`.
68 |
69 | ```graphql
70 | # ...
71 | type Subscription {
72 | pinAdded: Pin
73 | }
74 | ```
75 |
76 | After that, add a new key called `Subscription` to the resolvers list in `pins/resolvers.js`. Add a type resolver for `pinAdded`, which will be an object with a `subscribe` key. Subscriptions resolvers are not functions, like Query or Mutation resolvers, they are objects with a `subscribe` key that returns `AsyncIterable`.
77 |
78 | What is an `AsyncIterable`? Asynchronous iteration is a data access protocol, a way to iterate through asynchronous data sources. [It is an ECMAScript proposal](https://github.com/tc39/proposal-async-iteration), which means it has a high chance of becoming a part of the language. [GraphQL.js subscriptions use them because they will be a part of the JavaScript standard eventually](https://github.com/graphql/graphql-js/issues/1135#issuecomment-350928960).
79 |
80 | Anyway, you need to return an `AsyncIterable` object in subscriptions' `subscribe` functions. All subscription compatible PubSub implementations provide a method called `asyncIterator` which receives an event name and return an `AsyncIterable`.
81 |
82 | ```js
83 | Subscription: {
84 | pinAdded: {
85 | subscribe: () => {
86 | return pubsub.asyncIterator("pinAdded");
87 | };
88 | }
89 | }
90 | ```
91 |
92 | Install the Postgres subscriptions PubSub by adding the following line to `package.json`'s dependencies list:
93 |
94 | ```json
95 | "dependencies": {
96 | "graphql-postgres-subscriptions": "^1.0.1"
97 | }
98 | ```
99 |
100 | After adding this new dependency, create a new instance of it and assign it to a new variable called `pubsub`.
101 |
102 | ```js
103 | const pubsub = new PostgresPubSub({
104 | connectionString: `${process.env.DATABASE_URL}?ssl=true`
105 | });
106 | ```
107 |
108 | When a new pin is created, you can fire an event to notify all users of this new pin.
109 |
110 | ```js
111 | pubsub.publish("pinAdded", { pinAdded: createdPin });
112 | ```
113 |
114 | This is what the new Subscription resolver from `pins/resolvers.js` looks like:
115 |
116 | ```js
117 | const {
118 | PostgresPubSub
119 | } = require("graphql-postgres-subscriptions");
120 |
121 | const { addPin } = require("./index");
122 | const { verify, authorize } = require("../authentication");
123 | const database = require("../database");
124 |
125 | const pubsub = new PostgresPubSub({
126 | connectionString: `${process.env.DATABASE_URL}?ssl=true`
127 | });
128 |
129 | const resolvers = {
130 | Query: {
131 | pins: () => database("pins").select()
132 | },
133 | Mutation: {
134 | addPin: async (_, { pin }, { token }) => {
135 | const [user] = await authorize(database, token);
136 | const {
137 | user: updatedUser,
138 | pin: createdPin
139 | } = await addPin(user, pin);
140 | await database("pins").insert(createdPin);
141 | pubsub.publish("pinAdded", { pinAdded: createdPin });
142 | return createdPin;
143 | }
144 | },
145 | Subscription: {
146 | pinAdded: {
147 | subscribe: () => {
148 | return pubsub.asyncIterator("pinAdded");
149 | }
150 | }
151 | }
152 | };
153 |
154 | module.exports = resolvers;
155 | ```
156 |
157 | The final changes you need to make are in `src/server.js`. You need to add `subscriptions: true` to the Apollo Server constructor. You also need to check for the existence of `req` and `req.headers`, because subscriptions don't send a `req` object.
158 |
159 | ```js
160 | const { ApolloServer } = require('apollo-server');
161 |
162 | const schema = require('./schema');
163 |
164 | const server = new ApolloServer({
165 | schema,
166 | context: async ({ req }) => {
167 | const context = {};
168 | if (req && req.headers && req.headers.authorization) {
169 | context.token = req.headers.authorization;
170 | }
171 | return context;
172 | },
173 | subscriptions: true
174 | });
175 |
176 | server.listen().then(({ url }) => {
177 | console.log(`🚀 Server ready at ${url}`);
178 | });
179 | ```
180 |
181 | [Remix](https://glitch.com/edit/#!/remix/pinapp-subscriptions) if you need a working version of the subscriptions example.
182 |
183 | Congratulations! You just implemented server side subscriptions. Now head over to your project's GraphQL Playground by clicking "Show".
184 |
185 | Complete the authentication process by sending a `sendShortLivedToken` mutation. Copy the token you received in your email inbox, and send it as a `token` param in `createLongLivedToken`. Place the result of the last mutation as an "Authorization" header.
186 |
187 | ```json
188 | {
189 | "Authorization": "eyJhbGciOiJIUzI..."
190 | }
191 | ```
192 |
193 | Keep this tab open, and create a new tab pointing to your GraphQL Playground. Create a new subscription query and press the play button. You will see a loading screen that says "Listening...".
194 |
195 | ```graphql
196 | subscription {
197 | pinAdded {
198 | title
199 | id
200 | }
201 | }
202 | ```
203 |
204 | Go back to your first mutation, paste the following `AddPin` mutation.
205 |
206 | ```graphql
207 | mutation AddPin($pin: PinInput!) {
208 | addPin(pin: $pin) {
209 | title
210 | }
211 | }
212 | ```
213 |
214 | And fill the pin variable argument in the "Query Variables" section.
215 |
216 | ```json
217 | {
218 | "pin": {
219 | "title": "Hello subscriptions!",
220 | "link": "https://pinapp-subscriptions.glitch.me/",
221 | "image": "https://pinapp-subscriptions.glitch.me/"
222 | }
223 | }
224 | ```
225 |
226 | You should see the created pin data instead of "Loading...".
227 |
228 | ```json
229 | {
230 | "data": {
231 | "pinAdded": {
232 | "title": "Hello subscriptions!",
233 | "id": "cce1efda-b851-4272-b840-9aefbb84097f"
234 | }
235 | }
236 | }
237 | ```
238 |
239 | The next section will show you how to send subscriptions with React Apollo.
240 |
241 | ## 5.4 Client side subscriptions
242 |
243 | Apollo client supports GraphQL subscriptions. Because subscriptions is an advanced feature of GraphQL, creating a client that supports subscriptions takes a little more effort than implementing a simple client. Instead of creating an instance of `"apollo-boost"`, you will create an instance of the more configurable `ApolloClient` from `"apollo-client"`.
244 |
245 | Setting up ApolloClient from `"apollo-client"` requires a bit more knowledge about the implementation details of Apollo Client than using `apollo-boost`, more specifically knowledge about its cache and link properties.
246 |
247 | Apollo client manages the data returned from GraphQL queries in a cache. As you may know, this library does much more than just providing a nice interface for interacting with GraphQL servers. It provides advanced data management features like caching, pagination, prefetching and more. It stores all information in a cache. This cache is configurable, you can store items in memory, in a redux store, and more.
248 |
249 | The network layer of Apollo client is called Apollo link. Links direct where your data goes and where it comes from. You can use Apollo link to swap your HTTP layer with a Websockets layer, or even use mocks instead of network calls.
250 |
251 | You will learn how to migrate away from Apollo boost to the configurable Apollo client in the next section.
252 |
253 | ## 5.5 Apollo boost migration
254 |
255 | This is what client initialization looks like with Apollo Boost.
256 |
257 | ```js
258 | const client = new ApolloClient({
259 | uri: process.env.REACT_APP_API_URL,
260 | request: operation => {
261 | if (this.state.token) {
262 | operation.setContext({
263 | headers: { Authorization: this.state.token }
264 | });
265 | }
266 | }
267 | });
268 | ```
269 |
270 | Apollo client needs you to explicitly set its data store by setting a cache, and configure the network layer with links. You will use `apollo-link-http` to point HTTP requests to your API. To simulate Apollo Boost error handling, you will setup `apollo-link-error`. Finally, you need the app to keep providing dynamic request interceptors in order to add the authentication token to every request. You will achieve this creating a custom instance of `ApolloLink`.
271 |
272 | Install the new dependencies to `package.json`. Remove `apollo-boost` and add the following dependencies:
273 |
274 | ```json
275 | "dependencies": {
276 | // ...
277 | "apollo-client": "^2.3.1",
278 | "apollo-cache-inmemory": "^1.2.1",
279 | "apollo-link-http": "^1.5.4",
280 | "apollo-link-error": "^1.0.9",
281 | "apollo-link": "^1.2.2"
282 | }
283 | ```
284 |
285 | Apollo client receives a link and cache properties. Setting up the cache will be much more straightforward than setting up the link. The Apollo client constructor receives an object with `link` and `cache` properties. Import `InMemoryCache` from `apollo-cache-inmemory`, initialize `InMemoryCache` and pass it to Apollo client.
286 |
287 | ```js
288 | // ...
289 | import { ApolloClient } from "apollo-client";
290 | import { InMemoryCache } from "apollo-cache-inmemory";
291 |
292 | // ...
293 |
294 | const client = new ApolloClient({
295 | cache: new InMemoryCache()
296 | });
297 | ```
298 |
299 | Now add a link property to `new ApolloClient()`. To create a single link from the previous links, you will use `ApolloLink.from`. This function receives an array of links, merges them and returns a single link.
300 |
301 | You will pass three links to `ApolloLink.from`. The first will contain a function that runs every time there is a network error. Create it using `onError` from `apollo-link-error`. The role of this function will be to check if the error is a network or GraphQL error, and log it accordingly.
302 |
303 | ```js
304 | onError(({ graphQLErrors, networkError }) => {
305 | if (graphQLErrors)
306 | graphQLErrors.map(({ message, locations, path }) =>
307 | console.log(
308 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
309 | )
310 | );
311 | if (networkError)
312 | console.log(`[Network error]: ${networkError}`);
313 | });
314 | ```
315 |
316 | The second link will simulate Apollo Boost's request interception. The app uses this feature to insert the token in every request, so it's important to keep providing this. The implementation of this function uses Observables. You could think as Observables as a superset of Promises. Learning about Observables is outside the scope of this book, but don't worry, we will only use them in this snippet.
317 |
318 | React Apollo has great information about how to migrate from Apollo Boost. They introduce this implementation of request interceptor in [their migration docs](https://www.apollographql.com/docs/react/advanced/boost-migration.html#advanced-migration).
319 |
320 | This is how you create an `ApolloLink` that intercepts every request:
321 |
322 | ```js
323 | new ApolloLink((operation, forward) => {
324 | const request = async operation => {
325 | if (this.state.token) {
326 | operation.setContext({
327 | headers: { Authorization: this.state.token }
328 | });
329 | }
330 | };
331 | return new Observable(observer => {
332 | let handle;
333 | Promise.resolve(operation)
334 | .then(oper => request(oper))
335 | .then(() => {
336 | handle = forward(operation).subscribe({
337 | next: observer.next.bind(observer),
338 | error: observer.error.bind(observer),
339 | complete: observer.complete.bind(observer)
340 | });
341 | })
342 | .catch(observer.error.bind(observer));
343 |
344 | return () => {
345 | if (handle) handle.unsubscribe();
346 | };
347 | });
348 | });
349 | ```
350 |
351 | Luckily, to add the third link, `HttpLink`, you just need to create a new instance of it and point it to your API's URL.
352 |
353 | ```js
354 | new HttpLink({
355 | uri: process.env.REACT_APP_API_URL,
356 | credentials: "same-origin"
357 | });
358 | ```
359 |
360 | This is how the initialization of `ApolloClient` looks like after adding all links and cache:
361 |
362 | ```js
363 | import React from "react";
364 | import { Container, Nav } from "pinapp-components";
365 | import { ApolloProvider } from "react-apollo";
366 | import { ApolloClient } from "apollo-client";
367 | import { InMemoryCache } from "apollo-cache-inmemory";
368 | import { HttpLink } from "apollo-link-http";
369 | import { onError } from "apollo-link-error";
370 | import { ApolloLink, Observable } from "apollo-link";
371 |
372 | // ...
373 |
374 | const client = new ApolloClient({
375 | link: ApolloLink.from([
376 | // Simulate Apollo Boost error handling
377 | onError(({ graphQLErrors, networkError }) => {
378 | if (graphQLErrors)
379 | graphQLErrors.map(({ message, locations, path }) =>
380 | console.log(
381 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
382 | )
383 | );
384 | if (networkError)
385 | console.log(`[Network error]: ${networkError}`);
386 | }),
387 | // Enable dynamic request interceptors
388 | new ApolloLink((operation, forward) => {
389 | const request = async operation => {
390 | if (this.state.token) {
391 | operation.setContext({
392 | headers: { Authorization: this.state.token }
393 | });
394 | }
395 | };
396 | return new Observable(observer => {
397 | let handle;
398 | Promise.resolve(operation)
399 | .then(oper => request(oper))
400 | .then(() => {
401 | handle = forward(operation).subscribe({
402 | next: observer.next.bind(observer),
403 | error: observer.error.bind(observer),
404 | complete: observer.complete.bind(observer)
405 | });
406 | })
407 | .catch(observer.error.bind(observer));
408 |
409 | return () => {
410 | if (handle) handle.unsubscribe();
411 | };
412 | });
413 | }),
414 | new HttpLink({
415 | uri: process.env.REACT_APP_API_URL,
416 | credentials: "same-origin"
417 | })
418 | ]),
419 | cache: new InMemoryCache()
420 | });
421 | ```
422 |
423 | As in every important PinApp milestone, feel free to [remix this step](https://glitch.com/edit/#!/remix/pinapp-apollo-boost-migration).
424 |
425 | And that's how you migrate from Apollo Boost. The ability to compose links provides the starting point for adding subscriptions to Apollo Client. The next section will teach you how to add a websockets transport using `apollo-link-ws`.
426 |
427 | ## 5.6 Implementing client side subscriptions
428 |
429 | In this section you will add to `PinListPage` the ability to subscribe for more pins. You will achieve this using GraphQL subscriptions. The app previously used `refetchQueries` to fetch all pins once the user added a new one. This new method will be much more efficient, because you won't send a new query, but instead listen to new pins and add them individually.
430 |
431 | The first step is adding to Apollo Client the ability to determine whether an operation needs to be handled using HTTP or Websockets. To achieve this, you will use a function from `apollo-client` called [`split`](https://www.apollographql.com/docs/link/composition.html#directional). It receives three functions as argument. The first function determines whether an operation should use the link from the second argument if it returns `true`, or the second link otherwise.
432 |
433 | Replace the third element of the `ApolloLink.from` array in `src/App.js` with a `split` call that redirects subscriptions to `WebSocketLink`:
434 |
435 | ```js
436 | import { ApolloLink, Observable, split } from "apollo-link";
437 | import { WebSocketLink } from "apollo-link-ws";
438 | import { getMainDefinition } from "apollo-utilities";
439 |
440 | // ...
441 |
442 | const client = new ApolloClient({
443 | link: ApolloLink.from([
444 | // ...,
445 | // ...,
446 | split(
447 | // split based on operation type
448 | ({ query }) => {
449 | const { kind, operation } = getMainDefinition(
450 | query
451 | );
452 | return (
453 | kind === "OperationDefinition" &&
454 | operation === "subscription"
455 | );
456 | },
457 | new WebSocketLink({
458 | uri: process.env.REACT_APP_API_URL.replace(
459 | "https://",
460 | "wss://"
461 | ),
462 | options: {
463 | reconnect: true
464 | }
465 | }),
466 | new HttpLink({
467 | uri: process.env.REACT_APP_API_URL,
468 | credentials: "same-origin"
469 | })
470 | )
471 | ]),
472 | cache: new InMemoryCache()
473 | });
474 | ```
475 |
476 | Now that your client knows how to handle subscriptions, add a `pinAdded` subscription to `src/queries.js`:
477 |
478 | ```js
479 | export const PINS_SUBSCRIPTION = gql`
480 | subscription {
481 | pinAdded {
482 | title
483 | link
484 | image
485 | id
486 | user_id
487 | }
488 | }
489 | `;
490 | ```
491 |
492 | Remove the `refetchQueries` prop in `src/AddPinPage.js`. The app is going to subscribe to new pins, instead of fetching all pins when a new one is added.
493 |
494 | The last step is adding a `subscribeToMore` function to the pin list component. It will call that function when it mounts.
495 |
496 | ```js
497 | class PinListPageContainer extends React.Component {
498 | componentDidMount() {
499 | this.props.subscribeToMore();
500 | }
501 | render() {
502 | return ;
503 | }
504 | }
505 | ```
506 |
507 | You will create a new component that will send it the `subscribeToMore` function as a prop. It will pass that information, along with the list of pins, using the functions as children pattern.
508 |
509 | ```js
510 | export default () => (
511 |
512 | {({ pins, subscribeToMore }) => (
513 |
517 | )}
518 |
519 | );
520 | ```
521 |
522 | The implementation of `PinListQuery` will be very similar to the implementation of `PinListPage` from the previous version of PinApp. The main difference will be that it receives a function as children, and passes it a `subscribeToMorePins` function. It will create that function in its render method, based on a property of the object that `Query` returns, called `subscribeToMore`.
523 |
524 | [`subscribeToMore`](https://www.apollographql.com/docs/react/advanced/subscriptions.html#subscribe-to-more) uses the data it received on a query and merges it directly into the store. It receives an object with two properties, a query in its `document` key, and a function called `updateQuery`.
525 |
526 | The function that determines how to merge the new data with the existing data is called `updateQuery`. It receives two arguments, the previous data stored as its first argument and the query response on the second argument.
527 |
528 | You are going to create a function that checks whether the new data contains a `pinAdded` property. If it does, you will merge the `pins` property of the previous data with the new added pin.
529 |
530 | ```js
531 | class PinListQuery extends React.Component {
532 | render() {
533 | return (
534 |
535 | {({ loading, error, data, subscribeToMore }) => {
536 | if (loading) {
537 | return (
538 |
542 | );
543 | }
544 | if (error) {
545 | return
Error
;
546 | }
547 | const subscribeToMorePins = () => {
548 | return subscribeToMore({
549 | document: PINS_SUBSCRIPTION,
550 | updateQuery: (prev, { subscriptionData }) => {
551 | if (
552 | !subscriptionData.data ||
553 | !subscriptionData.data.pinAdded
554 | ) {
555 | return prev;
556 | }
557 | const newPinAdded =
558 | subscriptionData.data.pinAdded;
559 |
560 | return Object.assign({}, prev, {
561 | pins: [...prev.pins, newPinAdded]
562 | });
563 | }
564 | });
565 | };
566 | return this.props.children({
567 | pins: data.pins,
568 | subscribeToMore: subscribeToMorePins
569 | });
570 | }}
571 |
572 | );
573 | }
574 | }
575 | ```
576 |
577 | That's it! Now not only you will be able to see the pins you add in the list of pins, but anyone using the app will see the created pin, thanks to subscriptions.
578 |
579 | [Remix this step's project](https://glitch.com/edit/#!/remix/pinapp-client-subscriptions) if you got stuck in any step, and refer to the official [React Apollo's subscriptions documentation](https://www.apollographql.com/docs/react/advanced/subscriptions.html) to learn more about subscriptions.
580 |
581 | ## 5.7 Summary
582 |
583 | You learned what subscriptions are, how to add them both to the backend and frontend. Before adding subscriptions to your backend, you migrated your database from SQLite3 to Postgres, because it also serves as a PubSub system. You also migrated your frontend from Apollo Boost to Apollo Client before adding subscriptions.
584 |
585 | At this point, PinApp has several complex features, so adding a comprehensive test suite makes sense. The next chapter will teach you how to test your backend and frontend using different testing strategies.
586 |
587 |
--------------------------------------------------------------------------------
/manuscript/chapter-6.md:
--------------------------------------------------------------------------------
1 | # 6. Testing
2 |
3 | Testing is key when producing solid software. A solid testing suite improves development speed because it provides confidence that all features keep working after adding new functionality.
4 |
5 | This chapter will teach you how to test GraphQL APIs and clients. You will write tests that verify the behavior of all features you added in this book.
6 |
7 | Let's start by learning about API testing.
8 |
9 | ## 6.1 How to test GraphQL APIs
10 |
11 | This section will teach you how to test GraphQL APIs using two approaches. The first will test the GraphQL layer, and the second will test the HTTP layer. Both methods will use [Jest](http://facebook.github.io/jest/), a JavaScript testing library.
12 |
13 | The first approach tests the GraphQL layer by sending queries and mutations directly against the app's schema.
14 |
15 | 
16 |
17 | The second approach tests that the HTTP layer works by creating a test client that sends queries and mutations against a server.
18 |
19 | 
20 |
21 | Both methodologies have benefits. Testing the HTTP layer is a great way to verify that your API works from the point of view of HTTP clients, which are the end users of an API. The other approach, testing the GraphQL layer, is faster and simpler because it does not add any HTTP-related overhead.
22 |
23 | Which one you choose depends on your use case. It is always a good idea to test systems from the point of view of their users, so testing APIs in the HTTP layer is always a great approach. Sometimes you want faster test runs to improve developer productivity, so you decide that testing the GraphQL layer is the best approach. Remember that you can even mix and match approaches.
24 |
25 | ## 6.2 Testing setup
26 |
27 | Before creating the tests itself, you will need to make some changes so that the codebase is more testable. Right now `server.js` defines a `Server` class, initializes it and calls `server.listen()`. The first change you need to make is split definition from usage.
28 |
29 | Create a file called `index.js`. It will `require` the Server class from `server.js`, and call `listen`.
30 |
31 | ```js
32 | const Server = require("./server");
33 |
34 | const server = new Server();
35 |
36 | server.listen().then(({ url }) => {
37 | console.log(`🚀 Server ready at ${url}`);
38 | });
39 | ```
40 |
41 | Modify the `start` script from `package.json`. It will run `index.js` instead of `server.js`.
42 |
43 | ```json
44 | "scripts": {
45 | "start": "node index.js",
46 | // ...
47 | },
48 | ```
49 |
50 | Now it's time to prepare `server.js` for testing. You will modify `Server` so that you are able to setup and stop it between tests. The Server class needs to initialize in its constructor all the resources it needs, and free up those resources in its `stop` method.
51 |
52 | Initialize database and pubsub in server's constructor. At this point, database initialization happens in `database.js`, and pubsub initialization happens in `pins/resolver.js`. Now they will both happen in the `Server` constructor from `server.js`.
53 |
54 | Also create a `stop` function in the `Server` class. It will clean up the main database client and the pubsub database client. This is very important, because if you don't clear up database connections after each test run, you will have to manually stop your test suite because you will quickly run out of available database connections.
55 |
56 | ```js
57 | const { ApolloServer } = require('apollo-server');
58 | const { PostgresPubSub } = require("graphql-postgres-subscriptions");
59 | const { Client } = require("pg");
60 |
61 | const schema = require('./schema');
62 | const createDatabase = require('./database');
63 |
64 | class Server extends ApolloServer {
65 | constructor() {
66 | const database = createDatabase();
67 | const client = new Client({
68 | connectionString: process.env.NODE_ENV === "test" ?
69 | `${process.env.TEST_DATABASE_URL}?ssl=true` :
70 | `${process.env.DATABASE_URL}?ssl=true`
71 | });
72 | client.connect();
73 | const pubsub = new PostgresPubSub({
74 | client
75 | });
76 | super({
77 | schema,
78 | context: async ({ req }) => {
79 | const context = { database, pubsub };
80 | if (req && req.headers && req.headers.authorization) {
81 | context.token = req.headers.authorization;
82 | }
83 | return context;
84 | }
85 | });
86 | this.database = database;
87 | this.pubsub = pubsub;
88 | }
89 | stop() {
90 | return Promise.all([
91 | super.stop(),
92 | this.database.destroy(),
93 | this.pubsub.client.end()
94 | ]);
95 | }
96 | }
97 |
98 | module.exports = Server;
99 | ```
100 |
101 | You may have noticed a new database url, called `TEST_DATABASE_URL`. Create a new database in any provider you'd like, and assign it to `TEST_DATABASE_URL=` in `.env`. Creating Postgres databases in Heroku is free of charge.
102 |
103 | Now all resolvers can access database from their third argument, `context`. Modify all three resolvers by removing `const database = require("../database")` and accessing it from context.
104 |
105 | Modify `authentication/resolvers.js`:
106 |
107 | ```js
108 | // ...
109 | const resolvers = {
110 | Query: {
111 | users: async (_, __, { database }) => { /* */ },
112 | me: async (_, __, { token, database }) => { /* */ }
113 | },
114 | Mutation: {
115 | sendShortLivedToken: async (_, { email }, { database }) => { /* */ },
116 | createLongLivedToken: (_, { token }) => { /* */ }
117 | },
118 | Person: { /* */ },
119 | User: {
120 | pins(person, _, { database }) { /* */ }
121 | }
122 | ```
123 |
124 | Modify `pins/resolvers.js`. Remove `PostgresPubSub` initialization, because it already is in `server.js`. Access `pubsub` and `database` from resolvers' context.
125 |
126 | ```js
127 | const { addPin } = require("./index");
128 | const { verify, authorize } = require("../authentication");
129 |
130 | const resolvers = {
131 | Query: {
132 | pins: (_ , __ , { database }) => database("pins").select(),
133 | },
134 | Mutation: {
135 | addPin: async (_, { pin }, { token, database, pubsub }) => { /* */ }
136 | },
137 | Subscription: {
138 | pinAdded: {
139 | subscribe: (_, __, { pubsub }) => { /* */ }
140 | }
141 | }
142 | };
143 |
144 | module.exports = resolvers;
145 | ```
146 |
147 | Modify `search/resolvers.js`:
148 |
149 | ```js
150 | const resolvers = {
151 | Query: {
152 | search: async (_, { text }, { database }) => { /* */ }
153 | },
154 | SearchResult: {
155 | __resolveType: searchResult => { /* */ }
156 | }
157 | };
158 |
159 | module.exports = resolvers;
160 | ```
161 |
162 | Also modify `database.js` so that it exports an initialization function, instead of initializing the database and exporting its instance.
163 |
164 | ```js
165 | module.exports = () => require('knex')(require("./knexfile"));
166 | ```
167 |
168 | The final thing you need before you start writing tests is adding Jest to the `"devDependencies"` in `package.json` and also adding a `"test"` script. This script will run `jest --watchAll --runInBand`. `watchAll` reruns the test suite whenever a file changes, and `runInBand` runs all tests serially instead of concurrently. This behavior is necessary because all tests share a single database, and running all of them at the same time would result in data corruption.
169 |
170 | ```json
171 | {
172 | "scripts": {
173 | // ...
174 | "test": "jest --watchAll --runInBand"
175 | },
176 | "devDependencies": {
177 | "jest": "^22.4.3"
178 | },
179 | ```
180 |
181 | As with all examples, you can [remix the testing example](https://glitch.com/edit/#!/remix/pinapp-server-testing) in case you need to refer to a working project.
182 |
183 | ## 6.3 GraphQL layer
184 |
185 | Testing the data layer is as simple as using the `graphql` function from `graphql-js` against your schema. You will recognize this pattern, because it is the same approach you used to learn queries and mutations in Chapter 1. The only difference this time is that you will use this library in the context of a Jest test.
186 |
187 | To test queries using this approach, a good strategy is seeding the database before the first test, and cleaning it up after the last one. This allows you to write fast tests that verify multiple queries, because queries don't modify your data.
188 |
189 | Jest snapshots are a great tool to test results of GraphQL queries. Snapshots store values in JSON files from each test on the first run. On successive runs of the test suite, Jest checks that the stored values have not changed. If the snapshots changed, the test fails; otherwise, it passes.
190 |
191 | Testing GraphQL results using snapshots is great because it is low effort way to verify that everything works. You can write tests by focusing on requests, and not on responses. Focusing on JSON responses can be a lot of manual work, so delegating it to Jest makes you write tests in less time.
192 |
193 | For example to write a test that checks the behavior of the `search` query, you could create a test that calls `graphql()` with a search query, a `"text"` variable with value `"First"`, and the app's schema.
194 |
195 | You are going to use this technique to test the data layer of PinApp's. Create a file called `server.test.js` with the following code that tests users, pins and search queries:
196 |
197 | ```js
198 | const { graphql } = require("graphql");
199 |
200 | const createDatabase = require("./database");
201 | const schema = require('./schema');
202 | const { search } = require("./queries");
203 |
204 | describe("GraphQL layer", () => {
205 | let database;
206 | beforeAll(async () => {
207 | database = createDatabase();
208 | return database.seed.run();
209 | });
210 | afterAll(() => database.destroy());
211 |
212 | it("should return users' pins", () => {
213 | const query = `
214 | {
215 | users {
216 | id
217 | email
218 | pins {
219 | user_id
220 | }
221 | }
222 | }
223 | `;
224 | return graphql(schema, query, undefined, { database })
225 | .then(result => {
226 | expect(result.data.users).toMatchSnapshot();
227 | });
228 | });
229 |
230 | it("should list all pins", () => {
231 | const query = `
232 | {
233 | pins {
234 | id
235 | title
236 | link
237 | image
238 | user_id
239 | }
240 | }
241 | `;
242 | return graphql(schema, query, undefined, { database })
243 | .then(result => {
244 | expect(result.data.pins).toMatchSnapshot();
245 | });
246 | });
247 |
248 | it("should search pins by title", () => {
249 | return graphql(schema, search, undefined, { database }, { text: "First" })
250 | .then(result => {
251 | expect(result.data.search).toMatchSnapshot();
252 | });
253 | });
254 | });
255 | ```
256 |
257 | This approach is inspired by an awesome open source project called [Spectrum](https://spectrum.chat/). It has an extensive testing suite that uses Jest snapshots to test their GraphQL schema. Check out [Spectrum's github repository](https://github.com/withspectrum/spectrum/tree/e603e77bbb965bbbc7c678d9e9295e976c9381e0/api/test) to see this approach in a production codebase.
258 |
259 | Sometimes it's best to recreate the exact conditions in which users interact with a system. In this case, users are HTTP clients, not `graphql-js` clients. The next section will teach you how to test the HTTP layer of GraphQL APIs.
260 |
261 | ## 6.4 HTTP Layer
262 |
263 | To test the HTTP layer, you are going to create an instance of `Server` before each test, and stop it after each one. You are also going to delete all pins and users before each test, and delete all emails.
264 |
265 | ```js
266 | const { graphql } = require("graphql");
267 |
268 | const createDatabase = require("./database");
269 | const schema = require('./schema');
270 | const { search } = require("./queries");
271 | const Server = require("./server");
272 | const { deleteEmails } = require("./email");
273 |
274 | describe("GraphQL layer", () => { /* */ });
275 |
276 | describe("HTTP layer", () => {
277 | let server;
278 | let serverInfo;
279 |
280 | beforeEach(async () => {
281 | server = new Server();
282 | /*
283 | Ignore event emitter errors.
284 | In most cases this error appears because a database query got sent after closing database connection.
285 | */
286 | server.pubsub.ee.on("error", () => {});
287 | await Promise.all([
288 | server.database("users").del(),
289 | server.database("pins").del()
290 | ]);
291 | serverInfo = await server.listen({ http: { port: 3001 } });
292 | deleteEmails();
293 | });
294 |
295 | afterEach(() => server.stop());
296 |
297 | // Tests
298 |
299 | });
300 | ```
301 |
302 | Most of the time, the tests you can write against the HTTP layer are very similar to the tests you can write agains the GraphQL layer. For example, testing that unauthorized users cannot add pins consists of creating a query, and sending it either against an HTTP server or agains the schema directly. In this case, we are going to write it against the HTTP server, but it is a matter of choice.
303 |
304 | ```js
305 | const { graphql } = require("graphql");
306 | const fetch = require("isomorphic-unfetch");
307 |
308 | // ... Previous imports
309 | const Server = require("./server");
310 | const { deleteEmails } = require("./email");
311 |
312 | describe("GraphQL layer", () => { /* */ });
313 |
314 | describe("HTTP layer", () => {
315 | let server;
316 | let serverInfo;
317 |
318 | beforeEach(async () => { /* */ });
319 |
320 | afterEach(() => server.stop());
321 |
322 | it("should not allow unauthorized users to add pins", () => {
323 | const variables = {
324 | pin: {
325 | title: "Example",
326 | link: "http://example.com",
327 | image: "http://example.com"
328 | }
329 | };
330 | return fetch(serverInfo.url, {
331 | body: JSON.stringify({ query: addPin, variables }),
332 | headers: { "Content-Type": "application/json" },
333 | method: "POST"
334 | })
335 | .then(response => response.json())
336 | .then(response => {
337 | expect(response.errors).toMatchSnapshot();
338 | });
339 | });
340 | ```
341 |
342 | ## 6.5 Testing email based authentication
343 |
344 | Up until this point, you have been using an SMTP server like [`Ethereal`](https://ethereal.email). But there is a better option for tests, Nodemailer provides the option of creating a JSON transport. This transporter does not communicate with any other server, it just stores the list of mails as JSON objects.
345 |
346 | Modify `email.js` by setting JSON transport in tests:
347 |
348 | ```js
349 | const nodemailer = require('nodemailer');
350 |
351 | let transporter;
352 |
353 | if (process.env.NODE_ENV === "test") {
354 | transporter = nodemailer.createTransport({
355 | jsonTransport: true
356 | });
357 | } else {
358 | transporter = nodemailer.createTransport({
359 | host: 'smtp.ethereal.email',
360 | port: 587,
361 | auth: {
362 | user: process.env.MAIL_USER,
363 | pass: process.env.MAIL_PASSWORD
364 | }
365 | });
366 | }
367 |
368 | function sendMail({ from, to, subject, text, html }) {
369 | const mailOptions = {
370 | from,
371 | to,
372 | subject,
373 | text,
374 | html
375 | };
376 | return new Promise((resolve, reject) => {
377 | transporter.sendMail(mailOptions, (error, info) => {
378 | if (error) {
379 | return reject(error);
380 | }
381 | resolve(info);
382 | });
383 | });
384 | }
385 |
386 | module.exports = {
387 | sendMail
388 | };
389 | ```
390 |
391 | In order to test email authentication, you are going to need to access the list of emails sent. You can keep an array of emails sent in `email.js` and expose them. You are also going to need a way to clean up this list of emails, so you are also going to expose a function called `deleteEmails`.
392 |
393 | ```js
394 | const nodemailer = require('nodemailer');
395 |
396 | let transporter;
397 | var emails = [];
398 |
399 | if (process.env.NODE_ENV === "test") {
400 | /* */
401 | } else {
402 | /* */
403 | }
404 |
405 | function sendMail({ from, to, subject, text, html }) {
406 | const mailOptions = { /* */ };
407 | emails.push(mailOptions);
408 | return new Promise((resolve, reject) => { /* */ });
409 | }
410 |
411 | function deleteEmails() {
412 | while(emails.length > 0) {
413 | emails.pop();
414 | }
415 | }
416 |
417 | module.exports = {
418 | emails,
419 | sendMail,
420 | deleteEmails
421 | };
422 | ```
423 |
424 | To test that users can create short lived tokens, you can send a `createShortLivedToken` query agains the server, and check that it sent an email containing the user's address.
425 |
426 | ```js
427 | const { graphql } = require("graphql");
428 | const fetch = require("isomorphic-unfetch");
429 |
430 | // ... Previous imports
431 | const {
432 | search,
433 | createShortLivedToken,
434 | } = require("./queries");
435 | const Server = require("./server");
436 | const { deleteEmails, emails } = require("./email");
437 |
438 | describe("GraphQL layer", () => { /* */ });
439 |
440 | describe("HTTP layer", () => {
441 | let server;
442 | let serverInfo;
443 |
444 | beforeEach(async () => { /* */ });
445 |
446 | afterEach(() => server.stop());
447 |
448 | // ...
449 |
450 | it("should allow users to create short lived tokens", () => {
451 | const email = "name@example.com";
452 | const variables = {
453 | email
454 | };
455 | return fetch(serverInfo.url, {
456 | body: JSON.stringify({ query: createShortLivedToken, variables }),
457 | headers: { "Content-Type": "application/json" },
458 | method: "POST"
459 | })
460 | .then(response => response.json())
461 | .then(response => {
462 | expect(emails[emails.length - 1].to).toEqual(email)
463 | });
464 | });
465 | });
466 | ```
467 |
468 | Testing that users can create long lived token is a little more complex. The strategy for testing this would be to first create a short lived token, then parse the token from the email sent and send it to the server as a `"token"` variable, along with a `createLongLivedToken` query.
469 |
470 | To parse the token, you are going to use Node API's [`url.parse`](https://nodejs.org/docs/latest/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost) function. When you pass it a URL as a first argument, and `true` as the second, it returns a query object. Parsing the url sent in the email message will contain a `token` key.
471 |
472 | To verify that the long lived token generated with `createLongLivedToken` is valid, you are going to use the `verify` function from `authenticate/index.js`. It returns the token data, or an error if the token is not valid. Checking that the token's email is the same as the user's email will be enough to verify that authentication works.
473 |
474 | ```js
475 | const { graphql } = require("graphql");
476 | const fetch = require("isomorphic-unfetch");
477 | const url = require("url");
478 |
479 | // ... Previous imports
480 | const {
481 | search,
482 | createShortLivedToken,
483 | createLongLivedToken,
484 | } = require("./queries");
485 | const Server = require("./server");
486 | const { deleteEmails, emails } = require("./email");
487 | const { verify } = require("./authentication");
488 |
489 | describe("GraphQL layer", () => { /* */ });
490 |
491 | describe("HTTP layer", () => {
492 | let server;
493 | let serverInfo;
494 |
495 | beforeEach(async () => { /* */ });
496 |
497 | afterEach(() => server.stop());
498 |
499 | // ...
500 |
501 | it("should allow users to create long lived tokens", () => {
502 | const email = "name@example.com";
503 | const variables = {
504 | email
505 | };
506 | return fetch(serverInfo.url, {
507 | body: JSON.stringify({ query: createShortLivedToken, variables }),
508 | headers: { "Content-Type": "application/json" },
509 | method: "POST"
510 | })
511 | .then(response => response.json())
512 | .then(response => {
513 | const token = url.parse(emails[emails.length - 1].text, true).query.token;
514 | return fetch(serverInfo.url, {
515 | body: JSON.stringify({ query: createLongLivedToken, variables: { token } }),
516 | headers: { "Content-Type": "application/json" },
517 | method: "POST"
518 | })
519 | })
520 | .then(response => response.json())
521 | .then(response => {
522 | expect(verify(response.data.createLongLivedToken).email).toEqual(email);
523 | });
524 | });
525 | });
526 | ```
527 |
528 | Testing that the app returns the current authenticated user consists of checking that the `me` query works. In order to test this, you need to simulate a login flow by creating a short lived token and exchanging it with a long lived one, finally passing it to the `me` query.
529 |
530 | ```js
531 | it("should return authenticated user", () => {
532 | const email = "name@example.com";
533 | const variables = {
534 | email
535 | };
536 | let token;
537 | return fetch(serverInfo.url, {
538 | body: JSON.stringify({ query: createShortLivedToken, variables }),
539 | headers: { "Content-Type": "application/json" },
540 | method: "POST"
541 | })
542 | .then(response => response.json())
543 | .then(response => {
544 | token = url.parse(emails[emails.length - 1].text, true).query.token;
545 | return fetch(serverInfo.url, {
546 | body: JSON.stringify({ query: createLongLivedToken, variables: { token } }),
547 | headers: { "Content-Type": "application/json" },
548 | method: "POST"
549 | })
550 | })
551 | .then(response => response.json())
552 | .then(response => {
553 | return fetch(serverInfo.url, {
554 | body: JSON.stringify({ query: me }),
555 | headers: { "Content-Type": "application/json", Authorization: token },
556 | method: "POST"
557 | });
558 | })
559 | .then(response => response.json())
560 | .then(response => {
561 | expect(response.data).toMatchSnapshot();
562 | });
563 | });
564 | ```
565 |
566 | Another test that needs a complete login flow is checking that authenticated users can create pins. To test this, complete a login flow and send a long lived token, along with the `addPin` query to the server.
567 |
568 | ```js
569 | it("should allow authenticated users to create pins", () => {
570 | const email = "name@example.com";
571 | const variables = {
572 | email
573 | };
574 | let token;
575 | return fetch(serverInfo.url, {
576 | body: JSON.stringify({ query: createShortLivedToken, variables }),
577 | headers: { "Content-Type": "application/json" },
578 | method: "POST"
579 | })
580 | .then(response => response.json())
581 | .then(response => {
582 | token = url.parse(emails[emails.length - 1].text, true).query.token;
583 | return fetch(serverInfo.url, {
584 | body: JSON.stringify({ query: createLongLivedToken, variables: { token } }),
585 | headers: { "Content-Type": "application/json" },
586 | method: "POST"
587 | })
588 | })
589 | .then(response => response.json())
590 | .then(response => {
591 | const pin = {
592 | title: "Example",
593 | link: "http://example.com",
594 | image: "http://example.com"
595 | };
596 | return fetch(serverInfo.url, {
597 | body: JSON.stringify({ query: addPin, variables: { pin } }),
598 | headers: { "Content-Type": "application/json", Authorization: token },
599 | method: "POST"
600 | });
601 | })
602 | .then(response => response.json())
603 | .then(response => {
604 | expect(response.data).toMatchSnapshot();
605 | });
606 | });
607 | ```
608 |
609 | This test completes all authentication related tests. The following section will teach you how to verify that subscriptions work in your API.
610 |
611 | ## 6.6 Subscription endpoints
612 |
613 | To test GraphQL Subscriptions you need a Websockets client, in the same way that you need an HTTP client to test queries and mutations. In this section you are going to use a Websockets subscriptions client from the [`"subscriptions-transport-ws" library`](https://github.com/apollographql/subscriptions-transport-ws).
614 |
615 | The first step is adding this library to `package.json`'s `"dependencies"`.
616 |
617 | ```json
618 | {
619 | "dependencies": {
620 | // ...
621 | "subscriptions-transport-ws": "^0.9.9"
622 | }
623 | }
624 | ```
625 |
626 | Testing a subscription query (like `pinAdded` from PinApp schema) involves pointing an instance of `SubscriptionClient` to a subscriptions url, sending the query and checking that the result is valid.
627 |
628 | To test `pinAdded` you need to simulate a login flow and create a pin. You are going to put this logic in a helper function called `authenticateAndAddPin`. It contains almost the same steps as the add pin test.
629 |
630 | ```js
631 | const { graphql } = require("graphql");
632 | const fetch = require("isomorphic-unfetch");
633 | const url = require("url");
634 | const { SubscriptionClient } = require("subscriptions-transport-ws");
635 |
636 | // ...
637 |
638 | describe("HTTP layer", () => {
639 | // ...
640 | it("should subscribe to pins", done => {
641 | const subscriptionClient = new SubscriptionClient(
642 | serverInfo.url.replace("http://", "ws://"),
643 | {
644 | reconnect: true,
645 | connectionCallback: error => {
646 | if (error) {
647 | done(error);
648 | }
649 | }
650 | }
651 | );
652 | subscriptionClient.on("connected", () => {
653 | subscriptionClient
654 | .request({
655 | query: pinsSubscription
656 | })
657 | .subscribe({
658 | next: result => {
659 | expect(result).toMatchSnapshot();
660 | done();
661 | },
662 | error: done
663 | });
664 | authenticateAndAddPin(serverInfo.url);
665 | });
666 | subscriptionClient.on("error", done);
667 | });
668 | });
669 |
670 | function authenticateAndAddPin(serverUrl) {
671 | const email = "name@example.com";
672 | const variables = {
673 | email
674 | };
675 | let token;
676 | return fetch(serverUrl, {
677 | body: JSON.stringify({ query: createShortLivedToken, variables }),
678 | headers: { "Content-Type": "application/json" },
679 | method: "POST"
680 | })
681 | .then(response => {
682 | token = url.parse(emails[emails.length - 1].text, true).query.token;
683 | const pin = {
684 | title: "Example",
685 | link: "http://example.com",
686 | image: "http://example.com"
687 | };
688 | return fetch(serverUrl, {
689 | body: JSON.stringify({ query: addPin, variables: { pin } }),
690 | headers: { "Content-Type": "application/json", Authorization: token },
691 | method: "POST"
692 | })
693 | .then(response => response.json());
694 | })
695 | .then(response => {
696 | if (response.errors) {
697 | throw new Error(response.errors[0].message);
698 | }
699 | })
700 | }
701 | ```
702 |
703 | This is the final step in testing PinApp's API. The next sections will teach you how to test GraphQL clients, more specifically how to test Apollo GraphQL clients.
704 |
705 | ## 6.7 How to test React Apollo GraphQL clients
706 |
707 | In this chapter you will learn how to test React Apollo clients. To do this, you will use [Jest](https://facebook.github.io/jest/) as a test runner, [Enzyme](https://github.com/airbnb/enzyme/) because it provides testing tools for React, and React Apollo's testing utilities.
708 |
709 | To test the network layers, you are going to take advantage of the fact that Apollo GraphQL's network layer is configurable using [Apollo Link](https://www.apollographql.com/docs/react/advanced/network-layer.html). The strategy is swapping the Provider defined in `src/App.js` with a `MockedProvider`. This Provider is useful for testing purposes because it does not communicate with any server, instead it receives an array of mocks that it uses for sending GraphQL responses. If `MockedProvider` has a mock that corresponds to a request, it sends the mock's response. If no mock matches a request, it throws an error.
710 |
711 | As with all steps, you have the chance to [remix the current example](https://glitch.com/edit/#!/remix/pinapp-client-testing) in case you need any help.
712 |
713 | Let's write a basic test. You may have seen this test a bunch of times if you are used to bootstrapping apps using [`create-react-app`](https://github.com/facebook/create-react-app). This test verifies that the app renders without crashing. To stop the app from making network requests, you will use Jest to replace `ApolloProvider` with a dummy component. You will also wrap the app with React Router's `MemoryRouter`, because Jest runs in Node, not in the browser.
714 |
715 | Create a file called `src/App.test.js` with the following contents:
716 |
717 | ```js
718 | import React from "react";
719 | import ReactDOM from "react-dom";
720 | import { MockedProvider } from "react-apollo/test-utils";
721 | import * as ReactRouter from "react-router";
722 | import * as ReactApollo from "react-apollo";
723 |
724 | const MemoryRouter = ReactRouter.MemoryRouter;
725 |
726 | ReactApollo.ApolloProvider = jest.fn(({ children }) =>
{children}
);
727 |
728 | import App from "./App";
729 |
730 | it("renders without crashing", () => {
731 | const div = document.createElement("div");
732 | ReactDOM.render(
733 |
734 |
735 |
736 |
737 | ,
738 | div
739 | );
740 | ReactDOM.unmountComponentAtNode(div);
741 | });
742 | ```
743 |
744 | You also need to pass a property called `noRouter` to `pinapp-component`'s `Container`. Otherwise it will try to use a Router implementation which depends on the browser's history API, which is not available in Node. Pass `noRouter={process.env.NODE_ENV === "test"}` to `Container` in `src/App.js`
745 |
746 | ```js
747 | // ...
748 | export default class App extends React.Component {
749 | // ...
750 | render() {
751 | return (
752 |
753 |
754 | {/* */}
755 |
756 |
757 | );
758 | }
759 | ```
760 |
761 | Finally install `react-router` by adding it to `package.json`. Note that the previous test will work whether or not you install `react-router`. This happens because `pinapp-components` already has React Router as a dependency. But now React Router is also a dependency of your app, because you use `MemoryRouter` in your tests.
762 |
763 | You also need to install `jest-cli` if you are following the examples on glitch. This is a temporary workaround because of a bug in `pnpm`, which is the package manager that Glitch uses. It is similar to NPM or Yarn, but much more disk efficient because it uses symlinks instead of installing duplicated packages. You normally don't need to install Jest if you are using `react-scripts` with Yarn or NPM, so skip `jest-cli` if you are developing outside of Glitch.
764 |
765 | ```json
766 | {
767 | "dependencies": {
768 | // ...
769 | "jest-cli": "23.0.1",
770 | "react-router": "^4.2.0"
771 | }
772 | }
773 | ```
774 |
775 | Run the test suite by opening the console and running `npm test`.
776 |
777 | Now let's write a test based on a use case of the app. You are going to verify that the app shows the text "There are no pins yet" initially.
778 |
779 | Instead of using React to render the App, you will use Enzyme's `mount` function. It performs a full DOM rendering. just like calling `ReactDOM.render`, the difference is that choosing `mount` allows you to use Enzyme's querying and expectations capabilities.
780 |
781 | You will pass a mock list instead of an empty array to `MockedProvider`. Mocks are object with two keys, `request` and `result`. `request` is an object that has a `query` key and can have a `variables` key. `result` contains a JavaScript object that simulates the server's response. In this case mocks will consist of two elements, the first simulates a `LIST_PINS` query with a list of empty pins as response, and the second simulates a `PINS_SUBSCRIPTION` query with no pin as a response. These are the two requests that App sends when it starts.
782 |
783 | ```js
784 | // ...
785 | import {
786 | LIST_PINS,
787 | PINS_SUBSCRIPTION,
788 | CREATE_SHORT_LIVED_TOKEN,
789 | CREATE_LONG_LIVED_TOKEN,
790 | ME,
791 | ADD_PIN
792 | } from "./queries";
793 |
794 | it("shows 'There are no pins yet' initially", async () => {
795 | const mocks = [
796 | {
797 | request: { query: LIST_PINS },
798 | result: {
799 | data: {
800 | pins: []
801 | }
802 | }
803 | },
804 | {
805 | request: {
806 | query: PINS_SUBSCRIPTION
807 | },
808 | result: { data: { pinAdded: null } }
809 | }
810 | ];
811 | const wrapper = mount(
812 |
813 |
814 |
815 |
816 |
817 | );
818 | // Wait for async pins query
819 | await wait();
820 | // Manually update enzyme wrapper
821 | // https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#for-mount-updates-are-sometimes-required-when-they-werent-before)
822 | wrapper.update();
823 | expect(
824 | wrapper.contains(node => node.text() === "There are no pins yet.")
825 | ).toBe(true);
826 | wrapper.unmount();
827 | });
828 | ```
829 |
830 | Another useful test would be verifying that the app shows a list of pins when it receives a non empty pins response. The test structure for doing this is very similar to the previous test, with the difference that the `LIST_PINS` query will contain a list of pins in the response. This test will verify that there is an element with class pins that has three elements with class pin.
831 |
832 | ```js
833 | it("should show a list of pins", async () => {
834 | const pins = [
835 | {
836 | id: "1",
837 | title: "Modern",
838 | link: "https://pinterest.com/pin/637540890973869441/",
839 | image:
840 | "https://i.pinimg.com/564x/5a/22/2c/5a222c93833379f00777671442df7cd2.jpg"
841 | },
842 | {
843 | id: "2",
844 | title: "Broadcast Clean Titles",
845 | link: "https://pinterest.com/pin/487585097141051238/",
846 | image:
847 | "https://i.pinimg.com/564x/85/ce/28/85ce286cba63daf522464a7d680795ba.jpg"
848 | },
849 | {
850 | id: "3",
851 | title: "Drawing",
852 | link: "https://pinterest.com/pin/618611698790230574/",
853 | image:
854 | "https://i.pinimg.com/564x/00/7a/2e/007a2ededa8b0ce87e048c60fa6f847b.jpg"
855 | }
856 | ];
857 | const mocks = [
858 | {
859 | request: { query: LIST_PINS },
860 | result: {
861 | data: {
862 | pins
863 | }
864 | }
865 | },
866 | {
867 | request: {
868 | query: PINS_SUBSCRIPTION
869 | },
870 | result: { data: { pinAdded: null } }
871 | }
872 | ];
873 | const wrapper = mount(
874 |
875 |
876 |
877 |
878 |
879 | );
880 | await wait();
881 | wrapper.update();
882 | expect(wrapper.find(".pins .pin").length).toBe(3);
883 | wrapper.unmount();
884 | });
885 | ```
886 |
887 | ## 6.8 Testing client-side authentication
888 |
889 | The login flow consists of two steps. The first is when the user clicks login, and then fill the email input with an email address, clicking submit afterwards. The second step happens when the user clicks the link in the received email, going to `/verify?token=123456`, which will authenticate the user if the token is valid.
890 |
891 | To test the first step, let's write a test that simulates the action that the user needs to take in order to receive a magic link in its email address. The first action is clicking the login button in the app's footer, which will redirect the user to the login page.
892 |
893 | ```js
894 | wrapper.find('a[href="/login"]').simulate("click", { button: 0 });
895 | ```
896 |
897 | To simulate user's actions, you will use an Enzyme function called `prop`. This function allows you to access properties from React components. In this case, it will be useful to access the `onChange` function from the email input, and the `onSubmit` function from the email form.
898 |
899 | The app will need a mock that will handle the API call when the user sends a `CREATE_SHORT_LIVED_TOKEN` mutation, so you will add this mock to the list. If you don't add this mock, the test will fail.
900 |
901 | Finally this test will verify that the app shows a an "Email sent" message.
902 |
903 | ```js
904 | it("should allow users to login", async () => {
905 | const email = "name@example.com";
906 | const mocks = [
907 | {
908 | request: { query: LIST_PINS },
909 | result: {
910 | data: {
911 | pins: []
912 | }
913 | }
914 | },
915 | {
916 | request: {
917 | query: PINS_SUBSCRIPTION
918 | },
919 | result: { data: { pinAdded: null } }
920 | },
921 | {
922 | request: {
923 | query: CREATE_SHORT_LIVED_TOKEN,
924 | variables: {
925 | email
926 | }
927 | },
928 | result: {
929 | data: {
930 | sendShortLivedToken: true
931 | }
932 | }
933 | }
934 | ];
935 | const wrapper = mount(
936 |
937 |
938 |
939 |
940 |
941 | );
942 | await wait();
943 | wrapper.update();
944 | expect(wrapper.find(".auth-banner").length).toBe(1);
945 | expect(wrapper.find('a[href="/profile"]').length).toBe(0);
946 | wrapper.find('a[href="/login"]').simulate("click", { button: 0 }); // Add { button: 0 } because of React Router bug https://github.com/airbnb/enzyme/issues/516
947 | wrapper
948 | .find("#email")
949 | .first()
950 | .prop("onChange")({ value: email });
951 | await wait();
952 | wrapper.update();
953 | wrapper.find("form").prop("onSubmit")({ preventDefault: () => {} });
954 | await wait();
955 | wrapper.update();
956 | expect(
957 | wrapper.contains(
958 | node =>
959 | node.text() === `We sent an email to ${email}. Please check your inbox.`
960 | )
961 | ).toBe(true);
962 | wrapper.unmount();
963 | });
964 | ```
965 |
966 | To test that the app authenticates users who enter the verify page, you will use a property from `MemoryRouter` called `initialEntries`. This property receives an array of URLs, so passing it `['/verify?token=${token}']` will start the app on the Verify page.
967 |
968 | The list of mocks will need a response for the `CREATE_LONG_LIVED_TOKEN` query, containing a string that represents the auth token.
969 |
970 | To verify that the authentication works, you will simulate a user who enters to the Profile page after a successful authentication. This is why you will add a response to the `ME` query to the list of mocks. Checking that the app shows the user's email is enough to verify that this test works.
971 |
972 | ```js
973 | it("should authenticate users who enter verify page", async () => {
974 | const email = "name@example.com";
975 | const token = "5minutes";
976 | const mocks = [
977 | {
978 | request: { query: LIST_PINS },
979 | result: {
980 | data: {
981 | pins: []
982 | }
983 | }
984 | },
985 | {
986 | request: {
987 | query: PINS_SUBSCRIPTION
988 | },
989 | result: { data: { pinAdded: null } }
990 | },
991 | {
992 | request: {
993 | query: CREATE_LONG_LIVED_TOKEN,
994 | variables: {
995 | token
996 | }
997 | },
998 | result: {
999 | data: {
1000 | createLongLivedToken: "30days"
1001 | }
1002 | }
1003 | },
1004 | {
1005 | request: { query: ME },
1006 | result: {
1007 | data: {
1008 | me: { email }
1009 | }
1010 | }
1011 | }
1012 | ];
1013 | const initialEntries = [`/verify?token=${token}`];
1014 | const wrapper = mount(
1015 |
1016 |
1017 |
1018 |
1019 |
1020 | );
1021 | await wait();
1022 | wrapper.update();
1023 | // Verify Page shows "Success!" for 1 second (1000 ms), then redirects to "/"
1024 | await wait(1000);
1025 | wrapper.update();
1026 | wrapper.find('a[href="/profile"]').simulate("click", { button: 0 });
1027 | await wait();
1028 | wrapper.update();
1029 | expect(
1030 | wrapper.find(".profile-page").contains(node => node.text() === email)
1031 | ).toBe(true);
1032 | wrapper.unmount();
1033 | });
1034 | ```
1035 |
1036 | In the next step you will learn how to test client side subscriptions by creating a test that verifies that users can add pins.
1037 |
1038 | ## 6.9 Client subscriptions
1039 |
1040 | MockedProvider is perfect for mocking request/response pairs, but it does not provide a way of testing server sent events, like subscriptions. Fortunately, React Apollo provides the tools you need to mock server sent events with `MockSubscriptionLink`.
1041 |
1042 | To simulate subscription results, you can create an instance of `MockSubscriptionLink` and use a function called `simulateResult`.
1043 |
1044 | ```js
1045 | subscriptionsLink.simulateResult({
1046 | result: {
1047 | data: {
1048 | pinAdded: {
1049 | title,
1050 | link,
1051 | image,
1052 | id: "1"
1053 | }
1054 | }
1055 | }
1056 | });
1057 | ```
1058 |
1059 | The strategy for testing subscriptions will be creating a custom MockContainer, and accessing `subscriptionsLink` by exposing it as a class property. This allows you to call `simulateResult` anywhere in the test.
1060 |
1061 | This MockContainer will have the same API and implementation as React Apollo's `MockProvider`. It will receive a list of mocks and create a `MockLink` using this list. It will merge this link with an instance of `MockSubscriptionLink` using `split`. To determine which link `MockSubscriptionsProvider` uses, you are going to define the same logic that you used to decide between `HttpLink` and `WebsocketLink` in `src/App.js`.
1062 |
1063 | Import the new dependencies and define a class called `MockSubscriptionLink` at the end of `src/App.test.js`.
1064 |
1065 | ```js
1066 | // ...
1067 | import {
1068 | MockedProvider,
1069 | MockLink,
1070 | MockSubscriptionLink
1071 | } from "react-apollo/test-utils";
1072 | import { InMemoryCache as Cache } from "apollo-cache-inmemory";
1073 | import { getMainDefinition } from "apollo-utilities";
1074 | import { split } from "apollo-link";
1075 | import ApolloClient from "apollo-client";
1076 |
1077 | const ApolloProvider = ReactApollo.ApolloProvider;
1078 | const MemoryRouter = ReactRouter.MemoryRouter;
1079 |
1080 | ReactRouter.Router = jest.fn(({ children }) =>
{children}
);
1081 | ReactApollo.ApolloProvider = jest.fn(({ children }) =>
{children}
);
1082 |
1083 | // ...
1084 |
1085 | it("should allow logged in users to add pins", async () => { /* */ });
1086 |
1087 | class MockedSubscriptionsProvider extends React.Component {
1088 | constructor(props, context) {
1089 | super(props, context);
1090 | const subscriptionsLink = new MockSubscriptionLink();
1091 | const addTypename = false;
1092 | const mocksLink = new MockLink(props.mocks, addTypename);
1093 | const link = split(
1094 | // split based on operation type
1095 | ({ query }) => {
1096 | const { kind, operation } = getMainDefinition(query);
1097 | return kind === "OperationDefinition" && operation === "subscription";
1098 | },
1099 | subscriptionsLink,
1100 | mocksLink
1101 | );
1102 | const client = new ApolloClient({
1103 | link,
1104 | cache: new Cache({ addTypename })
1105 | });
1106 | this.client = client;
1107 | this.subscriptionsLink = subscriptionsLink;
1108 | }
1109 | render() {
1110 | return (
1111 |
1112 | {this.props.children}
1113 |
1114 | );
1115 | }
1116 | }
1117 | ```
1118 |
1119 | Now it's time to verify that logged in users can create pins, and the new pins appear in the list. This test will perform the same initial steps as the previous authentication tests. It will differ with those tests once it authenticates a user, because it will navigate to the add pin page instead of the profile.
1120 |
1121 | Once the user is in the add pin page, it will simulate the user filling out the new pin form and clicking "Add". For this to complete successfully. you will add a mock for the `ADD_PIN` query to the mocks list.
1122 |
1123 | After this, the test will simulate a new subscription result by accessing the `subscriptionsLink` from the `MockedSubscriptionsProvider` instance and calling `simulateResult` with a new pin.
1124 |
1125 | The test will check that this new pin appears in the pins list by using `expect(wrapper.find(".pins .pin").length).toBe(1);`.
1126 |
1127 | ```js
1128 | it("should allow logged in users to add pins", async () => {
1129 | const title = "GraphQL College";
1130 | const link = "http://graphql.college";
1131 | const image = "http://www.graphql.college/fullstack-graphql";
1132 | const email = "name@example.com";
1133 | const token = "5minutes";
1134 | const mocks = [
1135 | {
1136 | request: { query: LIST_PINS },
1137 | result: {
1138 | data: {
1139 | pins: []
1140 | }
1141 | }
1142 | },
1143 | {
1144 | request: {
1145 | query: PINS_SUBSCRIPTION
1146 | },
1147 | result: { data: { pinAdded: null } }
1148 | },
1149 | {
1150 | request: {
1151 | query: CREATE_LONG_LIVED_TOKEN,
1152 | variables: {
1153 | token
1154 | }
1155 | },
1156 | result: {
1157 | data: {
1158 | createLongLivedToken: "30days"
1159 | }
1160 | }
1161 | },
1162 | {
1163 | request: { query: ME },
1164 | result: {
1165 | data: {
1166 | me: { email }
1167 | }
1168 | }
1169 | },
1170 | {
1171 | request: {
1172 | query: ADD_PIN,
1173 | variables: {
1174 | pin: {
1175 | title,
1176 | link,
1177 | image
1178 | }
1179 | }
1180 | },
1181 | result: {
1182 | data: {
1183 | addPin: {
1184 | title,
1185 | link,
1186 | image
1187 | }
1188 | }
1189 | }
1190 | }
1191 | ];
1192 | const initialEntries = [`/verify?token=${token}`];
1193 | const wrapper = mount(
1194 |
1195 |
1196 |
1197 |
1198 |
1199 | );
1200 | await wait();
1201 | wrapper.update();
1202 | await wait(1000);
1203 | wrapper.update();
1204 | wrapper
1205 | .find('a[href="/upload-pin"]')
1206 | .first()
1207 | .simulate("click", { button: 0 });
1208 | wrapper.update();
1209 | wrapper
1210 | .find('[placeholder="Title"]')
1211 | .first()
1212 | .prop("onChange")({ target: { value: title } });
1213 | wrapper
1214 | .find('[placeholder="URL"]')
1215 | .first()
1216 | .prop("onChange")({ target: { value: link } });
1217 | wrapper
1218 | .find('[placeholder="Image URL"]')
1219 | .first()
1220 | .prop("onChange")({ target: { value: image } });
1221 | wrapper.update();
1222 | wrapper.find("form").prop("onSubmit")({ preventDefault: () => {} });
1223 | const subscriptionsLink = wrapper.find(MockedSubscriptionsProvider).instance()
1224 | .subscriptionsLink;
1225 | subscriptionsLink.simulateResult({
1226 | result: {
1227 | data: {
1228 | pinAdded: {
1229 | title,
1230 | link,
1231 | image,
1232 | id: "1"
1233 | }
1234 | }
1235 | }
1236 | });
1237 | await wait(1000);
1238 | wrapper.update();
1239 | expect(wrapper.find(".pins .pin").length).toBe(1);
1240 | wrapper.unmount();
1241 | });
1242 | ```
1243 |
1244 | Testing subscriptions is very straightforward once you can simulate results using `MockSubscriptionLink`.
1245 |
1246 | ## 6.9 Summary
1247 |
1248 | In this chapter you learned how to test GraphQL APIs and React Apollo clients.
1249 |
1250 | You used two different strategies to write API tests, once that tests the GraphQL layer and another that tests the HTTP layer. To write expectations, you used Jest snapshots in some cases and manual expectations in other occasions.
1251 |
1252 | You tested queries and mutations in React Apollo clients using `MockedProvider`. You also learned how to test subscriptions by using `MockSubscriptionLink` to simulate server sent events.
1253 |
1254 | Now you are ready to apply this techniques to verify the correct behavior of your GraphQL Applications.
1255 |
--------------------------------------------------------------------------------
/manuscript/images/addpin-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/addpin-page.png
--------------------------------------------------------------------------------
/manuscript/images/api.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/api.png
--------------------------------------------------------------------------------
/manuscript/images/business-logic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/business-logic.png
--------------------------------------------------------------------------------
/manuscript/images/client-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/client-server.png
--------------------------------------------------------------------------------
/manuscript/images/client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/client.png
--------------------------------------------------------------------------------
/manuscript/images/current-file-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/current-file-structure.png
--------------------------------------------------------------------------------
/manuscript/images/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/database.png
--------------------------------------------------------------------------------
/manuscript/images/final-file-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/final-file-structure.png
--------------------------------------------------------------------------------
/manuscript/images/graphql-playground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/graphql-playground.png
--------------------------------------------------------------------------------
/manuscript/images/graphql-schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/graphql-schema.png
--------------------------------------------------------------------------------
/manuscript/images/http.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/http.png
--------------------------------------------------------------------------------
/manuscript/images/login-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/login-page.png
--------------------------------------------------------------------------------
/manuscript/images/pinlist-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/pinlist-page.png
--------------------------------------------------------------------------------
/manuscript/images/profile-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/profile-page.png
--------------------------------------------------------------------------------
/manuscript/images/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/schema.png
--------------------------------------------------------------------------------
/manuscript/images/server-layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/server-layers.png
--------------------------------------------------------------------------------
/manuscript/images/server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/server.png
--------------------------------------------------------------------------------
/manuscript/images/subscriptions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/subscriptions.png
--------------------------------------------------------------------------------
/manuscript/images/title_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/title_page.png
--------------------------------------------------------------------------------
/manuscript/images/types-resolvers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/types-resolvers.png
--------------------------------------------------------------------------------
/manuscript/images/verify-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GraphQLCollege/fullstack-graphql-ru/faa0319a2e9e3deaa9023ad81aae7b9516c1a884/manuscript/images/verify-page.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fullstack-graphql",
3 | "description": "Book about fullstack GraphQL development with React and Node",
4 | "homepage": "http://graphql.college/fullstack-graphql",
5 | "author": {
6 | "email": "mayorga.julian@gmail.com",
7 | "name": "Julian Mayorga"
8 | },
9 | "license": "CC-BY-NC-SA-4.0"
10 | }
11 |
--------------------------------------------------------------------------------
/styles/highlight.min.css:
--------------------------------------------------------------------------------
1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}
--------------------------------------------------------------------------------
/styles/styles.css:
--------------------------------------------------------------------------------
1 | @page {
2 | margin: 0;
3 | }
4 | body {
5 | margin: 0;
6 | }
7 | /* this stylesheet is used when generating a PDF with PrinceXML or any other tool that understands the CSS used. */
8 | /* define a page */
9 | @page {
10 | size: 900px 1350px;
11 | margin: 100pt 70pt 100pt;
12 | }
13 |
14 | @page:first {
15 | margin: 0;
16 | }
17 |
18 | img {
19 | max-width: 100%;
20 | }
21 |
22 | img:first-of-type {
23 | margin: 0 auto;
24 | display: block;
25 | object-fit: cover;
26 | }
27 |
28 | p:first-child {
29 | margin: 0;
30 | padding: 0;
31 | }
32 |
33 | /* styles for the right hand spread
34 | Bottom left we display the title of the book, bottom right the page using a CSS counter, top right the content of the current chapter */
35 | @page:right {
36 | @bottom-left {
37 | margin: 10pt 0 30pt 0;
38 | border-top: 0.25pt solid #666;
39 | content: string(booktitle);
40 | font-size: 9pt;
41 | color: #333;
42 | }
43 | @bottom-right {
44 | margin: 10pt 0 30pt 0;
45 | border-top: 0.25pt solid #666;
46 | content: counter(page);
47 | font-size: 9pt;
48 | }
49 | @top-right {
50 | content: string(doctitle);
51 | margin: 30pt 0 10pt 0;
52 | font-size: 9pt;
53 | color: #333;
54 | }
55 | }
56 |
57 | /* styles for the left hand spread
58 | Bottom right book title, bottom left current page */
59 | @page:left {
60 | @bottom-right {
61 | margin: 10pt 0 30pt 0;
62 | border-top: 0.25pt solid #666;
63 | content: string(booktitle);
64 | font-size: 9pt;
65 | color: #333;
66 | }
67 | @bottom-left {
68 | margin: 10pt 0 30pt 0;
69 | border-top: 0.25pt solid #666;
70 | content: counter(page);
71 | font-size: 9pt;
72 | }
73 | }
74 |
75 | /* first page */
76 | @page:first {
77 | @bottom-right {
78 | content: normal;
79 | margin: 0;
80 | }
81 |
82 | @bottom-left {
83 | content: normal;
84 | margin: 0;
85 | }
86 | }
87 |
88 | /* reset chapter and figure counters on the body */
89 | body {
90 | counter-reset: chapternum figurenum;
91 | font-family: 'Rubik', 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
92 | line-height: 1.5;
93 | font-size: 11pt;
94 | }
95 |
96 | /* Grab the first h1 and set the string booktitle. bookttitle will be used on footers. */
97 | h1:first-of-type {
98 | string-set: booktitle content();
99 | }
100 |
101 | /* get the title of the current chapter - this will be the content of the h1
102 | reset figure counter as figures start from 1 in each chapter */
103 | h2 {
104 | string-set: doctitle content();
105 | page-break-before: always;
106 | counter-reset: figurenum;
107 | counter-reset: footnote;
108 | line-height: 1.3;
109 | }
110 |
111 | h1 {
112 | page-break-before: always;
113 | }
114 |
115 | /* increment chapter counter */
116 | h1.chapter:before {
117 | counter-increment: chapternum;
118 | content: counter(chapternum) ". ";
119 | }
120 |
121 | /* increment and display figure counter */
122 | figcaption:before {
123 | counter-increment: figurenum;
124 | content: counter(chapternum) "-" counter(figurenum) ". ";
125 | }
126 |
127 | /* footnotes */
128 | .fn {
129 | float: footnote;
130 | }
131 |
132 | .fn {
133 | counter-increment: footnote;
134 | }
135 |
136 | .fn::footnote-call {
137 | content: counter(footnote);
138 | font-size: 9pt;
139 | vertical-align: super;
140 | line-height: none;
141 | }
142 |
143 | .fn::footnote-marker {
144 | font-weight: bold;
145 | }
146 |
147 | @page {
148 | @footnotes {
149 | border-top: 0.6pt solid black;
150 | padding-top: 8pt;
151 | }
152 | }
153 |
154 | h1,
155 | h2,
156 | h3,
157 | h4,
158 | h5 {
159 | font-weight: bold;
160 | page-break-after: avoid;
161 | page-break-inside: avoid;
162 | }
163 |
164 | h1 + p,
165 | h2 + p,
166 | h3 + p {
167 | page-break-before: avoid;
168 | }
169 |
170 | table,
171 | figure {
172 | page-break-inside: avoid;
173 | }
174 |
175 | #table-of-contents + ul {
176 | list-style: none;
177 | margin: 0;
178 | padding: 0;
179 | }
180 |
181 | #table-of-contents + ul li {
182 | list-style: none;
183 | }
184 |
185 | /* create page numbers using target-counter in the TOC */
186 | #table-of-contents + ul a::after {
187 | content: leader(".") target-counter(attr(href), page);
188 | }
189 |
190 | #table-of-contents + ul li {
191 | line-height: 2;
192 | }
193 |
194 | #table-of-contents + ul li a {
195 | color: #e535ab;
196 | }
197 |
198 | #table-of-contents + ul li a::after {
199 | /* Does not work because weasyprint does not support page */
200 | content: leader('.') target-counter(attr(href), page);
201 | }
202 |
203 | a {
204 | color: #000;
205 | }
206 |
207 | /* add page number to cross references */
208 | a.xref:after {
209 | content: " (page " target-counter(attr(href, url), page) ")";
210 | }
211 |
212 | .highlight-inline {
213 | background: yellow;
214 | color: gray;
215 | }
216 | .highlight-inline span {
217 | background: inherit;
218 | color: inherit;
219 | }
220 |
221 | :not(pre) > code {
222 | background-color: white;
223 | color: #e535ab;
224 | }
225 | a > code[class*='language-'] {
226 | color: #007acc;
227 | }
228 | /* Prisma color scheme based on graphql.org css */
229 | pre {
230 | padding: 7px 14px;
231 | margin: 1em -4px;
232 | overflow: auto;
233 | position: relative;
234 | box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08),
235 | inset 0 -1px 0 rgba(0, 0, 0, 0.08), inset -1px 0 0 rgba(0, 0, 0, 0.08),
236 | inset 4px 0 0 rgba(0, 0, 0, 0.08);
237 | border-radius: 3px;
238 | }
239 | .hljs {
240 | font-family: 'Roboto Mono', Menlo, Monaco, monospace;
241 | font-weight: 400;
242 | color: #202020;
243 | font-size: 13px;
244 | line-height: 17px;
245 | direction: ltr;
246 | text-align: left;
247 | white-space: pre;
248 | word-spacing: normal;
249 | word-break: normal;
250 | -moz-tab-size: 2;
251 | -o-tab-size: 2;
252 | tab-size: 2;
253 | -webkit-hyphens: none;
254 | -moz-hyphens: none;
255 | -ms-hyphens: none;
256 | hyphens: none;
257 | position: relative;
258 | }
259 | /* Comment */
260 | .hljs-comment,
261 | .cm-comment {
262 | color: #999;
263 | }
264 | /* Punctuation */
265 | .hljs-punctuation,
266 | .cm-punctuation {
267 | color: #555;
268 | }
269 | /* Keyword */
270 | .hljs-keyword,
271 | .cm-keyword {
272 | color: #b11a04;
273 | }
274 | /* OperationName, FragmentName */
275 | .hljs-constant,
276 | .cm-def {
277 | color: #d2054e;
278 | }
279 | /* FieldName */
280 | .hljs-attr-name,
281 | .cm-property {
282 | color: #1f61a0;
283 | }
284 | /* FieldAlias */
285 | .cm-qualifier {
286 | color: #1c92a9;
287 | }
288 | /* ArgumentName and ObjectFieldName */
289 | .hljs-attr,
290 | .cm-attribute {
291 | color: #8b2bb9;
292 | }
293 | /* Number */
294 | .hljs-number,
295 | .cm-number {
296 | color: #2882f9;
297 | }
298 | /* String */
299 | .hljs-string,
300 | .cm-string {
301 | color: #d64292;
302 | }
303 | /* Boolean */
304 | .hljs-boolean,
305 | .cm-builtin {
306 | color: #d47509;
307 | }
308 | /* EnumValue */
309 | .hljs-enum,
310 | .cm-string-2 {
311 | color: #0b7fc7;
312 | }
313 | /* Variable */
314 | .hljs-variable,
315 | .cm-variable {
316 | color: #397d13;
317 | }
318 | /* Directive */
319 | .hljs-function,
320 | .cm-meta {
321 | color: #b33086;
322 | }
323 | /* Type */
324 | .hljs-type-name,
325 | .cm-atom {
326 | color: #ca9800;
327 | }
328 |
--------------------------------------------------------------------------------