node package manager
Easy collaboration. Discover, share, and reuse code in your team. Create a free org »

@sheerun/graphql-anywhere

graphql-anywhere

npm version Build Status

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
  • document: A GraphQL document, as generated by the template literal from graphql-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
  • fragmentMatcher: Decide whether to execute a fragment. Default is to always execute all fragments.
    • Signature is: (rootValue, typeCondition, context) => boolean

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 is true 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 from graphql-tag, typically either a query or a fragment.
  • data: an object of data to be filtered by the doc

Filter data according to doc.

import { check } from 'graphql-anywhere'
 
check(doc, data);
  • doc: a GraphQL document, as generated by the template literal from graphql-tag, typically either a query or a fragment.
  • data: an object of data, as may have been filtered by doc.

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 from graphql-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.

  • Arguments
  • Variables
  • Aliases
  • Fragments, both named and inline
  • @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>'
);