this idea is in development in https://github.com/sw-yx/babel-blade, head over to check it out
this is a macro (read about babel-plugin-macros if you are not familiar) for solving the "double declaration problem" in GraphQL queries. (mutations might be tackled some other day).
What is the "double declaration problem"? Simply it is the bad developer experience of having to declare what you want to query in the GraphQL template string, and then again when you are using the data in your application. Ommissions are confusing to debug and overfetching due to stale queries is also a problem.
Here is a typical graphql query using urql (taken straight from urql's docs):
import { Connect, query } from "urql";
const QueryString = `
query Movie($id: String) {
movie(id: $id) {
id,
title,
description,
genres,
poster {
uri
}
}
}
`;
const Movie = ({ id, onClose }) => (
<div>
<Connect
query={query(QueryString, { id: id })}
children={({ loaded, data }) => {
return (
<div className="modal">
{loaded === false ? (
<p>Loading</p>
) : (
<div>
<h2>{data.movie.title}</h2>
<p>{data.movie.description}</p>
<button onClick={onClose}>Close</button>
</div>
)}
</div>
);
}}
/>
</div>
);
you see how title
and description
are specified twice, while poster
and genre
aren't even used.
Using blade.macro
, you can now write:
import { Connect, query } from 'urql';
const movieQuery = createRazor() // doesnt take arguments for now
const Movie = ({ id, onClose }) => (
<div>
<Connect
query={query(movieQuery, { id: id })} // razor transpiles into a query string
children={({ loaded, data }) => {
const DATA = movieQuery(data) // razor(foobar) initializes DATA as a blade, names query as DATA
const movie = DATA.movie // `movie` is an alias. `movie` is also a blade now
return (
<div className="modal">
{loaded === false ? (
<p>Loading</p>
) : (
<div>
<h2>{movie.test.title}</h2>
<p>{movie.monkey.chimp.description}</p>
<button onClick={onClose}>Close</button>
</div>
)}
</div>
);
}}
/>
</div>
);
This transpiles to:
import { Connect, query } from 'urql';
const Movie = ({ id, onClose }) => (
<div>
<Connect
query={query("query DATA { movie { test { title }, monkey { chimp { description }}}}", { id: id })} // razor transpiles into a query string
children={({ loaded, data }) => {
const DATA = data // razor(foobar) initializes DATA as a blade, names query as DATA
const movie = DATA.movie // `movie` is an alias. `movie` is also a blade now
return (
<div className="modal">
{loaded === false ? (
<p>Loading</p>
) : (
<div>
<h2>{movie.test.title}</h2>
<p>{movie.monkey.chimp.description}</p>
<button onClick={onClose}>Close</button>
</div>
)}
</div>
);
}}
/>
</div>
);
a key insight that makes this work:
- graphql data objects are never functions, so we can overload the "blade" as a function call to pass arguments to our graphql queries.
- aliases must be globally unique, just like variable declarations...
How we execute this code is a challenging task.
There are five big aspects to take care of:
makeBlade(name, args)
should tag the identifier it is assigned to (e.g.blade
) as a special identifier. This is the start of the GraphQL query.- when
blade(foo)
is called onfoo
, it tagsfoo
as a "blade", which is a piece of a GraphQL query. any descendants of a blade are also blades - blades can be called as functions to supply arguments to the blade
- assignments create aliases!
- wherever
blade
is referenced as a variable, the final GraphQL query is to be inserted
The job of a razor is:
- if a simple reference, to replace as a graphql query string
- if called, to make the assigned identifier into a blade. optionally passing along the query params.
One razor, many blades.
The job of a blade is:
- if the property is called as a function, add those arguments to the set
- if a property is accessed, add it to the graphql set
- if the property is assigned, make it an alias
- search scope for more references, recursively
The name comes from "roller blade", also known as inline skates. The inspiration comes from a boss who described this as "inline GraphQL". But also it just sounds cool so there we go.
accounting for as much of the graphql spec as possible
- operation name
- arguments and directives
- fields
- autogenerated aliases
- query variables '$'
- separate fragments (paritally done)
- inline fragments (and union types)
- meta fields
we could try a JSX version:
import { Connect, query } from "urql";
import makeBlade from "blade.macro";
const MovieQuery = makeBlade() // query is named "MovieQuery"
const Movie = ({ id, onClose }) => (
<div>
<Connect
query={query(MovieQuery, { id: id })}
children={({ loaded, data }) => {
return (
<MovieQuery movie={id}>
{data => (
<div className="modal">
{loaded === false ? (
<p>Loading</p>
) : (
<div>
<h2>{data.movie.title}</h2>
<p>{data.movie.description}</p>
<button onClick={onClose}>Close</button>
</div>
)}
</div>
)}
</MovieQuery>
);
}}
/>
</div>
);