🥭 apollo-juicer 🥭
Create simple reusable GraphQL queries.
Table of Contents
Installation
npm i @parhelion/apollo-juicer
Building
git clone https://github.com/haxius/apollo-juicer.git
yarn install
yarn run build
Note: If you wish to see a non-minified and obfuscated compile use
yarn run build-dev
Testing / Linting
<...git clone and yarn install>
yarn run test
yarn run lint
Explanation
Traditionally GraphQL queries and mutations dot a modern application.
Sometimes they are organized neatly into folders and other times they can be sporatically placed.
When changes happen to the queries from the server-side this can result in multiple locations needing updates on the client-side.
apollo-juicer
attempts to rectfy this by providing a common-root in which one can derive queries.
Traditional example:
file: graphql/products.js
export const PRODUCTS = gql`
query products($category: ID) {
products(category: $category) {
name
price
}
}
`;
export const PRODUCT_TYPES = gql`
query productTypes {
productTypes {
name
}
}
`;
Using Apollo it is reasonable to assume that one might consume either of these queries using the Query component:
<Query query={PRODUCT_TYPES}>...</Query>
or
<Query query={PRODUCTS} variables={{ category: "1234" }}>...</Query>
With traditional Apollo if one wanted a component to consume both queries one would either nest a query (ouch), or more properly combine the queries with a special query:
export const PRODUCTS_WITH_TYPES = gql`
query productsWithTypes($category: ID) {
products(category: $category) {
name
price
}
productTypes {
name
}
}
`;
The problem with this is the duplication of work, and duplication of queries. If a change is made on the API one now has more than one place to fix it. Now one could partially rectify this by exporting strings from their GraphQL Files like such:
file: graphql/products.js
export const PRODUCTS = `
products(category: $category) {
name
price
}
`;
export const PRODUCT_TYPES = `
productTypes {
name
}
`;
Then when they wish to consume it they could do something like this:
export const PRODUCTS_WITH_TYPES = gql`
query productsWithTypes ($category ID) {
${PRODUCTS}
${PRODUCT_TYPES}
}
`;
But what happens when one or more of those data sets have duplicate arguments?
file: graphql/products.js
export const PRODUCTS = `
products(category; $category) {
name
price
}
`;
export const PRODUCT_TYPES = `
productTypes(category: $category) {
name
}
`;
One would need a solution that would provide unique naming for the arguments and connect the pieces so the output would look like this:
export const PRODUCTS_WITH_TYPES = gql`
query productsWithTypes($productCategory: ID, $typeCategory: ID) {
products(category: $productCategory) {
name
price
}
productTypes(category: $typeCategory) {
name
}
}
`;
Queue apollo-juicer
.
Usage
There are two methods exported by apollo-juicer
.
The output of which should be self explanatory.
buildQuery
In order to use these methods, one has to construct their queries (currently) as JavaScript objects.
The PRODUCTS
query mentioned above would be exported instead as follows:
file: graphql/products.js
export const PRODUCTS = {
name: "products",
alias: "product",
variables: [{ name: "parent", type: "ID" }],
results: ["name", "price"]
};
export const PRODUCT_TYPES = {
name: "productTypes",
alias: "type",
variables: [{ name: "parent", type: "ID" }],
results: [ "name" ]
};
Now it's the users choice when or how they want to convert this into an actual gql
query. One could export it wrapped or wrap it on import (preferred) but all that is neccessary to convert it into a usable query is:
import { buildQuery } from "@parhelion/apollo-juicer";
import { PRODUCTS } from "./graphql/products.js";
const PRODUCTS_QUERY = buildQuery(PRODUCTS);
...
inSomeComponentsRenderMethod() {
return (
<Query query={PRODUCTS_QUERY} variables={{ parent: "1234" }}>
...
</Query>
)
}
How does this benefit the developer? Did we not just add another layer to constructing a GraphQL query?
Enter combineQueries()
combineQueries
Using the example again from above:
file: graphql/products.js
export const PRODUCTS = `
products(category; $category) {
name
price
}
`;
export const PRODUCT_TYPES = `
productTypes(category: $category) {
name
}
`;
If one wishes to combine multiple queries together without needing to:
- worry about duplicate argument names
- write a third query
import { buildQuery } from "@parhelion/apollo-juicer";
import { PRODUCTS, PRODUCT_TYPES } from "./graphql/products.js";
const PRODUCTS_QUERY = combineQueries([PRODUCTS, PRODUCT_TYPES]);
...
inSomeComponentsRenderMethod() {
return (
<Query query={PRODUCTS_QUERY} variables={{ productParent: "1234", typeParent: "5678" }}>
...
</Query>
)
}
Behind the scenes, the query that is generated would look something like this:
query combined($productCategory: ID, $typeCategory: ID) {
products(category: $productCategory) {
name
price
}
productTypes(category: $typeCategory) {
name
}
}
Wasn't that much cleaner?
-
apollo-juicer
uses the query alias to construct deduped arguments -
PRODUCT
andPRODUCT_TYPES
are now re-usable ** That is to say, one could combine them in many different areas of the app in many different ways with other query objects without ever needing to rewrite the query. ** In addition to that, if the GraphQL definition forproducts
orproductTypes
changes, one only has to update the code in one place.
Advanced
Both buildQuery
and combineQueries
have an extra object parameter that can be passed to request alternative formatting to the resultset.
===
buildQuery(<query>[, {options}])
===
buildQuery
can be passed the following options:
wrapper: boolean = true
: When set to false, the query (...args) {}
portion of the result is omitted. Just returning the data-set. This is used internally by combineQueries
when building the individual datasets but can be used if needed elsewhere.
omitGql: boolean = false
: When set to true, the result will be returned as a string instead of a graphql-tag
.
===
combineQueries([<query>, <query>, ...][, {options}])
===
combineQueries
can be passed the following options:
omitGql: boolean = false
: When set to true, the result will be returned as a string instead of a graphql-tag
.
ClosingThoughts
Ultimately this is not a perfect solution and could be improved upon in a few key ways.
-
apollo-juicer
SHOULD be modified to parse GraphQL strings: This would allow the consumer to simply pass it a traditional GraphQL string and one would not need to objectize their queries. New users to apollo-juicer could simply import it into their project and use the combineQueries method (buildQuery would no longer be needed) right out of the box. -
apollo-graphql
could officially support parameterized fragments: This would eliminate the need for this project to exist at all and it could be deprecated.
I, the author, think that this is an imperfect and temporary solution. There are probably other (and perhaps better) solutions out there.
Please do not hesitate to open an issue or open a pull request if you think things could be improved.
Support
Please open an issue for support.
Contributing
Please contribute using Github Flow. Create a branch, add commits, and open a pull request.