Currently, if we want to use HTTP caching in GraphQL, we must use a GraphQL server that supports persistent queries. This is because persistent queries already store GraphQL queries on the server; Therefore, we do not need to provide this information in the request.
In order for the GraphQL server to also support HTTP caching over a single endpoint, the GraphQL query must be provided as a URL parameter. The GraphQL over HTTP specification will hopefully achieve this goal by providing a standardized language for all GraphQL clients, servers, and libraries to interact with each other.
But I suspect that all attempts to pass GraphQL queries through URL parameters are far from ideal. This is because the URL parameter must be supplied as a single line value, so the query will need to be encoded or reformatted to make it hard to understand (for us humans, not machines).
For example, this is what a GraphQL query looks like when all the newlines are replaced with Spaces to fit a single line.
{ posts(limit:5) { id title @titleCase excerpt @default(value:"No title", condition:IS_EMPTY) author { name } tags { id name } comments(limit:3, order:"date|DESC") { id date(format:"d/m/Y") author { name } content } } }
Copy the code
Can you make sense of it? I don’t know.
This is how the GraphiQL client encodes a simple query {posts {ID title}} as a URL parameter.
%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D
Copy the code
Again, we don’t know what’s going on here.
Both examples illustrate a problem: From a technical standpoint, single-line GraphQL queries work, transferring information to the server, but it’s not easy for people to read and write these queries.
There are many benefits to being able to operate with single-line queries. For example, we could write queries directly in the browser’s address bar without needing some GraphQL client.
It’s not that I don’t like the GraphQL client — in fact, I do like GraphiQL. But I really don’t like the idea that I’m relying on them.
In other words, we can benefit from a query syntax that allows people to query.
- Write a query directly on a single line
- See the contents of a single line query at a glance
This is a formidable challenge. But it is not insurmountable.
In this article, I will introduce an alternative syntax that supports being “easy to read and write in a single line” by us humans.
I’m not really proposing to introduce this syntax into GraphQL– I know it will never happen. However, this syntax design process illustrates some of the issues we must pay attention to when designing the GraphQL over HTTP specification.
Why is GraphQL’s syntax so hard to understand in a single line?
Let’s take a look at the problems with the GraphQL syntax before generalizing it to other grammars.
Identify the problem
In my opinion, the difficulty comes from the nested fields in the GraphQL query, which can be moved back and forth throughout the query. It is this comings and goings that makes it difficult to grasp when it is written as a line.
This is not hard to understand if nesting in a query only advances. Take this query for example.
{
posts {
id
title
excerpt
comments {
id
date
content
author {
id
name
url
posts {
id
title
}
}
}
}
}
Copy the code
Here, nesting just goes forward.
GraphQL query, only forward.
When we look at the query that always pushes forward and scan it from left to right, we can still understand what entity each field belongs to.
{ posts { id title excerpt comments { id date content author { id name url posts { id title } } } } }
Copy the code
Now, consider the same GraphQL query, but rearrange the fields so that the leaf appears after the join.
{
posts {
id
comments {
id
date
author {
posts {
id
title
}
id
name
url
}
content
}
title
excerpt
}
}
Copy the code
In this case, we can say that the field is both forward and backward.
GraphQL queries, forward and backward.
The query can be written on a single line like this.
{ posts { id comments { id date author { posts { id title } id name url } content } title excerpt } }
Copy the code
Now, understanding this query is not so easy. After the cascade (that is, immediately after the join), we may not remember what entity was before, so we have no idea where the field belongs.
Which entity do these fields belong to?
My guess is that it has to do with the brain’s limited short-term memory, which can hold no more than a few items at a time).
When there are many levels of advance and retreat, then it is difficult to fully grasp. The question is understandable.
{ posts { id comments { id date children { id author { name url } content } author { posts { id title tags { name } } id name friends { id name } url } content } title excerpt } author { name } }Copy the code
But there’s no way to make its single row equivalent meaningful.
{ posts { id comments { id date children { id author { name url } content } author { posts { id title tags { name } } id name friends { id name } url } content } title excerpt } author { name } }Copy the code
In summary, GraphQL queries are not easy to represent in a single line, because of its nested behavior, we humans can understand it.
Inductive problem
This problem is not unique to GraphQL. In fact, it will happen in a syntax — any syntax — in which elements move forward and backward.
Take JSON as an example.
{ "name": "leoloso/PoP", "description": "PoP monorepo", "repositories": [ { "type": "package", "package": { "name": "leoloso-pop-api-wp/newsletter-subscriptions-rest-endpoints", "version": "master", "type": "wordpress-plugin", "source": { "url": "https://gist.github.com/leoloso/6588f6c1bdcce82fc317052616d3dfb4", "type": "git", "reference": "master" } } }, { "type": "package", "package": { "name": "Leoloso-pop-api-wp /disable-user-edit-profile", "version": "0.1.1", "type": "wordpress-plugin", "source": {"url": "https://gist.github.com/leoloso/4e367eb8d8014a7aa7580567608bd5b4", "type": "git", "reference": "master" } } }, { "type": "vcs", "url": "https://github.com/leoloso/wp-muplugin-loader.git" } ], "minimum-stability": "Dev", "-- - stable" : true, "require" : {" PHP ":" 8.0 ", "getpop/API - rest" : "dev-master", "getpop/engine-wp-bootloader": "dev-master" }, "extra": { "branch-alias": { "dev-master": "1.0-dev"}, "installer-types": ["graphiql-client", "graphql-voyager"], "installer-paths": { "wordpress/wp-content/mu-plugins/{$name}/": [ "type:wordpress-muplugin" ], "wordpress/wp-content/plugins/{$name}/": [ "type:wordpress-plugin", "getpop/engine-wp-bootloader" ] } }, "config": { "sort-packages": true } }Copy the code
Converting it to a single line makes it really hard to understand.
{ "name": "leoloso/PoP", "description": "PoP monorepo", "repositories": [ { "type": "package", "package": { "name": "leoloso-pop-api-wp/newsletter-subscriptions-rest-endpoints", "version": "master", "type": "wordpress-plugin", "source": { "url": "https://gist.github.com/leoloso/6588f6c1bdcce82fc317052616d3dfb4", "type": "git", "reference": "master" } } }, { "type": "package", "package": { "name": "Leoloso-pop-api-wp /disable-user-edit-profile", "version": "0.1.1", "type": "wordpress-plugin", "source": {"url": "https://gist.github.com/leoloso/4e367eb8d8014a7aa7580567608bd5b4", "type": "git", "reference": "master" } } }, { "type": "vcs", "url": "https://github.com/leoloso/wp-muplugin-loader.git" } ], "minimum-stability": "Dev", "-- - stable" : true, "require" : {" PHP ":" 8.0 ", "getpop/API - rest" : "dev-master", "getpop/engine-wp-bootloader": "dev-master" }, "extra": { "branch-alias": { "dev-master": "1.0-dev"}, "installer-types": ["graphiql-client", "graphql-voyager"], "installer-paths": { "wordpress/wp-content/mu-plugins/{$name}/": [ "type:wordpress-muplugin" ], "wordpress/wp-content/plugins/{$name}/": [ "type:wordpress-plugin", "getpop/engine-wp-bootloader" ] } }, "config": { "sort-packages": true } }Copy the code
What’s more, when the syntax uses spacing to nest its elements, it can’t even be written on a single line.
This is the case with YAML, for example.
services: _defaults: public: true autowire: true autoconfigure: true PoP\API\PersistedQueries\PersistedQueryManagerInterface: class: \PoP\API\PersistedQueries\PersistedQueryManager # Override the service PoP\ComponentModel\Schema\FieldQueryInterpreterInterface: class: \PoP\API\Schema\FieldQueryInterpreter PoP\API\Hooks\: resource: '.. /src/Hooks/*'Copy the code
Design a different query syntax
I’ll describe another design of the GraphQL syntax: the PQL syntax, used by the GraphQL of PoP (the GraphQL server in PHP I wrote), accepts via GET.
Since the GraphQL syntax’s problems stem from shrinking nested fields, the solution seems obvious: the flow of the query must be forward.
How does PQL achieve this? To prove this, let’s explore the syntax of PQL.
The field of grammar
In GraphQL, a field is written like this.
{
alias:fieldName(fieldArgs)@fieldDirective(directiveArgs)
}
Copy the code
In PQL, a field is written like this.
fieldName(fieldArgs)[@alias]<fieldDirective(directiveArgs)>
Copy the code
So it’s similar, but there are some differences.
- The alias is not placed before the field, but after it.
- Aliases are not used
:
, but with the@
(Optional[...].
(used as a bookmark, explained later) - Instructions are not used
@
Instead, it is identified by (and optionally (for “bookmarks”, explained later)).<... >
These differences are directly related to the always-forward process required for queries.
In my own experience, when I write queries directly into the browser address bar, I always think of alias names after I write the field names, not before. So, using the order in GraphQL, I had to go back to that location (press the left arrow key), add the alias, and then go back to the final location (press the right arrow key).
This is very troublesome. It makes more sense to place the alias after the field name, making it a natural flow.
It does not make sense to use :. When defining an alias after a field name. GraphQL uses this notation to make the JSON response respect the shape of the query. Once the order between fields and aliases has been reversed, it seems natural to use @.
This in turn means that we can no longer use @ to identify instructions. Instead, I chose a syntax around <… > (for example,
), so that instructions can also be nested (for example,
>), making it possible for GraphQL by PoP to support composable instruction functionality.
field
In GraphQL, you can add two or more fields by adding Spaces or line breaks between them.
{
foo
bar
}
Copy the code
We use the character in PQL | to separate fields.
foo|bar
Copy the code
We can already see visually how queries are grouped into a single row.
- There is no
{}
character - No Spaces or newlines
We can also see that queries can be composed directly in the browser with the URL parameter Query.
For example, execute the query URLid | __typename is: ${endpoint}? Query = id | __typename.
Using DevTools, we can see how GraphQL’s individual endpoints support HTTP caching.
GraphQL HTTP cache for a single endpoint.
For all the queries demonstrated below, there is a link _ execute the query in the browser _. Click on them to see how PQL works on actual sites in production.
Make the query visually appealing
Like GraphQL, newlines (and Spaces) add no semantics. Therefore, we can easily add a newline character to help visualize the query.
foo|
bar
Copy the code
When using Firefox, you can copy this query (from a text editor, web page, etc.) and paste it into the browser’s address bar, where all line breaks are automatically removed to form an equivalent single-line query.
Copy/paste the query in Firefox.
The connection
GraphQL uses the character {} to define the connected data.
{
posts {
author {
id
}
}
}
Copy the code
In PQL, queries can only go forward, not backward. So, there is one that is equal to {, that is., but not equal to}, because you don’t need it.
posts.
author.
id
Copy the code
Execute the query in the browser.
We can put the | and. Combined to get multiple fields for any entity. Consider this GraphQL query.
{
posts {
id
title
author {
id
name
url
}
}
}
Copy the code
Its equivalent in PQL would be.
posts.
id|
title|
author.
id|
name|
url
Copy the code
Execute the query in the browser.
At this point, we can face the challenge: how does PQL only accept forward fields?
The syntax for forward-only processes
The queries seen above are all forward. Now let’s deal with queries that also need to retreat, such as this GraphQL query.
{
posts {
id
author {
id
name
url
}
comments {
id
content
}
}
}
Copy the code
PQL uses characters to connect elements. It is similar to |, used to connect to fields, but there is a fundamental difference: the right of the field from the roots began to traverse the graph again.
The query above, then, has such an equivalent in PQL.
posts.
id|
author.
id|
name|
url,
posts.
comments.
id|
content
Copy the code
Execute the query in the browser.
Please note, in order to visually appealing, the name | and the left of the url have the same filling, because | maintained the same path posts. The author. But after, there is no left padding because the query starts at the root again.
We can think of this query as retreating as well.
Forward and backward queries in PQL.
In GraphQL, we can go back to the previous position in the query — the parent node in the graph — the same number of times as the level we traversed. But in PQL, we can’t do that: we always go all the way back to the root of the graph.
Starting at the root again, we must specify the full path to the node again to continue adding fields. This makes the query more verbose. For example, the posts path in the query above appears once in GraphQL, but twice in PQL.
This redundancy forces us humans to recreate paths as we read and write queries at each level of the graph. Doing so allows us to understand the query when expressing it in a single line.
posts.id|author.id|name|url,posts.comments.id|content
Copy the code
Because we recreate the path in our mind, we don’t have the short-term memory problems that cause us to get disoriented when looking at GraphQL queries.
Use bookmarks to eliminate long text
Having to recreate the entire path to a node can become an annoyance.
Consider this GraphQL query.
{
users {
posts {
author {
id
name
}
comments {
id
content
}
}
}
}
Copy the code
And its equivalent in PQL.
users.
posts.
author.
id|
name,
users.
posts.
comments.
id|
content
Copy the code
Execute the query in the browser.
To retrieve the Comments field, we need to add users.posts. Again. The deeper the graph, the longer the replication path.
To solve this problem, PQL introduces a new concept: “bookmarks”, which provides a shortcut to an already-traveled path so that we can easily continue loading data from this point.
When we iterate over a path, we use […] To define a bookmark, and then refer to its bookmark with the same […] Automatically retrieves the path from the root of the query.
In the above query, we can bookmark users.posts.
users.
posts[userposts].
author.
id|
name,
[userposts].
comments.
id|
content
Copy the code
Execute the query in the browser.
To make things easier to understand, we can also add padding to the left of the applied bookmark to match the padding of its path (so comments appear under posts).
users.
posts[userposts].
author.
id|
name,
[userposts].
comments.
id|
content
Copy the code
With bookmarks, we can still understand queries when expressing them in a single line.
users.posts[userposts].author.id|name,[userposts].comments.id|content
Copy the code
If we need to define both a bookmark and an alias, we can embed the @ symbol in […]. .
users.
posts[@userposts].
author.
id|
name,
[userposts].
comments.id|
content
Copy the code
Execute the query in the browser.
Reduced field parameter
In GraphQL, values in String field parameters must be quoted “…” .
{
posts {
id
title
date(format: "d/m/Y")
}
}
Copy the code
It turns out that having to enter these quotes when composing queries in the browser is pretty annoying; I often forget them and then have to use the ARROW keys to navigate left or right to add them.
Therefore, string quotes can be omitted in PQL.
posts.
id|
title|
date(format:d/m/Y)
Copy the code
Execute the query in the browser.
String quotes are necessary, otherwise the query will be scrambled.
posts.
id|
title|
date(format:"d M, Y")
Copy the code
Execute the query in the browser.
In addition, field parameters can sometimes be implicit; For example, when a field has only one field parameter. In this case, PQL allows it to be omitted.
posts.
id|
title|
date(d/m/Y)
Copy the code
Execute the query in the browser.
variable
In GraphQL, variables are defined in the body of the request as an encoded JSON.
{
"query":"query ($format: String) {
posts {
id
title
date(format: $format)
}
}",
"variables":"{
\"format\":\"d/m/Y\"
}"
}
Copy the code
Instead, PQL uses HTTP standard input and passes variables via $_GET or $_POST.
? query= posts. id| title| date($format) &format=d/m/YCopy the code
Execute the query in the browser.
We can also pass variables under input.
? query= posts. id| title| date($format) &variables[format]=d/m/YCopy the code
Execute the query in the browser.
fragment
GraphQL uses fragments to reuse query sections.
{ users { ... userData posts { comments { author { ... userData } } } } } fragment userData on User { id name url }Copy the code
In PQL, fragments are defined the same way variables are: as input in $_GET or $_POST. They use — to refer to.
? query= users. --userData| posts. comments. author. --userData &userData= id| name| urlCopy the code
Execute the query in the browser.
Fragments can also be defined under input.
? query= users. --userData| posts. comments. author. --userData &fragments[userData]= id| name| urlCopy the code
Execute the query in the browser.
Convert queries between GraphQL and PQL syntax
PQL is a superset of the GraphQL query syntax. Therefore, any query written using standard GraphQL syntax can also be written in PQL.
Conversely, not every query written in PQL can be written in GraphQL syntax, because PQL supports features that GraphQL does not, such as composable fields and composable instructions.
The PQL contains most of the same elements.
- Fields and field parameters
- Instructions and instruction parameters
- The alias
- fragment
- variable
The elements it does not support are
- operation
- Operation name, variable definition, and default variable
on
Element to indicate what type/interface the fragment must be applied to.
Even if these elements are not supported, their basic functionality is supported in different ways.
The operation is missing because it is no longer needed: we now have the option to request a query using GET (for queries) or POST (for mutations).
In GraphQL, the operation name is required only if the document contains many operations and we need to specify which operation to perform, or perhaps several operations together, via @export.
In the former case, this is not necessary for PQL — we pass only the queries that must be executed, not all of them.
In the latter case, multiple operations can be executed together in a single request, while maintaining their order of execution, by using; And connect them like this.
posts.
author.
id|
name|
url;
posts.
comments.
id|
content
Copy the code
Execute the query in the browser.
In GraphQL, variable definitions are used to define the type of a variable, enabling clients such as GraphiQL to display errors when the type is different. This is a nice feature, but not really needed for the execution of the query itself.
Default variable values can be defined like any variable: by a URL parameter.
The on element is not needed because we can use the directive @include to find out the type of the object, taking a composable field, isType, as an argument, and depending on this value, apply a predetermined fragment or not.
For example, take this GraphQL query.
{
customPosts {
__typename
... on Post {
title
}
}
}
Copy the code
This is equivalent to PQL.
customPosts.
__typename|
title<include(if:isType(Post))>
Copy the code
Execute the query in the browser.
Transform introspective queries
Let’s convert the introspective queries that GraphiQL (and other clients) use to get schema metadata from GraphQL syntax to PQL.
This introspection query is this.
query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ... FullType } directives { name description locations args { ... InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ... InputValue } type { ... TypeRef } isDeprecated deprecationReason } inputFields { ... InputValue } interfaces { ... TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ... TypeRef } } fragment InputValue on __InputValue { name description type { ... TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }Copy the code
The corresponding PQL query looks like this.
? query= __schema[schema]. queryType. name, [schema]. mutationType. name, [schema]. subscriptionType. name, [schema]. types. --FullType, [schema]. directives. name| description| locations| args. --InputValue &fragments[FullType]= kind| name| description| fields(includeDeprecated: true)[@fields]. name| description| args. --InputValue, [fields]. type. --TypeRef, [fields]. isDeprecated| deprecationReason, [fields]. inputFields. --InputValue, [fields]. interfaces. --TypeRef, [fields]. enumValues(includeDeprecated: true)@enumValues. name| description| isDeprecated| deprecationReason, [fields]. possibleTypes. --TypeRef &fragments[InputValue]= name| description| defaultValue| type. --TypeRef &fragments[TypeRef]= kind| name| ofType. kind| name| ofType. kind| name| ofType. kind| name| ofType. kind| name| ofType. kind| name| ofType. kind| name| ofType. kind| nameCopy the code
Execute the query in the browser. (Note that the query in this link is slightly different from the query above, because I still need itAdd pairs to the fragment.
The support of the token).
This is a query written on a single line.
? query=__schema[schema].queryType.name,[schema].mutationType.name,[schema].subscriptionType.name,[schema].types.--FullTyp e,[schema].directives.name|description|locations|args.--InputValue&fragments[FullType]=kind|name|description|fields(incl udeDeprecated: true)[@fields].name|description|args.--InputValue,[fields].type.--TypeRef,[fields].isDeprecated|deprecationReason,[field s].inputFields.--InputValue,[fields].interfaces.--TypeRef,[fields].enumValues(includeDeprecated: true)@enumValues.name|description|isDeprecated|deprecationReason,[fields].possibleTypes.--TypeRef&fragments[InputValue]= name|description|defaultValue|type.--TypeRef&fragments[TypeRef]=kind|name|ofType.kind|name|ofType.kind|name|ofType.kind| name|ofType.kind|name|ofType.kind|name|ofType.kind|name|ofType.kind|nameCopy the code
Some more examples
This query has a fragment containing nested paths, variables, instructions, and other fragments.
? query= posts(limit:$limit, order:$order). --postData| author. posts(limit:$limit). --postData &postData= id| title| --nestedPostData| date(format:$format) &nestedPostData= comments<include(if:$include)>. id| content &format=d/m/Y &include=true &limit=3 &order=titleCopy the code
Execute the query in the browser.
This query applies an instruction to a fragment and then applies it to all fields in that fragment.
? query= posts. id| --props<include(if:hasComments())> &fragments[props]= title| dateCopy the code
Execute the query in the browser.
Finally, in this blog post, there are many examples of single-line queries embedded directly as URL parameters, with additional attributes of PQL syntax (not described in this article).
conclusion
To support HTTP caching, we currently have to use a GraphQL server that supports persistent queries.
But what about GraphQL single endpoints? Can it also support HTTP caching? If so, is it possible to let people write queries without having to rely on clients or libraries?
The answer to these questions is: yes, it can. However, the GraphQL syntax currently hinders this approach due to its nested behavior.
In this article, I showed an alternative syntax, called PQL, that enables the GraphQL server to accept queries via URL parameters, while enabling people to read and write queries in a line, or even directly in the browser’s address bar.
Designing a URL-based query syntax for GraphQL first appeared on the LogRocket blog.