the-guild-org / graphql-gateway-specifications

6 stars 0 forks source link

[RFC] Persisted Operations #3

Open dotansimha opened 1 year ago

dotansimha commented 1 year ago

Summary

Persisted Operations (or, Persisted Queries) goal is to provide a security and hardening layer on top of the GraphQL engine. Instead of executing a regular GraphQL operation, the clients are required to use a special string (hash) that's being translated into the actual GraphQL operation body.

For a more comprehensive/official spec, please refer to https://github.com/graphql/graphql-over-http/pull/264/files

Hash / Identifier

The hash for the persisted operation is usually generated at build/development time. The hash could be a generated, consistent string, an opaque value, or an actual computed hash based on the GraphQL operation body (for example, SHA256).

Store

The store is in charge of storing the actual mapping between the hash and the GraphQL operation body.

Simple Key->Value structure

A simple approach to providing the data to the store is to use a JSON key->value structure, as implemented by GraphQL-Yoga (https://the-guild.dev/graphql/yoga-server/docs/features/persisted-operations#quick-start).

This structure aims to be as simple as possible, and things like versioning can be part of the hash.

{
     "someKey": "value"
}

Apollo Persisted Query Manifest

Apollo is managing their own format, based on JSON, for managing the stored data. This format is called apollo-persisted-query-manifest.

{
  "format": "apollo-persisted-query-manifest",
  "version": 1,
  "operations": [
    {
      "id": "e0321f6b438bb42c022f633d38c19549dea9a2d55c908f64c5c6cb8403442fef",
      "body": "query GetItem { thing { __typename } }",
      "name": "GetItem",
      "type": "query"
    }
  ]
}

Execution

Prior to the GraphQL engine execution, the gateway needs to translate the hash into the GraphQL operation. To do that efficiently, the server needs to be able to load the data from the store, and then allow the client to specify the hash.

There are several ways of exposing the persisted operations as entry points:

Relay

Relay client sends a custom JSON structure to the server, and it is expected to be handled this way:

{
  "doc_id": "hash",
   "variables": {},
}

https://relay.dev/docs/guides/persisted-queries/#network-layer-changes

HotChocolate

Based on Relay's standard, but with id field instead of doc_id. (https://chillicream.com/docs/hotchocolate/v13/performance/persisted-queries#client-expectations)

Wundergraph's HTTP routes

By default, Wundergraph disables the GraphQL endpoint (/graphql) and exposes HTTP-based routes for accessing the GraphQL operation.

This process also involved exposing the GraphQL queries as GET operations, and GraphQL mutations as POST operations:

query something {
   __typename
}

Will be translated into:

/something

Variables are either passed as ?variables={"myVar": "1"} or ?myVar=1.

Apollo manifest

Apollo is using the GraphQL extensions field to specify the store manifest version and the hash, over POST:

{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}}

Apollo also allows users to enable persisted operations over GET.

Non-persisted operations

A gateway can choose to allow the execution of non-persisted operations.

n1ru4l commented 11 months ago

The store could also be external, e.g. an S3 bucket as in a setup with many GraphQL clienrs you rarely can embed all all client operations within the gateway at deployment time. for several reasons: many different client versions (where every client has different GraphQL operations that beed to be supported) or logistical issues (client code being in many different repositories that you may or may not control).

n1ru4l commented 11 months ago

One thing that is often overlooked isbthat a GraphQL document can contain multiple executable operation definintions, where the actual operation being executed is determined via the operationName parameter. I think the persisted document protocol should still allow that to be as spec compatible as possible.

JoviDeCroock commented 11 months ago

Hey,

Wanted to jump in here to say that maybe we can formalise the request format with the rfc not sure if y'all have opinions here but omitting query is currently not allowed when strictly following the http spec. Specifying operationName should be allowed so folks are allowed to specify an exact operation from a hash imho.

n1ru4l commented 11 months ago

As for the protocol, I would prefer a non-/graphql endpoint protocol extension -and rather a /<prefix>/<documentHash>/<optionalExecutionalOperationName> format. Where depending on whether it is a mutation or query, either a POST request or GET request must be used. We would also need to figure out how to support this with The GraphQL over WS and GraphQL over SSE protocols.