Merge pull request #290 from microsoft/alloy/learn-graphql

Documentation for learning how to GraphQL
This commit is contained in:
Eloy Durán 2023-04-22 01:24:39 +02:00 коммит произвёл GitHub
Родитель 04c7ecd82b 21acad1c9a
Коммит 651f972b99
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
48 изменённых файлов: 3798 добавлений и 2113 удалений

3
.vscode/settings.json поставляемый
Просмотреть файл

@ -1,4 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"cSpell.words": ["Daichi", "fukuda", "Kadji"]
}

Просмотреть файл

@ -1,18 +1,8 @@
```diff
@@ 888 d8b 888 888 d8b @@
@@ 888 Y8P 888 888 Y8P @@
@@ 888 888 888 @@
@@ .d88b. 888d888 8888b. 88888b. 88888b. 888 888888 8888b. 888888 888 .d88b. 88888b. @@
@@ d88P"88b 888P" "88b 888 "88b 888 "88b 888 888 "88b 888 888 d88""88b 888 "88b @@
@@ 888 888 888 .d888888 888 888 888 888 888 888 .d888888 888 888 888 888 888 888 @@
@@ Y88b 888 888 888 888 888 d88P 888 888 888 Y88b. 888 888 Y88b. 888 Y88..88P 888 888 @@
@@ "Y88888 888 "Y888888 88888P" 888 888 888 "Y888 "Y888888 "Y888 888 "Y88P" 888 888 @@
@@ 888 888 @@
@@ Y8b d88P 888 @@
@@ "Y88P" 888 @@
```
<p align="center">
<img width="100" src="./website/static/img/graphitation-logo.png">
</p>
# Project
# Graphitation
GraphQL tooling & runtime support needed for MS Teams and beyond

Просмотреть файл

@ -0,0 +1,15 @@
---
sidebar_position: 4
id: faq
title: Frequently Asked Questions
---
# FAQ
## When should I expose a relationship as an object type?
Whenever you have a field called `somethingId` or `something_id`, it is most likely the case that you will want to expose `something` as a relationship with an object type.
If you do need just the raw identifier, e.g. for passing to APIs _outside_ of GraphQL, the user can query for `something.id`.
[🔗 More information](./thinking-in-graphql.md#-design-from-back-end-perspective)

Просмотреть файл

@ -0,0 +1,4 @@
{
"label": "Guides",
"position": 2
}

Просмотреть файл

@ -0,0 +1,10 @@
---
sidebar_position: 5
id: graphql-client
title: The GraphQL Client
description: How a good GraphQL client unlocks the true potential of GraphQL
---
# The GraphQL Client
TODO.

Просмотреть файл

@ -0,0 +1,461 @@
---
sidebar_position: 4
id: graphql-execution
title: GraphQL Execution
description: How data is retrieved and assembled according to the schema and requests
---
# GraphQL Execution
:::info
This section talks about field resolvers, the `Query` root type, and root-field resolvers—_which are ordinary field resolvers, but on the `Query` root type_. It is recommended to read at least sections 1 through 4 of [this upstream guide](https://graphql.org/learn/execution/), prior to reading this here guide; or [this section of the spec](http://spec.graphql.org/October2021/#sec-Execution).
:::
The role of an execution engine in GraphQL is to convert between underlying services into GraphQL schema types for use in the front-end. We call this “resolution”.
It does so by traversing the schema and resolving the fields requested in the query. The executor follows the structure of the query and passes the data returned by each field resolver to its child field resolvers. The executor ensures that only the fields that are requested by the client are resolved, and that the final result matches the shape of the query.
## Get only what you need
A core part of GraphQL is that it allows clients to specify **the exact data** they need from the service. Unlike traditional RESTful APIs, where clients have to make multiple requests or receive more data than they need, GraphQL lets clients define the structure of the data they want and get **_only_ that data** in a single request. Notably, this means that the client does not need to pay the price for any business logic required for fields that are not needed by the client. This makes GraphQL APIs more efficient, flexible, and scalable to clients that have such needs.
## Resolution
To resolve the data of the GraphQL query, we need to define how each field in the schema is fetched from the data source. There are different ways to do this, depending on how we structure our code and how we optimize our performance. In this section, we will explore two variants of how to resolve the data of the query, starting with a naive version that simply returns the full entire response from the root-field, to one that has explicit field resolvers for each field with custom logic.
But first, let's quickly cover how the executor will process your query. Let's consider the conversation list UI once more:
<table>
<tr>
<th>Schema</th>
<th>Query</th>
</tr>
<tr>
<td>
```graphql
type Query {
conversations: [Conversation]
}
type Conversation {
title: String
lastMessage: String
receivedAt: String
participants: [Person]
}
type Person {
avatarURL: String
}
```
</td>
<td>
```graphql
query {
conversations {
title
lastMessage
receivedAt
participants {
avatarUrl
}
}
}
```
</td>
</tr>
</table>
```mermaid
graph TD
A[Query.conversations] --> C["map(Array&lt;Conversation&gt;)"]
C --> D[Conversation.title]
C --> E[Conversation.lastMessage]
C --> F[Conversation.receivedAt]
C --> G[Conversation.participants]
G --> H["map(Array&lt;Person&gt;)"]
H --> I[Person.avatarURL]
```
In this case, when we query for conversations, GraphQL will:
1. Execute the resolver function for the `Query.conversations` field, which returns an array of `Conversation` objects.
1. Then, for each individual `Conversation` object in the array, GraphQL will execute the resolver function for the `Conversation.title`, `Conversation.lastMessage`, `Conversation.receivedAt`, and `Conversation.participants` fields.
1. And finally, for each `Person` object in the `Conversation.participants` array, GraphQL will execute the resolver function for the `Person.avatarURL` field.
### 👎 Greedy resolution
The first resolution variant is the simplest one, where we just return the full response from the root-field. This means that we have a single resolver function for the `conversations` field in the `Query` type, and it returns an array of objects that match the shape of the `Conversation` type. We don't need to define any other resolver functions for the nested fields, because GraphQL will [by default](#a-note-on-the-default-field-resolver) use the property values of the objects as the field values.
For example, if we have a data source that looks like this:
```js
function getMyConversations() {
return [
{
title: "Joshua and Daichi",
lastMessage: "You: Thank you!!",
receivedAt: "10:29 AM",
participants: [
{
avatarURL: "https://example.com/joshua.jpg",
},
{
avatarURL: "https://example.com/daichi.jpg",
},
],
},
{
title: "Kadji Bell",
lastMessage: "You: I like the idea, lets pitch it!",
participants: [
{
avatarURL: "https://example.com/kadji.jpg",
},
],
receivedAt: "10:02 AM",
},
];
}
```
Then our resolver function for the conversations field can simply return this array:
```js
const resolvers = {
Query: {
conversations: () => getMyConversations(),
},
};
```
This approach is easy to implement, and while it works for trivial queries and data sources, it has some drawbacks. For instance, it does not follow the core idea of GraphQL to [get only what you need](#get-only-what-you-need), which leads to inefficient resource usage and performance issues. If we only want to get the `title` and `lastMessage` fields of each conversation, we still get the participants array with _all_ their `avatarURLs`. This may seem innocuous in this contrived example, but imagine more complex data sources that require expensive logic to fulfil the participants data, and it can quickly add up.
:::info
It is important to realize that what a field resolver returns does **not** equal what is returned to the client. Only fields selected in the request document are ever returned. If we had executed the following query, with the above resolver and rich data, the executor would still only ever send the `title` values to the client.
```graphql
query {
conversations {
title
}
}
```
:::
:::note
### A note on the default field resolver
The default field resolver is a function that GraphQL uses to resolve the value of a field when no explicit resolver is provided. It works by looking up the property with the same name as the field on the parent object, or calling it as a function if it is one. For example, if we have a field called `title` on a type called `Conversation`, and no resolver for it, the default field resolver will try to return `conversation.title` or call `conversation.title()` if it exists.
The following set of resolvers has the same result as the above, but _without_ relying on the default field resolver:
```js
const resolvers = {
Query: {
conversations: () => getMyConversations(),
},
Conversation: {
title: (conversation) => conversation.title,
lastMessage: (conversation) => conversation.lastMessage,
participants: (conversation) => conversation.participants,
receivedAt: (conversation) => conversation.receivedAt,
},
Person: {
avatarURL: (person) => person.avatarURL,
},
};
```
:::
### 👍 Lazy resolution
The second resolution variant is more flexible and efficient than the first one, where we can have explicit field resolvers for each field in the schema. These field resolver functions allow us to define how to derive the field's value from the data source.
For the following examples, consider this updated version of the conversation data source:
```js
function getMyConversations() {
const conversations = [
{
title: "Joshua and Daichi",
lastMessage: "You: Thank you!!",
receivedAt: "2023-04-15T17:29:00-08:00",
participantIDs: ["joshua", "daichi"],
},
{
title: "Kadji Bell",
lastMessage: "You: I like the idea, lets pitch it!",
participantIDs: ["kadji"],
receivedAt: "2023-04-15T17:02:00-08:00",
},
];
}
const resolvers = {
Query: {
conversations: () => getMyConversations(),
},
};
```
Now consider that the `receivedAt` value has not already been formatted for display in the data source, so instead we define a resolver function for this field that calculates its human-readable value from the raw format. Here is what that field resolver function could look like:
```js
const resolvers = {
Conversation: {
// Transform the `conversation.receivedAt` value to HH:MM AM/PM
receivedAt: (conversation) => {
const date = new Date(conversation.receivedAt);
return date.toLocaleTimeString("en-US", {
hour: "numeric",
hour12: true,
minute: "numeric",
});
},
},
};
```
Similarly, the `participants` value in the data source is more likely to be a list of person IDs, than it is to be a list of full-fledged person objects. In this scenario, we need to issue an extra call to the data source to get the actual data. It should go without saying that we absolutely want this to be done only when the client needs this data, and not fetch it greedily in the `Query.conversations` root-field. Here is what that field resolver function could look like:
```js
const resolvers = {
Conversation: {
participants: (conversation) => getPeopleByIDs(conversation.participantIDs),
},
};
```
_Neat_ 📸
#### Flexibility for different needs
We can use this approach to optimize our performance by _only_ fetching or returning the data that we need for each field.
Crucially, each field resolver only resolves exactly that which it is named after. For example, the `Query.conversations` field returns a list of conversations from the data source. Similarly, if we only want to get the `title`, `lastMessage`, and `receivedAt` fields of each conversation, we can avoid fetching or returning the participants array with all their `avatarURL`s.
As you have learned in [The Design of GraphQL](the-design-of-graphql.md), this flexibility is at the heart of its design for composition of data requirements.
#### Consistency throughout the schema
Another benefit of using explicit field resolvers is that they can apply to any field that returns a `Conversation` type, not just the top-level query. This means that you can reuse the same logic and transformations for different queries that also involve conversations. For instance, if you have a `Person` type that has a `conversations` field which returns all the conversations that a user participates in, you can use the same field resolvers as you would use for the `Query.conversations` result. This way, you can avoid inconsistency in your API's results, while staying flexible in the queries it can execute.
In this case, only the following schema addition would be necessary to enable the above example:
```graphql
type Person {
conversations: [Conversation]
}
```
Plus a field resolver function that does no work other than getting the conversations based on the appropriate context:
```js
const resolvers = {
Person: {
conversations: (person) => getConversationsForPersonById(person.id),
},
Query: {
person: (_, args) => getPerson(args.id),
},
};
```
With that in place, you now have a schema that allows lazy resolution with a query like the following:
```graphql
query {
person(id: "daichi-fukuda") {
conversations {
title
}
}
}
```
### Striking the right balance
Using a greedy GraphQL field resolver that does all its work in a single field resolver can _seem_ like a simple and straightforward way to implement a schema, but **it has significant drawbacks** in terms of resource usage and performance. It results in over-fetching data that is not needed by the client, and wasting time and memory on processing it.
In conclusion, lazy field resolvers are **the recommended way** to implement any field that requires some custom logic. This can include scalar fields that need some derivation or transformation, as well as object fields that need to fetch associated data from other sources. Only for fields that are already present in the parent types data source, and need no further processing, you can rely on the default field resolver—this usually applies to scalar fields only.
## Models
In GraphQL execution, there is no need for the GraphQL schema to match the data source. This means that we can design our schema based on the needs of our clients, rather than the structure of our database or API. In fact, very often we will want to hide values that the clients don't need at all or those values from which we derive the field's result.
For example, we might have a field in the schema called `fullName`, which concatenates the `firstName` and `lastName` values from our model. We don't need to expose those fields in our schema if they are not useful to our clients, but the field resolver _does_ need access to the model data for it to be able to do its work.
```graphql
type Person {
fullName: String!
# NOTE: These fields do NOT exist in the schema.
# firstName: String!
# lastName: String!
}
```
```ts
const resolvers = {
Person: {
fullName: (personDatabaseRow) =>
`${personDatabaseRow.firstName} ${personDatabaseRow.lastName}`,
},
};
```
Here, the `personDatabaseRow` argument has all the underlying data we need. We call such a source, **the model**. _Crucially_, the model type is **not** equal to the schema type. The model type is where the data comes _from_, the schema type is what the resolver transforms the data _to_.
A model can be a raw response from the data source, an intermediate representation, or a full fledged model class instance. A raw data source response is the most basic form of a model. It could be a row from a database table, a document from a database, or a JSON object from an API response.
An intermediate representation is a model that has some processing or transformation applied to it, perhaps ahead of time. For example, we might have a model that adds some computed properties during a background synchronization task. Note that this should **not** be transformation to full schema types.
A full fledged model class instance is a model that has methods and behaviors associated with it. For example, we might have a model class that implements validation rules, business logic, or custom methods for manipulating the data.
Depending on our use case and preferences, we can choose any of these forms of models for our GraphQL execution. The only requirement is that our resolver functions can access the relevant properties of our models to return the correct values for our schema fields.
:::tip
A good way to think about a model, is that whatever your data source returns **is your model**. In turn, these models are what the resolvers operate on to _lazily_ map underlying data to the public schema types.
:::
:::caution
#### A warning for statically typed language users
For type-safe field resolver implementations, you will typically want to generate typings to use in your resolvers. By default, codegen tools will typically emit typings that _exactly_ match the schema types. What this means is that your field resolver function will be required to return the data for child fields already transformed according to the schema. I.e. this forces you to apply [greedy resolution](#-greedy-resolution). No bueno.
You will therefore absolutely want to pick a codegen tool that allows you to specify custom model typings for specific schema types. In the TypeScript space, such tools include:
- Our own [graphitation supermassive codegen](https://github.com/microsoft/graphitation/tree/main/packages/cli), which allows you to annotate your schema definition with the model typings to use.
- The popular [graphql-codegen](https://the-guild.dev/graphql/codegen) tool, which [allows you to provide configuration](https://the-guild.dev/blog/better-type-safety-for-resolvers-with-graphql-codegen) with schema type to model type mappings.
:::
## Performant data loading
Integral to resolution of a graph of connected data, is that a query will end up containing many entities of the same kind, or perhaps even contain the same entity multiple times. For instance, for each conversation fetch all participants—a classic N+1 problem.
```mermaid
graph LR
Q["Query.conversations"] --> C1["Conversation.participants"]
Q --> C2["Conversation.participants"]
subgraph Person Data Source
P1["Data Request 1"]
P2["Data Request 2"]
P3["Data Request 3"]
end
C1 -->|"getPerson('joshua')"| P1
C1 -->|"getPerson('daichi')"| P2
C2 -->|"getPerson('kadji')"| P3
P1 --> U1["Person"]
P2 --> U2["Person"]
P3 --> U3["Person"]
classDef external fill:#f9f,stroke:#333;
class P1,P2,P3 external;
```
These entities might be necessary for unrelated parts of the application, but still, for performance reasons we want to be able to batch that entity data loading. [DataLoader](https://github.com/graphql/dataloader) is a utility used to abstract request batching in GraphQL. It allows you to reason about a batch of requests, **without** needing to do so in the field resolver functions—keeping them decoupled and without sacrificing the performance of batched data loading.
#### Basic data loading
Lets look at how DataLoader could be used for the participants in our chat-list example. First we define the DataLoader instance, like so:
```js
const personLoader = new DataLoader(async (ids) => {
return getPeopleFromServiceByIDs(ids);
});
```
In essence, DataLoader takes a function that, given a set of IDs (or keys), will return a promise for a set of values.
Then, for an individual conversation in the chat-list, we could use the DataLoader instance, like so:
```js
const resolvers = {
Conversation: {
participants: async (conversation) => {
return personLoader.loadMany(conversation.participantIds);
},
},
};
```
```mermaid
graph LR
Q["Query.conversations"] --> C1["Conversation.participants"]
Q --> C2["Conversation.participants"]
subgraph Person Data Source
P1["Data Request 1"]
P2["Data Request 2"]
end
C1 -->|"personLoader.loadMany(['joshua', 'daichi'])"| P1
C2 -->|"personLoader.loadMany(['kadji'])"| P2
P1 --> U1["Person"]
P1 --> U2["Person"]
P2 --> U3["Person"]
classDef external fill:#f9f,stroke:#333;
class P1,P2 external;
```
This example isn't all that ground-breaking, as we already have the list of participant IDs for each conversation in the `Conversation.participants` field resolver and can easily load them as a batch. (The only true benefit would be the caching of the people data, allowing for fast retrieval when resolving the same people again elsewhere in the query.)
#### Decoupled batching
It gets more interesting when we consider that the execution engine will resolve the participants for each conversation in the list **in parallel**. You could imagine it to work something like this pseudo code:
```js
Promise.all([
Conversation.participants({ ... }),
Conversation.participants({ ... })
])
```
Now, when we pass a single ID (_or set_) to the DataLoader, we expect a single value (_or respective set_) to be returned; yet still batch them with the participants of _all_ other conversations.
```mermaid
graph LR
Q["Query.conversations"] --> C1["Conversation.participants"]
Q --> C2["Conversation.participants"]
subgraph Person Data Source
P["Data Request 1"]
end
C1 -->|"personLoader.loadMany(['joshua', 'daichi'])"| P
C2 -->|"personLoader.loadMany(['kadji'])"| P
P --> U1["Person"]
P --> U2["Person"]
P --> U3["Person"]
classDef external fill:#f9f,stroke:#333;
class P external;
```
How this works is that all requests made of a DataLoader during a single tick of the JavaScript run-loop, will get batched together and passed to the batch function as a single list.
So, given our [prior example data](#👎-greedy-resolution):
1. The execution engine would invoke the `Conversation.participants` field resolver twice.
- Once in a conversation with Joshua and Daichi: `personLoader.loadMany(["joshua", "daichi"])`
- And once in a conversation with Kadji: `personLoader.loadMany(["kadji"])`
1. The DataLoader instance would then receive the following enqueued IDs as a _single_ list: `["joshua", "daichi", "kadji"]`
1. And return the requested people to the 2 invocations of the `Conversation.participants` field resolver for further transforming.
1. Finally, the execution engine moves on to the next level of the query, by invoking the `Person.avatarURL` field resolver for each of the 3 people.
#### Caching
Additionally, DataLoader provides caching of entities during a single execution pass of an operation. This means that any participants present in all conversations, such as the authenticated user, will only get requested once. But also, if one of those people is requested again later on in the query, DataLoader will simply return it immediately.
:::info
A walkthrough of the DataLoader v1 source code by one of its authors, Lee Byron. While the source has changed since this video was made, it is still a good overview of the rationale of DataLoader and how it works.
<a href="https://youtu.be/OQTnXNCDywA" target="_blank" alt="DataLoader Source Code Walkthrough"><img src="https://img.youtube.com/vi/OQTnXNCDywA/0.jpg" /></a>
:::

Двоичные данные
website/docs/learn-graphql/guides/images/SmallChatList.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 54 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 43 KiB

После

Ширина:  |  Высота:  |  Размер: 43 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 45 KiB

После

Ширина:  |  Высота:  |  Размер: 45 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 48 KiB

После

Ширина:  |  Высота:  |  Размер: 48 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 47 KiB

После

Ширина:  |  Высота:  |  Размер: 47 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 46 KiB

После

Ширина:  |  Высота:  |  Размер: 46 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 46 KiB

После

Ширина:  |  Высота:  |  Размер: 46 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 33 KiB

После

Ширина:  |  Высота:  |  Размер: 33 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 45 KiB

После

Ширина:  |  Высота:  |  Размер: 45 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 47 KiB

После

Ширина:  |  Высота:  |  Размер: 47 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 50 KiB

После

Ширина:  |  Высота:  |  Размер: 50 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 36 KiB

После

Ширина:  |  Высота:  |  Размер: 36 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 33 KiB

После

Ширина:  |  Высота:  |  Размер: 33 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 78 KiB

После

Ширина:  |  Высота:  |  Размер: 78 KiB

Просмотреть файл

@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 3
id: the-design-of-graphql
title: The design of GraphQL
description: What was GraphQL designed to solve for and how to leverage that?
@ -7,25 +7,17 @@ description: What was GraphQL designed to solve for and how to leverage that?
# The design of GraphQL
## Flux
Unfortunately, the community has largely lost sight of the original design considerations that Facebook had for GraphQL. Key components of its design are misunderstood and often entirely ignored by popular GraphQL clients. Facebooks own GraphQL client, [Relay](https://relay.dev), incorporates all the GraphQL best-practices learned from using GraphQL _as it was designed_, but alas the choice was made to separate the strong opinions of how to use GraphQL from GraphQLs own documentation to avoid being prescriptive.
To understand how Facebook designed GraphQL and React to work together, we must first go back to the origins of React and learn about [Flux](https://facebook.github.io/flux/docs/in-depth-overview/). Flux is the application architecture that Facebook uses for building client-side web applications. It complements React's composable view components by utilizing a unidirectional data flow, making interactions easier to reason about.
:::danger
Any GraphQL client for data-driven UI applications that does **not** have a strong opinion on making “fragments” the unit around which the user-interface components are built, is **not** leveraging key GraphQL design components nor setting you up for success with complex data-driven UI applications.
:::
![](./slidedeck/Slide2.png)
With that in mind, forget what you might already know about GraphQL for a bit and lets go back to when Facebook designed GraphQL—when they had realized that user-interfaces and the back-end services backing them would end up getting coupled together, making iterating on complex applications like theirs extremely hard.
But theres a missing piece here. How do we get the data from the “remote” service into the client in the first place?
## Example
![](./slidedeck/Slide3.png)
And especially in complex applications, they realised that the service and the view end-up getting coupled together, which makes development difficult.
![](./slidedeck/Slide4.png)
## The problem
### Example
Lets take a simple example, here we have the Chat-List component of Teams. Theres a list of conversations, content preview, and some details about the participants. So if we would structure this, there would be 3 major components.
Lets take a look at the `ChatList` component of Teams. Theres a list of conversations, content preview, and some details about the participants. So if we would structure this, there would be 3 major components.
- Theres going to be the outer `ChatList` component.
- The `ChatList` component would contain many `ChatListItem` components, one for each conversation that the user has.
@ -41,7 +33,7 @@ The service sends some data down to the client, `ChatList` passes it on to its c
![](./slidedeck/Slide11.png)
### Leaky Abstractions
## Problem
But of course this is a simplification, what happens when we add some color to this? The `ChatList` component needs an item count, the `ChatListItem` component needs an avatar, and the `ConversationInfo` needs a title and last message preview.
@ -50,7 +42,7 @@ Furthermore, if we look at `ConversationInfo`, we have actually leaked its detai
![](./slidedeck/Slide13.png)
So what happens when we change `ConversationInfo`? Well, were not just changing `ConversationInfo`, were also changing `ChatListItem` and what it passes down. We might have to change `ChatList`, dependending on how it structured things. And we _certainly_ have to change the service, so that it sends the new information.
So what happens when we change `ConversationInfo`? Well, were not just changing `ConversationInfo`, were also changing `ChatListItem` and what it passes down. We might have to change `ChatList`, depending on how it structured things. And we _certainly_ have to change the service, so that it sends the new information.
![](./slidedeck/Slide14.png)
@ -58,7 +50,7 @@ How did we get here? How did we get to a place where making a simple change to `
The big problem was a lack of modularity. We wanted `ConversationInfo` to be a self-contained component, but it wasnt. Its implementation details were leaked to `ChatListItem`, _and_ up to the service. The thing that was missing was a way for `ConversationInfo` and other components to specify what data they require. That specification didnt live in the component itself, it was spread all over the application.
### The Solution
## The Solution
What we want is some way for each component to statically define its data needs in a simple way.
@ -80,8 +72,6 @@ From here on out, its exactly the same diagram as before. We have a service,
![](./slidedeck/Slide25.png)
### Conclusion
Its a subtle change, but a _key_ one.
Weve taken the details about what data `ConversationInfo` requires _out_ of the service, where it doesnt belong, and have put it _in_ the `ConversationInfo` component where it does.
@ -90,9 +80,15 @@ Because inherently, our rendering logic for `ConversationInfo` and its data-spec
So if we want to do this, if we want each component to be able to specify its own data needs, how can we do so? The realization is that our data-specification has a key property that it needs to fulfill, which is composition.
## Composition
### Composition
Composition in GraphQL is achieved by leveraging fragments, which are snippets of a query that can be composed together to form larger queries. These fragments are colocated with their components and composed into a tree that very much follows the shape of the component tree.
Composition in GraphQL is achieved by leveraging fragments, which are snippets of a query that can be composed together to form larger queries. These fragments are co-located with their components and composed into a tree that very much follows the shape of the component tree.
:::tip
A good way to think about this, is that in a components fragment you select exactly and only the data that _this_ component needs to render. These are either properties the component renders directly, needs to pass to components not backed by GraphQL data (such as very basic controls), or a fragment spread for components it renders that are backed by GraphQL data.
:::
In the following React code samples, each component defines the exact properties it needs in a GraphQL fragment, and then for any child components it spreads [read: refers to] the fragment belonging to that component. It knows that it has children with data dependencies, but it doesn't _need_ to care about the details of that data.
```tsx
function ChatList() {
@ -171,7 +167,7 @@ Because a component and its data requirements are self-contained:
### Global Optimization
At the framework level, transperantly to the UI engineer, we can:
At the framework level, transparently to the UI engineer, we can:
- Use tooling to extract and optimize query
- Fetch data in single request for a single render pass
@ -180,10 +176,9 @@ At the framework level, transperantly to the UI engineer, we can:
- Couple lazy asset loading to data resolving, including the required components themselves
- Move extracted queries upstream so the pipeline can ahead-of-time optimize/prepare data in a generic manner across builds
## Closing Statement
## Takeaways
Unfortunately, due in large part to lack of proper documentation and guidance by Facebook, the community has largely lost sight of these original design considerations. Fragments are seen as a way to DRY up code, rather than uniquely expressing the data requirements of a single component, and often entirely ignored by popular GraphQL clients.
Any GraphQL client for data-driven UI applications that does not make fragments the unit around which everything is built, is not setting you up for success—assuming you have needs similar to Facebook.
Facebook's own GraphQL client, [Relay](https://relay.dev), is the golden reference for how to do this right.
- GraphQL was created to allow composition of data-requirements for UI components in complex data-driven applications.
- Smaller network payloads is great, but not the primary design goal.
- Fragments are the manner in which a component's unique data-requirements can be composed.
- They are not meant simply for DRY purposes, nor should they be shared by different components.

Просмотреть файл

@ -0,0 +1,229 @@
---
sidebar_position: 2
id: thinking-in-graphql
title: Thinking in GraphQL
description: How to think of the GraphQL abstraction layer and its purpose.
---
# Thinking in GraphQL
:::info
This section shows GraphQL query and schema definition syntax. The first part of [this upstream guide](https://graphql.org/learn/schema/) will be useful to explain any bits that are not immediately clear.
:::
As you will learn in [the design of GraphQL](./the-design-of-graphql.md) section, GraphQL was designed to allow components to express their own data requirements, and for those requirements to be composable into one or more larger UIs—whilst not introducing any unnecessary coupling between the various components that make up the larger UI. Before making our way to that section, though, lets build a base-line understanding of how to think in GraphQL.
## Abstractions for complex data-driven UI
The GraphQL schema encodes the data requirements that the host is expected to be able to provide to the UI. At _minimum_, this schema will be an intersection of all of the data requirements expressed by the UIs—but it may be a superset because of past or future UI design iterations.
It is very natural to initially view GraphQL from a "back-end" perspective, but this way of thinking will lead your schema design down the wrong path. Instead, you should view the schema as sitting in between your back-end and the front-end. Once you understand that this schema is _in service of_ the UI, it then logically follows that the schema exposes the domain data in ways that will very much resemble the way in which the data is presented in the UI.
For instance, a conversation list UI might care about presenting a list of conversations the user is in, with their last messages, associated participants, their avatars, and so on and so forth. It does not care about:
- the conversation metadata coming from a different back-end service than the participant metadata
- that in some cases the back-end might have de-normalized [parts of] message metadata onto the conversation
- that multiple back-end services might return different subsets for what is, semantically speaking, the same conversation object
- or even the very act of fetching the data from the various back-end services
These are the types of implementation details that you want to abstract away from complex data-driven UI code, as that makes it easier to reason about and thus to maintain/optimize over time. Additionally, when thinking of complex applications, you will want to encapsulate business logic in a way that allows you to re-use it for other UIs, or perhaps even compute/transform the data in a background thread.
All of these needs are met by a GraphQL schema that acts as the abstraction layer between your back-end and front-end.
## The “graph” in GraphQL
Another important UI consideration is rendering performance, or sometimes perceived performance. The former is achieved by having all data available that is necessary, for the initial state of the UI that the user should see, such that only a single render pass is required. (Sometimes this might mean that it can take a little while longer before rendering can start, but even then a single render pass can still provide an improvement to perceived performance.)
Ideally all this data can be provided within a reasonable time-frame, but even then there are provisions in state-of-the-art GraphQL stacks that allow you to design a controlled loading experience using [the “render-as-you-fetch” pattern](https://17.reactjs.org/docs/concurrent-mode-suspense.html#traditional-approaches-vs-suspense), as outlined in [this in-depth presentation](https://www.youtube.com/watch?v=Tl0S7QkxFE4) by a Facebook/Relay engineer.
All in all, what this means is that the schema _should_ enable a piece of UI to fetch all data it needs, in a single request. This is where “the graph” comes in, which means that the types that make up the schema are connected to each other in semantically meaningful ways and can be retrieved as a meaningful whole.
:::note
### Broad-Query
This concept might seem foreign even to those already familiar with GraphQL. To solve this at Microsoft, we had to go as far as invent a new name for this very core concept: **Broad-Query**.
However, because in GraphQL _all_ queries are meant to be “broad”, we will **not** keep repeating the “Broad-Query” term. After all, we want you to walk away from this guide as someone who truly understands GraphQL!
:::
### 👍 Schema design from front-end perspective
When designing the schema in a vacuum, it might be hard to imagine what those connections should be. However, when considered from the perspective of a concrete piece of UI, and working your way backwards, it actually becomes a lot easier.
Let's consider the conversation list UI example again:
<table>
<tr>
<th>Conversation list</th>
<th>UI components</th>
<th>GraphQL query</th>
</tr>
<tr>
<td>
<img src={require("./images/SmallChatList.png").default} border="1" />
</td>
<td>
```jsx
<ChatList>
<ChatListItem>
<Avatar />
<Title />
<LastMessage />
<Timestamp />
</ChatListItem>
</ChatList>
```
</td>
<td>
```graphql
query {
conversations {
participants {
avatarUrl
}
title
lastMessage
receivedAt
}
}
```
</td>
</tr>
</table>
The UI components were probably very natural to imagine, right? Well, as you can see, the GraphQL query you would want to be able to write is an equally natural extrapolation.
Finally, completing our top-down approach from UI design to GraphQL schema, the schema to power this would need to look like this:
```graphql
type Query {
conversations: [Conversation]
}
type Conversation {
participants: [Person]
title: String
lastMessage: String
receivedAt: String
}
type Person {
avatarURL: String
}
```
### 👎 Schema design from back-end perspective
To contrast, lets look at a back-end perspective schema, and how it makes it impossible to fetch all data in a single request.
```graphql
type Query {
conversations: [Conversation]
person(id: ID): Person
}
type Conversation {
participantIDs: [ID]
}
```
In this case, every root-field maps to a back-end service, and it of course does not return the full data for each related entity in its response payload, but rather contains foreign-keys to those related entities.
Because we can only get the IDs of participants in a conversation, rather than the actual `Person` objects they refer to, we are being forced to make an additional request for _each_ participant in all of the conversations in the list. This is the N+1 problem and forces the UI to perform a waterfall of requests. This in turn will lead to a slow loading experience or staggered UI rendering.
## Generic _and_ domain-specific
The benefit of GraphQL is that it allows you to design your data schema in a way that reflects the domain of your application, rather than the structure of your database or the layout of your UI. This means that you can define types and fields that represent the entities and relationships in your domain, and expose them through a single endpoint that can be queried in a concise manner.
However, this does not mean that you should create a schema that is tailored to a specific UI component or view. Doing so would limit the reusability and composability of your schema, and make it harder to evolve over time. Instead, you should aim to create a schema that is generic enough to support any UI requirement, but still specific enough to capture the domain logic and constraints.
### Query design
For example, a “person” whose name is rendered in one place of the UI, is the same “person” whose email address is rendered elsewhere in the UI. Modeling this with a single GraphQL type (e.g. `Person`), regardless of what data source the data originates, allows you to have a single source of truth to consider.
For example, this React component and GraphQL fragment:
```jsx
function PersonAvatar(props) {
const person = useFragment(
graphql`
fragment PersonAvatarFragment on Person {
avatarUrl
}
`,
props.person,
);
return <img src={person.avatarUrl} />;
}
```
Can be used in _any_ component hierarchy that embeds a "person". Such as a chat-list item:
```jsx
function ChatListItem(props) {
const conversation = useFragment(
graphql`
fragment ChatListItemFragment on Conversation {
title
participants {
...PersonAvatarFragment
}
}
`,
props.conversation,
);
return (
<li>
<h3>{conversation.title}</h3>
{conversation.participants.map(person => <PersonAvatar person={person}>)}
</li>
);
}
```
Or in a signed-in user control:
```jsx
function MeControl(props) {
const { me } = useFragment(
graphql`
fragment MeControlFragment on Query {
me {
displayName
...PersonAvatarFragment
}
}
`,
props.query,
);
return (
<div>
<h3>Signed-in as {me.displayName}</h3>
<PersonAvatar person={me}>
</div>
);
}
```
But also crucial to a performant architecture when dealing with a complex data-driven application—_and as you will learn more about in [the GraphQL client guide](./graphql-client.md)_—because now the person avatar is backed by a single source of truth, updating the `Person` record in the GraphQL client's data-store with a new `avatarUrl` allows the frameworks to know exactly all of the places in the UI where that data is needed and thus which components to re-render.
I.e. the following pseudo-code is all it takes to automatically re-render the `PersonAvatar` in the chat-list for each conversation the signed-in user participates in, as well as in the signed-in user control:
```js
client.update({
type: "Person",
id: "42",
avatarUrl: "http://example.com/new-me-who-dis",
});
```
## Conclusion
Once you start applying these patterns, you will notice how easy it becomes to re-use UI components and their fragments in various different component hierarchies, and also how easy it becomes to _update_ that data in a single place and have the change reflected everywhere in the UI.
This is where it all comes together and GraphQL truly shines ☀️

Просмотреть файл

@ -0,0 +1,4 @@
{
"label": "How-To",
"position": 3
}

Просмотреть файл

@ -0,0 +1,12 @@
---
id: howto-models
title: How-To guide for models
description: How to properly create models
---
# How-To: Models
TODO:
- Show a more elaborate example where the field resolver needs data that we clearly do not want to expose in the schema type.
- What logic to put in the model vs resolver, do not convert to schema types in the model

Просмотреть файл

@ -4,8 +4,28 @@ sidebar_position: 1
# Intro
## Preface
> GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API.
>
> — [graphql.org](https://graphql.org)
While technically accurate, its brevity leaves a lot of room for misguided understanding, typically based on prior experiences.
The authors of this guide have observed that **the original premise of GraphQL is lost on most users**. On the one hand this speaks to the versatility of GraphQL, but on the other hand it means that missing key nuances can cause incorrect application of GraphQL, even in the exact same context it was designed for: **complex data-driven UI applications**.
This guide aims to teach you everything you need to understand about GraphQL from that perspective, including how to design schemas, how to implement field resolvers, and how to effectively use this to build these data-driven UIs.
This guide does not aim to replace [the canonical graphql.org sites documentation](https://graphql.org/learn/). Some familiarity with GraphQL might be necessary for some sections, where possible the guide will link to the relevant existing documentation.
:::caution
Each section of this guide builds upon the knowledge gained in the previous section. Not taking this into account means you may end up repeating the mistakes made in the community to learn about GraphQL outside of the context that it was designed for.
If possible, read the guide from start to finish.
:::
## About
GraphQL is a new way of thinking about data and how to access it. It is a data query language that was invented by Facebook in 2012 to make it easier to deal with the complexity of data and state driven UI applications, combined with the scale of their workforce and codebase. GraphQL has since been open-sourced and is now used by many companies that have to deal with the same complexities.
GraphQL isn't tied to any specific database or storage engine, instead it is an abstraction over the underlying APIs, expressed using a GraphQL schema. This higher level interface is more convenient for UI developers, as it allows them to access the data in a more consistent and predictable way. Additionally, a good GraphQL schema is focussed on expressing the actual domain models and the semantics thereof, rather than the underlying API data and the many disparate forms it can take. By carefully designing your schema, you can optimize your data retrieval to get exactly the data your UI needs, and _only_ that data.
This guide will teach you everything you need to know about GraphQL, including how to design schemas, how to implement field resolvers, and how to effectively use this to build data-driven user-interface applications.

Просмотреть файл

@ -0,0 +1,4 @@
{
"label": "Packages",
"position": 2
}

Просмотреть файл

@ -1,4 +1,4 @@
{
"label": "Apollo React/Relay Duct-Tape",
"position": 2
"position": 3
}

Просмотреть файл

@ -17,6 +17,11 @@ const config = {
projectName: "graphitation",
trailingSlash: false,
markdown: {
mermaid: true,
},
themes: ["@docusaurus/theme-mermaid"],
presets: [
[
"classic",
@ -45,8 +50,8 @@ const config = {
navbar: {
title: "graphitation",
logo: {
alt: "My Site Logo",
src: "img/logo.svg",
alt: "Graphitation Logo",
src: "img/graphitation-logo.png",
},
items: [
{
@ -57,9 +62,9 @@ const config = {
},
{
type: "doc",
docId: "apollo-react-relay-duct-tape/intro",
docId: "packages/apollo-react-relay-duct-tape/intro",
position: "left",
label: "Apollo React/Relay Duct-Tape",
label: "Packages",
},
{
href: "https://github.com/microsoft/graphitation",
@ -71,15 +76,15 @@ const config = {
footer: {
style: "dark",
links: [
{
title: "Docs",
items: [
{
label: "Apollo React/Relay Duct-Tape",
to: "/docs/apollo-react-relay-duct-tape/intro",
},
],
},
// {
// title: "Docs",
// items: [
// {
// label: "Apollo React/Relay Duct-Tape",
// to: "/docs/apollo-react-relay-duct-tape/intro",
// },
// ],
// },
{
title: "Community",
items: [

Просмотреть файл

@ -15,8 +15,8 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.0.0-beta.14",
"@docusaurus/preset-classic": "2.0.0-beta.14",
"@docusaurus/core": "^2.4.0",
"@docusaurus/preset-classic": "^2.4.0",
"@mdx-js/react": "^1.6.21",
"clsx": "^1.1.1",
"prism-react-renderer": "^1.2.1",
@ -24,9 +24,10 @@
"react-dom": "^17.0.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.14",
"@tsconfig/docusaurus": "^1.0.4",
"typescript": "^4.5.2"
"@docusaurus/module-type-aliases": "^2.4.0",
"@docusaurus/theme-mermaid": "^2.4.0",
"@tsconfig/docusaurus": "^1.0.7",
"typescript": "^4.7.4"
},
"resolutions": {
"cross-fetch": "^3.1.5",

Просмотреть файл

@ -5,6 +5,10 @@
width: 100%;
}
.features a {
color: #000;
}
.featureSvg {
height: 200px;
width: 200px;

Просмотреть файл

@ -1,41 +1,37 @@
import React from "react";
import clsx from "clsx";
import styles from "./HomepageFeatures.module.css";
import Link from "@docusaurus/Link";
type FeatureItem = {
title: string;
title: string | JSX.Element;
image: string;
description: JSX.Element;
};
const FeatureList: FeatureItem[] = [
{
title: "Easy to Use",
image: "/img/undraw_docusaurus_mountain.svg",
title: <Link to="docs/learn-graphql/intro">Learn GraphQL</Link>,
image: "img/graphql-logo-rhodamine.png",
description: (
<>
Docusaurus was designed from the ground up to be easily installed and
used to get your website up and running quickly.
Comprehensive documentation for learning GraphQL, following all
best-practices.
</>
),
},
{
title: "Focus on What Matters",
image: "/img/undraw_docusaurus_tree.svg",
description: (
<>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go
ahead and move your docs into the <code>docs</code> directory.
</>
title: (
<Link to="docs/packages/apollo-react-relay-duct-tape/intro">
Packages
</Link>
),
},
{
title: "Powered by React",
image: "/img/undraw_docusaurus_react.svg",
image: "img/graphitation-logo.png",
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can
be extended while reusing the same header and footer.
Graphitation is a monorepo that contains various NPM packages that we
use in MS Teams and other M365 projects. All of these are available as
Open-Source Software.
</>
),
},
@ -43,7 +39,7 @@ const FeatureList: FeatureItem[] = [
function Feature({ title, image, description }: FeatureItem) {
return (
<div className={clsx("col col--4")}>
<div className={clsx("col col--6")}>
<div className="text--center">
<img className={styles.featureSvg} alt={title} src={image} />
</div>

Просмотреть файл

@ -4,15 +4,24 @@
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
/**
* These are our logo's primary colors according to this tool: https://www.imgonline.com.ua/eng/get-dominant-colors-result.php
*
* - #7103e0 <- this is the one we use right now
* - #5d039d
* - #a569ca
* - #f9530c
*
* I then used this tool to create a set of shades: https://uicolors.app/create
*/
:root {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: rgb(33, 175, 144);
--ifm-color-primary-darker: rgb(31, 165, 136);
--ifm-color-primary-darkest: rgb(26, 136, 112);
--ifm-color-primary-light: rgb(70, 203, 174);
--ifm-color-primary-lighter: rgb(102, 212, 189);
--ifm-color-primary-lightest: rgb(146, 224, 208);
--ifm-color-primary: #8b16ff;
--ifm-color-primary-dark: #8004fd;
--ifm-color-primary-darker: #7103e0;
--ifm-color-primary-darkest: #5905ad;
--ifm-color-primary-light: #943dff;
--ifm-color-primary-lighter: #ab75ff;
--ifm-color-primary-lightest: #c5a6ff;
--ifm-code-font-size: 95%;
}

Просмотреть файл

@ -16,9 +16,9 @@ function HomepageHeader() {
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro"
to="https://github.com/microsoft/graphitation"
>
Docusaurus Tutorial - 5min
View on GitHub
</Link>
</div>
</div>

Двоичные данные
website/static/img/docusaurus.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 5.0 KiB

Двоичные данные
website/static/img/favicon.ico

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.5 KiB

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичные данные
website/static/img/graphitation-logo-only-capital.pxd Normal file

Двоичный файл не отображается.

Двоичные данные
website/static/img/graphitation-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичные данные
website/static/img/graphitation-logo.pxd Normal file

Двоичный файл не отображается.

Двоичные данные
website/static/img/graphql-logo-black.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 28 KiB

Двоичные данные
website/static/img/graphql-logo-rhodamine.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 34 KiB

Разница между файлами не показана из-за своего большого размера Загрузить разницу