Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

[RFC] Persisted Document support #109

Closed
wants to merge 2 commits into from

Conversation

alangenfeld
Copy link

@alangenfeld alangenfeld commented Jul 20, 2016

This adds support to express-graphql for GraphQL persisted documents. This is a feature we have discussed in ad-hoc ways since open sourcing GraphQL, and are now attempting to demonstrate more concretely. Persisted documents are foundational to how we use GraphQL at Facebook, especially for our mobile clients. Additional background context is being added to the README.

The goal in adding this support to express-graphql is to provide a clear example of how this idea can be implemented which should allow it to be translated to any other GraphQL server middleware or implementation.

Notes:

  • persistValidatedDocument and providing the /persist endpoint is still a nascent idea, that needs the most scrutiny. Then once its settled, tests.

@ghost ghost added the CLA Signed label Jul 20, 2016
@alangenfeld
Copy link
Author

I am not the greatest JS engineer, so basic/style feedback is expected and welcome.

@coveralls
Copy link

Coverage Status

Coverage decreased (-16.8%) to 83.178% when pulling 370cdad on alangenfeld:persisted-document into 6dcf37a on graphql:master.

request: Request,
response: Response
): Promise<OptionsData> {
return new Promise(resolve => {
Copy link
Contributor

@kassens kassens Jul 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for new Promise:

return Promise.resolve(
  typeof options === 'function' ?
    options(request, response) :
    options
).then

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried that, but doing so prevents us from catching exceptions thrown from options when it is a function at [1]

@nodkz
Copy link

nodkz commented Aug 20, 2016

@alangenfeld @wincent @josephsavona why persisted document?

Document can't have variables for fetching. According to provided variables, we may get the absolutely different responses for same document (query) id.

I think better naming is persisted query. Word document brings to much ambiguity.

@dschafer
Copy link
Contributor

Yeah, this is a tricky nomenclature issue. The parameter is params.query, but params.query doesn't have to contain a GraphQL Query; it has to contain a GraphQL Document. So I could see either name working here.

I'm not sure Document is ambiguous in this context, since it refers to http://facebook.github.io/graphql/#sec-Language.Query-Document (or to the Document flowtype in graphql-js), but it doesn't match the existing parameter naming.

@nodkz
Copy link

nodkz commented Aug 20, 2016

QueryDocument will be a nice name, but not just document. Anyway for me, PersistedQuery is the best naming.

@dschafer please consider better name for flowtype parameter in graphql-js. Document is not good.

returns an id that can be used to load it later. This is used in conjunction
with `loadPersistedDocument`. Providing this function will enable persisting
documents via the `document` parameter at the `/persist` subpath. It is
recommended that this option only be enabled in development deployments.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me when you would want to use persistValidatedDocument in development. You basically have to be storing the persisted documents either in some database, or in your source code, right? And either way, hitting a persistValidatedDocument REST API on a development backend server is not going to be the easiest way to get these persisted documents into the place they need to go. In your development environment you can just write them to the db or put them in your source code directly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to build tooling for client development, you might want to include persisting all required client queries into your build process and have an API for this, making this interface HTTP is pretty obvious.

Persisting them to a database has the nice benefit (especially for larger organizations) that you don't need a new server deployment after adding (updating) a new query. You could even imagine that Github's public GraphQL API allowed clients to persist queries.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would have to be running your backend server during your build process, though - isn't that somewhat unusual? Rather than writing some backend code to persist the document, and making your build process hit an HTTP API on your running backend server, your build process could just execute the code to persist the document directly. I guess overall it seems like this is something people could implement themselves just as easily as if we added a persistValidatedDocument API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's unusual at all - in fact it seems the opposite to me. When building my iOS app or Android app or web app, I'd be really surprised if in that context I couldn't access my GraphQL API because it was also being built. I would expect a GraphQL API to be decoupled from the apps that are using it such that the build and deploy processes are also separate. Typically the build process for an iOS, Android or web app does not have access to a database, which is why exposing an HTTP API is beneficial.

@@ -53,6 +53,16 @@ The `graphqlHTTP` function accepts the following options:
* **`graphiql`**: If `true`, may present [GraphiQL][] when loaded directly
from a browser (a useful tool for debugging and exploration).

* **`loadPersistedDocument`**: A function that takes an input id and returns a
valid Document. If provided, this will allow your GraphQL endpoint to execute
a document specified via `documentID`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These documents typically each have a unique name, right? Why not use the name as the identifier instead of a separate documentID? I think that is how Airbnb is setting up their graphql backend.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the document might have a unique name like HomePageQuery, this is not enough, as the query likely changes over time as new client versions are released. It is important that old clients still get the old version of HomePageQuery.

README.md Outdated
@@ -156,6 +166,22 @@ new GraphQLObjectType({
});
```

## Persisted Documents

Putting control of the query in to clients hands is one of the primary benefits
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in to -> into
clients -> clients'

@josephsavona
Copy link

@lacker the problem with using the document name as the identifier is that the document changes over time, the id serves as a hash for a rev of a particular document.

@alangenfeld
Copy link
Author

Apologies for the long delay, its been a hectic few months in GraphQL land at FB. Thanks everybody for the feedback so far. I think the questions that are still open are:

  • naming: persisted document vs. persisted query
  • whether supporting /graphql/persist?document= if you provide persistValidatedDocument makes sense

@lacker
Copy link
Contributor

lacker commented Sep 27, 2016

My suggestions + rationale for the open questions:

  • "persisted query" because in the graphql community more people refer to them as "queries" than as "documents"
  • no support for /graphql/persist?document=. It doesn't seem like this provides enough benefit over people implementing their own endpoint.

@helfer
Copy link
Contributor

helfer commented Sep 27, 2016

In apollo-server we called them "stored operations", since that encompasses queries, mutations and subscriptions. I know that technically they can also contain fragments, which makes them documents, but at least it's a less confusing name than "queries". Using "queries" may lead people to believe that mutations can't be persisted.

@leebyron
Copy link
Contributor

leebyron commented Nov 3, 2016

Let's revive this and get it shored up and ready to land.

Some thoughts:

"Document" is absolutely the right name. We've been bad about clarity around names and in fact should consider renaming some of the fields accepted in this library to help with that. A Document is a whole GraphQL file or request. An operation is one query/mutation (a document may contain many in addition to fragments). A query is a kind of operation. Since we're typically talking about documents that have one operation that is a query, we say "GraphQL query" - but I think it's important to get the naming right for this which should allow persisting any kind of document.

Perhaps we should rearrange the API such that you provide both or neither functions. Right now we could assert one or the other, but I think the API could help with that. Maybe:

persistedDocuments: {
  save: document => id 
  load: id => document
}

I think we're missing an operation as well, which is getting the actual document text for a given id. Tools like GraphiQL would want to use this to populate the left pane. Maybe we should expose paths savepersisted and loadpersisted to represent both?

One last thought - should we be persisting the actual string text rather than the AST itself? Or perhaps allowing either with a typeof === 'string' check? I'm thinking if your document contains comments or preferred formatting, that a round trip through persistence should retain those.

@josh
Copy link

josh commented Nov 3, 2016

should we be persisting the actual string text rather than the AST itself?

I've been curious about the same thing for @github. Right now we're thinking of making our persisted IDs a SHA256 of the document text. Normalizing the document AST would let us store slightly different text documents under the same hashed ID. Seems like a good idea to me. Is there even a mechanize that would allow clients to retrieve the original document text? If they can only persist and execute operations, any comments wouldn't make a difference.

@leebyron
Copy link
Contributor

leebyron commented Nov 3, 2016

I think it would be valuable to retrieve the original document text for use with tools like GraphiQL. It would be immensely helpful for debugging to be able to take one of these persisted IDs and check what the server is actually running for it.

@leebyron
Copy link
Contributor

leebyron commented Nov 3, 2016

Also, if you'd prefer to have comments or formatting be irrelevant to the hash of a query, that's certainly something you could have a client decide by doing a hash(print(parse(text))) to normalize first.

@JeffRMoore
Copy link

Is there some value here to borrowing the prepared statement nomenclature from SQL? Many programmers are already familiar the concept of working with a statement in two phases, one phase to setup the statement and the second phase to execute it with parameters.

The methods in this example might become prepareDocument and loadPreparedDocument?

I wonder if "persist" terminology might require slightly more cognition to disambiguate. It is the communication pattern with the server that is being persisted, not necessarily the data being exchanged.

resolveOptionsData(options, request, response),
parseBody(request)
]).then(initData => {
const [ optionsData, bodyData ] = initData;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

travis.yml contains Node 0.12 with all tests currently passing. I believe destructuring isn't supported there.

@leebyron
Copy link
Contributor

I'll take over from here :)

@alangenfeld
Copy link
Author

Thanks @leebyron! Apologies for not being able to see this one through.

@raderio
Copy link

raderio commented Nov 1, 2017

Is someone using approach: on dev environment support mix(persisted queries & arbitrary queries) and on prod environment support only persisted queries?

@taion
Copy link

taion commented Dec 15, 2017

What's the status on this PR?

@j0k3r
Copy link

j0k3r commented Mar 26, 2018

Any news?

@alloy
Copy link

alloy commented Apr 9, 2018

@leebyron @kassens We’re now starting to use persisted queries at Artsy and so I’d like to contribute some time to take this PR over the finish line. Could you summarise the work left to do from your perspective?

Copy link
Contributor

@leebyron leebyron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for letting this sit. I'd love to have your help on this, @alloy.

From looking over the PR it seems that really this just needs a few things, repairing conflicts and cleaning things up a bit would be helpful!

I also think persistValidatedDocument should provide both the AST and the original query string, probably as (text, ast) => id so that the unedited text can be saved and a call to print be the user is unnecessary for most storage cases.

Similarly, It would be nice if loadPersistedDocument accepted a return type of text | ast and used typeof === 'string' to disambiguate

let response = await request(app)
.post(persistString({
document: '{test}'
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a test that posting data works instead of just formatting a query string

if (showGraphiQL) {
return null;
}
throw httpError(400, 'Must provide query string.');
throw httpError(400, 'Must provide query string or document ID.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably only say document ID should be provided if loadPersistedDocument is available

// Return 400: Bad Request if any validation errors exist.
response.statusCode = 400;
return { errors: validationErrors };
if (query) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 if statements are a bit confusing in sequence, I would expect:

if (documentID) {
} else if (query) {
} else {

@@ -278,7 +285,7 @@ function getGraphQLParams(urlData: Object, bodyData: Object): GraphQLParams {
// Name of GraphQL operation to execute.
const operationName = urlData.operationName || bodyData.operationName;

return { query, variables, operationName };
return { query, variables, operationName, documentID};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting (note: we should enable prettier on this repo)

@alloy
Copy link

alloy commented Apr 9, 2018

@leebyron Looks straightforward enough, thanks! 👍 I’ll start work on that this week.

@facebook-github-bot
Copy link

Thank you for your pull request. We require contributors to sign our Contributor License Agreement, and yours has expired.

Before we can review or merge your code, we need you to email cla@fb.com with your details so we can update your status.

@sibelius
Copy link

sibelius commented Jan 2, 2019

let's make this happen in 2019?

@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@alloy
Copy link

alloy commented Jan 2, 2019

I made many attempts at updating this PR, but the lack of context and it being so out of date made it hard :/ It’s definitely still on my TODO list, so yeah maybe this will be the year!

@sibelius
Copy link

sibelius commented Jan 5, 2019

this could be achieve in a middleware based way, so we don't need to make express-graphql more complex

like this https://gist.github.com/robrichard/d5a6ae7ecb3c9efc7fad7b3236d255e7 solution by @robrichard

using the middleware approach makes easy to work with express-graphql, koa-graphql, apollo-server and any other middleware based server

@josephsavona
Copy link

@sibelius that seems like a great approach

@Janpot
Copy link

Janpot commented Jan 5, 2019

But it should be standardized, no? To guarantee interoperability between the different client and server libraries. Middleware or not is just an implementation detail, right?

@alloy
Copy link

alloy commented Jan 7, 2019

Yeah agreed that that could be a good approach 👍 I do think we may still need to add some change here (eg to be able to use already parsed and validated documents), but those should be easy to do from scratch.

@sibelius
Copy link

sibelius commented Jan 7, 2019

we could have another package to handle all this persisted queries validation and logic

Base automatically changed from master to main February 10, 2021 15:09
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet