Skip to content

Commit

Permalink
Persisted queries (#2354)
Browse files Browse the repository at this point in the history
Summary:
Enables a simple but straightforward mechanism to use persisted queries in open-source. Passing the new `--persist-output <path>` option now triggers the compiler to create an `id` for each operation (instead of leaving `id` null) and to save a mapping of ids to corresponding query text in the path specified by the developer. Developers can then save the id=>text mapping to a database or similar so that their GraphQL server can resolve query ids to text.

Note that the original PR saved a query mapping alongside each generated artifact and then aggregated those into a final query map, I've simplified this to only create a single aggregate query map at the end without any other intermediate artifacts. Also, instead of the `--persist` and optional `--persist-output` options, I've reduced this to just a single `--persist-output` argument so that the path to which the JSON mapping is saved is always user provided - this should help avoid confusion about where it ends up getting created and means that Relay compiler doesn't have to treat it as a generated file that may need to be cleaned up.

-- Original PR --

Hi guys, here's my attempt at implementing persisted queries for relay modern. Please review and let me know if it's ok.

Thanks!
Pull Request resolved: #2354

Reviewed By: alunyov

Differential Revision: D13569254

Pulled By: josephsavona

fbshipit-source-id: cc10ce8b947016d1e3911a91230c44361ae98d9e
  • Loading branch information
yusinto authored and facebook-github-bot committed Jan 3, 2019
1 parent c761128 commit 708f842
Show file tree
Hide file tree
Showing 8 changed files with 643 additions and 359 deletions.
42 changes: 33 additions & 9 deletions docs/Modern-GraphQLInRelay.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ graphql`
`;
```

The result of using the `graphql` template tag are `GraphQLTaggedNode`s, which are used to define [Query Renderers](./query-renderer), [Fragment Containers](./fragment-container), [Refetch Containers](./refetch-container), [Pagination Containers](./pagination-container), etc.
The result of using the `graphql` template tag is a `GraphQLTaggedNode`; a runtime representation of the GraphQL document which can be used to define [Query Renderers](./query-renderer), [Fragment Containers](./fragment-container), [Refetch Containers](./refetch-container), [Pagination Containers](./pagination-container), etc.

However, `graphql` template tags are **never executed at runtime**. Instead, they are compiled ahead of time by the [Relay Compiler](#relay-compiler) into generated artifacts that live alongside your source code, and which Relay requires to operate at runtime. The [Relay Babel plugin](./installation-and-setup.html#setup-babel-plugin-relay) will then convert the `graphql` literals in your code into `require()` calls for the generated files.
Note that `graphql` template tags are **never executed at runtime**. Instead, they are compiled ahead of time by the [Relay Compiler](#relay-compiler) into generated artifacts that live alongside your source code, and which Relay requires to operate at runtime. The [Relay Babel plugin](./installation-and-setup.html#setup-babel-plugin-relay) will then convert the `graphql` literals in your code into `require()` calls for the generated files.

## Directives

Expand All @@ -44,7 +44,7 @@ query TodoListQuery($userID: ID) {
}
```

See [Fragment Container docs](./fragment-container.html#passing-arguments-to-a-fragment) for more details.
See the [Fragment Container docs](./fragment-container.html#passing-arguments-to-a-fragment) for more details.

### `@argumentDefinitions`

Expand All @@ -62,29 +62,38 @@ fragment TodoList_list on TodoList @argumentDefinitions(
}
```

See [Fragment Container docs](./fragment-container.html#passing-arguments-to-a-fragment) for more details.
See the [Fragment Container docs](./fragment-container.html#passing-arguments-to-a-fragment) for more details.

### `@connection(key: String!, filters: [String])`

When using the [Pagination Container](./pagination-container.html), Relay expects connection fields to be annotated with a `@connection` directive. For more detailed information and example, check out our docs on using `@connection` inside a Pagination Container [`here`](./pagination-container.html#connection).
When using the [Pagination Container](./pagination-container.html), Relay expects connection fields to be annotated with a `@connection` directive. For more detailed information and an example, check out the [docs on using `@connection` inside a Pagination Container](./pagination-container.html#connection).

**Note:** `@connection` is also supported in [compatibility mode](./relay-compat.html)

### `@relay(plural: Boolean)`

When defining a fragment, you can use the `@relay(plural: true)` directive to indicate that the fragment is backed by a [GraphQL list](http://graphql.org/learn/schema/#lists-and-non-null), meaning that it will inform Relay that this particular field is an array. For example:
When defining a fragment for use with a Fragment container, you can use the `@relay(plural: true)` directive to indicate that container expects the prop for that fragment to be a list of items instead of a single item. A query or parent that spreads a `@relay(plural: true)` fragment should do so within a plural field (ie a field backed by a [GraphQL list](http://graphql.org/learn/schema/#lists-and-non-null). For example:

```javascript
// Plural fragment definition
graphql`
fragment TodoItems_items on TodoItem @relay(plural: true) {
id
text
}`;

// Plural fragment usage: note the parent type is a list of items (`[TodoItem}]`)
fragment TodoApp_app on App {
items {
// parent type is a list here
...TodoItem_items
}
}
```

### `@relay(mask: Boolean)`

Relay by default will only expose the data for fields explicitly requested by a [component's fragment](./fragment-container.html#createfragmentcontainer), which is known as [data masking](./thinking-in-relay#data-masking).
By default Relay will only expose the data for fields explicitly requested by a [component's fragment](./fragment-container.html#createfragmentcontainer), which is known as [data masking](./thinking-in-relay#data-masking).

However, `@relay(mask: false)` can be used to prevent data masking; when including a fragment and annotating it with `@relay(mask: false)`, its data will be available directly to the parent instead of being masked for a different container.

Expand Down Expand Up @@ -144,7 +153,23 @@ Will cause a generated file to appear in `./__generated__/MyComponent.graphql`,
with both runtime artifacts (which help to read and write from the Relay Store)
and [Flow types](https://flow.org/) to help you write type-safe code.

The Relay Compiler is responsible for generating code as part of a build step which, at runtime, can be used statically. By building the query ahead of time, the client's JS runtime is not responsible for generating a query string, and fields that are duplicated in the query can be merged during the build step, to improve parsing efficiency. If you have the ability to persist queries to your server, the compiler's code generation process provides a convenient time to convert a query or mutation's text into a unique identifier, which can greatly reduce the upload bytes required in some applications.
The Relay Compiler is responsible for generating code as part of a build step which can then be referenced at runtime. By building the query ahead of time, the Relay's runtime is not responsible for generating a query string, and various optimizations can be performed on the query that could be too expensive at runtime (for example, fields that are duplicated in the query can be merged during the build step, to improve efficiency of processing the GraphQL response).

### Persisting queries
Relay Compiler supports the use of **persisted queries**, in which each version of a query is associated to a unique ID on the server and the runtime uploads only the persisted ID instead of the full query text. This has several benefits: it can significantly reduce the time to send a query (and the upload bytes) and enables *whitelisting* of queries. For example, you may choose to disallow queries in text form and only allow queries that have been persisted (and that presumably have passed your internal code review process).

Persisted queries can be enabled by instructing Relay Compiler to emit metadata about each query, mutation, and subscription into a JSON file. The generated file will contain a mapping of query identifiers to query text, which you can then save to your server. To enable persisted queries, use the `--persist-output` flag to the compiler:

```js
"scripts": {
"relay": "relay-compiler --src ./src --schema ./schema.graphql --persist-output ./path/to/persisted-queries.json"
}
```

Relay Compiler will then create the id => query text mapping in the path you specify. You can then use this complete
json file in your server side to map query ids to operation text.

For more details, refer to the [Persisted Queries section](./persisted-queries.html).

### Set up relay-compiler

Expand Down Expand Up @@ -222,7 +247,6 @@ This would produce three generated files, and two `__generated__` directories:
* `src/Components/__generated__/DictionaryComponent_definition.graphql.js`
* `src/Queries/__generated__/DictionaryQuery.graphql.js`


### Importing generated definitions

Typically you will not need to import your generated definitions. The [Relay Babel plugin](./installation-and-setup.html#setup-babel-plugin-relay) will then convert the `graphql` literals in your code into `require()` calls for the generated files.
Expand Down
149 changes: 149 additions & 0 deletions docs/Modern-PersistedQueries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
id: persisted-queries
title: Persisted Queries
---

The relay compiler supports persisted queries which is useful because:

* the client operation text becomes just an md5 hash which is usually shorter than the real
query string. This saves upload bytes from the client to the server.

* the server can now whitelist queries which improves security by restricting the operations
that can be executed by a client.

## Usage on the client

### The `--persist-output` flag
In your `npm` script in `package.json`, run the relay compiler using the `--persist-output` flag:

```js
"scripts": {
"relay": "relay-compiler --src ./src --schema ./schema.graphql --persist-output ./path/to/persisted-queries.json"
}
```

The `--persist-ouput` flag does 3 things:

1. It converts all query and mutation operation texts to md5 hashes.

For example without `--persist-output`, a generated `ConcreteRequest` might look like below:

```js
const node/*: ConcreteRequest*/ = (function(){
//... excluded for brevity
return {
"kind": "Request",
"operationKind": "query",
"name": "TodoItemRefetchQuery",
"id": null, // NOTE: id is null
"text": "query TodoItemRefetchQuery(\n $itemID: ID!\n) {\n node(id: $itemID) {\n ...TodoItem_item_2FOrhs\n }\n}\n\nfragment TodoItem_item_2FOrhs on Todo {\n text\n isComplete\n}\n",
//... excluded for brevity
};
})();
```

With `--persist-output <path>` this becomes:

```js
const node/*: ConcreteRequest*/ = (function(){
//... excluded for brevity
return {
"kind": "Request",
"operationKind": "query",
"name": "TodoItemRefetchQuery",
"id": "3be4abb81fa595e25eb725b2c6a87508", // NOTE: id is now an md5 hash of the query text
"text": null, // NOTE: text is null now
//... excluded for brevity
};
})();
```

2. It generates a JSON file at the `<path>` you specify containing a mapping from query ids
to the corresponding operation texts.

```js
"scripts": {
"relay": "relay-compiler --src ./src --schema ./schema.graphql --persist-output ./src/queryMaps/queryMap.json"
}
```

The example above writes the complete query map file to `./src/queryMaps/queryMap.json`. You need to ensure all the directories
leading to the `queryMap.json` file exist.

### Network layer changes
You'll need to modify your network layer fetch implementation to pass a documentId parameter in the POST body instead of a query parameter:
```js
function fetchQuery(operation, variables,) {
return fetch('/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
documentId: operation.id, // NOTE: pass md5 hash to the server
// query: operation.text, // this is now obsolete because text is null
variables,
}),
}).then(response => {
return response.json();
});
}
```
## Executing Persisted Queries on the Server
To execute client requests that send persisted queries instead of query text, your server will need to be able
to lookup the query text corresponding to each id. Typically this will involve saving the output of the `--persist-output <path>` JSON file to a database or some other storage mechanism, and retrieving the corresponding text for the ID specified by a client.
For universal applications where the client and server code are in one project, this is not an issue since you can place
the query map file in a common location accessible to both the client and the server.
### Compile time push
For applications where the client and server projects are separate, one option is to have an additional npm run script
to push the query map at compile time to a location accessible by your server:
```js
"scripts": {
"push-queries": "node ./pushQueries.js",
"relay": "relay-compiler --src ./src --schema ./schema.graphql --persist-ouput <path> && npm run push-queries"
}
```
Some possibilities of what you can do in `./pushQueries.js`:
* `git push` to your server repo
* save the query maps to a database
### Run time push
A second more complex option is to push your query maps to the server at runtime, without the server knowing the query ids at the start.
The client optimistically sends a query id to the server, which does not have the query map. The server then in turn requests
for the full query text from the client so it can cache the query map for subsequent requests. This is a more complex approach
requiring the client and server to interact to exchange the query maps.
### Simple server example
Once your server has access to the query map, you can perform the mapping. The solution varies depending on the server and
database technologies you use, so we'll just cover the most common and basic example here.

If you use `express-graphql` and have access to the query map file, you can import the `--persist-output` JSON file directly and
perform the matching using the `matchQueryMiddleware` from [relay-compiler-plus](https://github.com/yusinto/relay-compiler-plus).

```js
import Express from 'express';
import expressGraphql from 'express-graphql';
import {matchQueryMiddleware} from 'relay-compiler-plus';
import queryMapJson from './path/to/persisted-queries.json';
const app = Express();
app.use('/graphql',
matchQueryMiddleware(queryMapJson),
expressGraphl({schema}));
```

## Using `--persist-output` and `--watch`
It is possible to continuously generate the query map files by using the `--persist-output` and `--watch` options simultaneously.
This only makes sense for universal applications i.e. if your client and server code are in a single project
and you run them both together on localhost during development. Furthermore, in order for the server to pick up changes
to the `queryMap.json`, you'll need to have server side hot-reloading set up. The details on how to set this up
is out of the scope of this document.
Loading

0 comments on commit 708f842

Please sign in to comment.