graphql-anywhere-optimized
Run a GraphQL query anywhere, without a GraphQL server or a schema. Just pass in one resolver. Use it together with graphql-tag.
npm install graphql-anywhere graphql-tag
I think there are a lot of potentially exciting use cases for a completely standalone and schema-less GraphQL execution engine. We use it in Apollo Client to read data from a Redux store with GraphQL.
Let's come up with some more ideas - below are some use cases to get you started!
API
import graphql from 'graphql-anywhere'
graphql(resolver, document, rootValue?, context?, variables?, options?)
-
resolver
: A single resolver, called for every field on the query.- Signature is:
(fieldName, rootValue, args, context, info) => any
- Signature is:
-
document
: A GraphQL document, as generated by the template literal fromgraphql-tag
-
rootValue
: The root value passed to the resolver when executing the root fields -
context
: A context object passed to the resolver for every field -
variables
: A dictionary of variables for the query -
options
: Options for execution
Options
The last argument to the graphql
function is a set of graphql-anywhere
-specific options.
-
resultMapper
: Transform field results after execution.- Signature is:
(resultFields, resultRoot) => any
- Signature is:
-
fragmentMatcher
: Decide whether to execute a fragment. Default is to always execute all fragments.- Signature is:
(rootValue, typeCondition, context) => boolean
- Signature is:
Resolver info
info
, the 5th argument to the resolver, is an object with supplementary information about execution. Send a PR or open an issue if you need additional information here.
-
isLeaf
: A boolean that istrue
if this resolver is for a leaf field of the query, i.e. one that doesn't have a sub-selection. -
resultKey
: The key the result of this field will be put under. It's either the field name from the query, or the field alias. -
directives
: An object with information about all directives on this field. It's an object of the format{ [directiveName]: { [argumentName]: value }}
. So for example a field with@myDirective(hello: "world")
will be passed as{ myDirective: { hello: 'world' }}
. Note that fields can't have multiple directives with the same name, as written in the GraphQL spec.
Utilities
See http://dev.apollodata.com/react/fragments.html for examples of how you might use these.
import { filter } from 'graphql-anywhere'
filter(doc, data);
-
doc
: a GraphQL document, as generated by the template literal fromgraphql-tag
, typically either a query or a fragment. -
data
: an object of data to be filtered by thedoc
Filter data
according to doc
.
import { check } from 'graphql-anywhere'
check(doc, data);
-
doc
: a GraphQL document, as generated by the template literal fromgraphql-tag
, typically either a query or a fragment. -
data
: an object of data, as may have been filtered bydoc
.
Check that data
is of the form defined by the query or fragment. Throw an exception if not.
import { propType } from 'graphql-anywhere'
X.propTypes = {
foo: propType(doc),
bar: propType(doc).isRequired,
}
-
doc
: a GraphQL document, as generated by the template literal fromgraphql-tag
, typically either a query or a fragment.
Generate a React propType
checking function to ensure that the passed prop is in the right form.
Supported GraphQL features
Why do you even need a library for this? Well, running a GraphQL query isn't as simple as just traversing the AST, since there are some pretty neat features that make the language a bit more complex to execute.
- [x] Arguments
- [x] Variables
- [x] Aliases
- [x] Fragments, both named and inline
- [x]
@skip
and@include
directives
If you come across a GraphQL feature not supported here, please file an issue.
Example: Filter a nested object
import gql from 'graphql-tag';
import graphql from 'graphql-anywhere';
// I don't need all this stuff!
const gitHubAPIResponse = {
"url": "https://api.github.com/repos/octocat/Hello-World/issues/1347",
"title": "Found a bug",
"body": "I'm having a problem with this.",
"user": {
"login": "octocat",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"url": "https://api.github.com/users/octocat",
},
"labels": [
{
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
"name": "bug",
"color": "f29513"
}
],
};
// Write a query that gets just the fields we want
const query = gql`
{
title
user {
login
}
labels {
name
}
}
`;
// Define a resolver that just returns a property
const resolver = (fieldName, root) => root[fieldName];
// Filter the data!
const result = graphql(
resolver,
query,
gitHubAPIResponse
);
assert.deepEqual(result, {
"title": "Found a bug",
"user": {
"login": "octocat",
},
"labels": [
{
"name": "bug",
}
],
});
Example: Generate mock data
// Write a query where the fields are types, but we alias them
const query = gql`
{
author {
name: string
age: int
address {
state: string
}
}
}
`;
// Define a resolver that uses the field name to determine the type
// Note that we get the actual name, not the alias, but the alias
// is used to determine the location in the response
const resolver = (fieldName) => ({
string: 'This is a string',
int: 5,
}[fieldName] || 'continue');
// Generate the object!
const result = graphql(
resolver,
query
);
assert.deepEqual(result, {
author: {
name: 'This is a string',
age: 5,
address: {
state: 'This is a string',
},
},
});
Example: Read from a Redux store generated with Normalizr
const data = {
result: [1, 2],
entities: {
articles: {
1: { id: 1, title: 'Some Article', author: 1 },
2: { id: 2, title: 'Other Article', author: 1 },
},
users: {
1: { id: 1, name: 'Dan' },
},
},
};
const query = gql`
{
result {
title
author {
name
}
}
}
`;
const schema = {
articles: {
author: 'users',
},
};
// This resolver is a bit more complex than others, since it has to
// correctly handle the root object, values by ID, and scalar leafs.
const resolver = (fieldName, rootValue, args, context): any => {
if (!rootValue) {
return context.result.map((id) => assign({}, context.entities.articles[id], {
__typename: 'articles',
}));
}
const typename = rootValue.__typename;
// If this field is a reference according to the schema
if (typename && schema[typename] && schema[typename][fieldName]) {
// Get the target type, and get it from entities by ID
const targetType: string = schema[typename][fieldName];
return assign({}, context.entities[targetType][rootValue[fieldName]], {
__typename: targetType,
});
}
// This field is just a scalar
return rootValue[fieldName];
};
const result = graphql(
resolver,
query,
null,
data // pass data as context since we have to access it all the time
);
// This is the non-normalized data, with only the fields we asked for in our query!
assert.deepEqual(result, {
result: [
{
title: 'Some Article',
author: {
name: 'Dan',
},
},
{
title: 'Other Article',
author: {
name: 'Dan',
},
},
],
});
Example: Generate React components
You can use the resultMapper
option to convert your results into anything you like. In this case, we convert the result fields into children for a React component:
const resolver = (fieldName, root, args) => {
if (fieldName === 'text') {
return args.value;
}
return createElement(fieldName, args);
};
const reactMapper = (childObj, root) => {
const reactChildren = Object.keys(childObj).map(key => childObj[key]);
if (root) {
return cloneElement(root, root.props, ...reactChildren);
}
return reactChildren[0];
};
function gqlToReact(query): any {
return graphql(
resolver,
query,
'',
null,
null,
{ resultMapper: reactMapper },
);
}
const query = gql`
{
div {
s1: span(id: "my-id") {
text(value: "This is text")
}
s2: span
}
}
`;
assert.equal(
renderToStaticMarkup(gqlToReact(query)),
'<div><span id="my-id">This is text</span><span></span></div>'
);