Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Implement graphql.persisted as a stand-in value for persisted documents #77

Closed
kitten opened this issue Feb 24, 2024 · 2 comments
Closed
Labels
future 🔮 An enhancement or feature proposal that will be addressed after the next release

Comments

@kitten
Copy link
Member

kitten commented Feb 24, 2024

Summary

Currently, persisted documents are mainly achieved using two methods:

  • Either, a hash is created at runtime for documents that are in-source
  • or; a compilation step replaces documents with stand-ins with document IDs
    In both situations, a “manifest” is often created that contains a mapping of document IDs to GraphQL document strings (Record<string, string>) that can then be consumed by server-side GraphQL APIs.

The latter method is often preferred in situations where GraphQL documents must compile away for security and/or obscurity purposes. When only a limited number of GraphQL documents are accepted by the API, compiling them away and keeping them out of the source reduces request payload sizes and keeps the usage of GraphQL completely private.
(This basically turns GraphQL into RPCs)

Proposed Solution

Currently, gql.tada sees being a runtime tool as a benefit, and compilation tools are complex to maintain and complicated tasks, like replacing all graphql() calls with shallow ASTs is potentially not as maintainable.

Instead, we're envisioning a stand-in function to be a transparent way to integrate persisted documents.

A stand-in call would look like the following:

const query = graphql(`
  query HelloQuery {
    hello
  }
`);

const persistedQuery = graphql.persisted<typeof query>('HelloQuery');

The graphql.persisted call accepts a GraphQL document as a generic and accepts a document ID as its only argument.

During runtime the call returns a faux GraphQL document:

// eval:
const persistedQuery: TadaDocumentNode<...> = {
  kind: Kind.DOCUMENT,
  definitions: [],
  documentId: 'HelloQuery',
};

This means that:

  • During runtime, graphql.persisted only outputs a persisted document — not a full GraphQL document AST
  • During compilation, i.e. check-time, this can still carry the type of the full document (query in our example above)
  • In the compiled JS output, query (and all fragments it depends on) compile away, since the calls are side-effect free and are not referred to in JS values (as long as they're not part of other side-effects or assigned to other values)

This has the advantage of leveraging tree-shaking for compilation, and lets users transparently assign document IDs to queries.

Non-handwritten Document IDs

Since, most commonly, documents use hashes in their IDs, GraphQLSP (and a future CLI, See #76) would be tasked to suggest replacing and/or updating the argument with a hash instead.

Requirements

  • The graphql.persisted call must transparently take on the type that it's passed by the generic, an act like a full document
  • The graphql.persisted call must validate that the generic is a GraphQL operation document and that it receives a document ID literal
  • To avoid runtime errors it may take on the shape of { kind: 'Document', definitions: [], documentId: string }, where definitions is always an empty array

External tasks

  • We may need a configuration option that tells us the format of document IDs (e.g. SHA256)
    • The LSP can then suggest to update/populate the document ID
    • The LSP can allow for a code action to update/populate the document ID, regardless of the configuration option
    • The CLI can lint and highlight outdated document IDs
    • The CLI can auto-fix outdated document IDs, when prompted

Furthermore the CLI must allow a manifest to be exported that contains a mapping of document IDs to document strings.

  • This should work regardless of how the query is passed to the call, e.g. typeof query could be replaced by queryType (Type checker traversal needed)
  • This should work regardless of how the documentId argument is passed i.e. if it's passed via a separate variable or property-access/import (Type checker traversal needed)

Considerations

  • For some clients this may cause runtime issues as their logic relies on having a valid, traversable AST (urql, urql Graphcache, Apollo client)
  • Failing the above, automatic persisted queries are preferred
    • This may mean that the CLI has to be able to extract documents into a manifest even if no graphql.persisted call is used. TBD
  • We'll have to evaluate how many clients can read the documentId property out-of-the-box from the faux-AST-object to send a persisted call
@JoviDeCroock
Copy link
Member

There is a middle ground that would be good to cover here which is persisted-operations but the document-definitions not removed from the front-end bundle. This bridges the gap for i.e. urql-graphCache and apollo-client and isn't as loose as APQ because the origin is still locked down without allowing for arbitrary operations.

This would require three changes

  • Allow for a second argument Document where we'd add the doc_id to at runtime
  • Add support for the second argument to GraphQLSP so that we check either the typeQuery or the second argument
  • Add support for the second argument to the CLI so that we check either the typeQuery or the second argument

@kitten
Copy link
Member Author

kitten commented Apr 15, 2024

Resolved by #188

@kitten kitten closed this as completed Apr 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
future 🔮 An enhancement or feature proposal that will be addressed after the next release
Projects
None yet
Development

No branches or pull requests

2 participants