-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
c761128
commit 708f842
Showing
8 changed files
with
643 additions
and
359 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.