graphql-anywhere
Version:
Run GraphQL queries with no schema and just one resolver
855 lines (632 loc) • 16 kB
text/typescript
import gql from 'graphql-tag';
import { Resolver, ExecInfo } from '..';
const execute = (graphql, r) => () => {
it('does basic things', async () => {
const resolver = (_, root) => r(root + 'fake');
const query = gql`
{
a {
b
...frag
}
}
fragment frag on X {
c
}
`;
const result = await graphql(resolver, query, '', null, null);
expect(result).toEqual({
a: {
b: 'fakefake',
c: 'fakefake',
},
});
});
it('works with enum args', async () => {
const resolver = (fieldName, root, args) => r(args.value);
const query = gql`
{
a(value: ENUM_VALUE)
}
`;
const result = await graphql(resolver, query);
expect(result).toEqual({ a: 'ENUM_VALUE' });
});
it('works with null args', async () => {
const resolver = (fieldName, root, args) => r(args.value);
const query = gql`
{
a(value: null)
}
`;
const result = await graphql(resolver, query);
expect(result).toEqual({ a: null });
});
it('traverses arrays returned from the resolver', async () => {
const resolver = () => r([1, 2]);
const query = gql`
{
a {
b
}
}
`;
const result = await graphql(resolver, query);
expect(result).toEqual({ a: [{ b: [1, 2] }, { b: [1, 2] }] });
});
it('can traverse an object', async () => {
const obj = {
a: {
b: 'fun',
c: ['also fun', 'also fun 2'],
d: 'not fun',
},
};
const resolver = (fieldName, root) => r(root[fieldName]);
const query = gql`
{
a {
b
c
}
}
`;
const result = await graphql(resolver, query, obj, null, null);
expect(result).toEqual({
a: {
b: 'fun',
c: ['also fun', 'also fun 2'],
},
});
});
it('can traverse nested arrays', async () => {
const obj = { a: [{ b: [[{ c: 1 }, { c: 2 }], [{ c: 3 }, { c: 4 }]] }] };
const resolver = (fieldName, root) => r(root[fieldName]);
const query = gql`
{
a {
b {
c
}
}
}
`;
const result = await graphql(resolver, query, obj, null, null);
expect(result).toEqual({
a: [{ b: [[{ c: 1 }, { c: 2 }], [{ c: 3 }, { c: 4 }]] }],
});
});
it('can use arguments, both inline and variables', async () => {
const resolver = (fieldName, _, args) => r(args);
const query = gql`
{
inline(int: 5, float: 3.14, string: "string")
variables(int: $int, float: $float, string: $string)
}
`;
const variables = {
int: 6,
float: 6.28,
string: 'varString',
};
const result = await graphql(resolver, query, null, null, variables);
expect(result).toEqual({
inline: {
int: 5,
float: 3.14,
string: 'string',
},
variables: {
int: 6,
float: 6.28,
string: 'varString',
},
});
});
it('will tolerate missing variables', async () => {
const resolver = (fieldName, _, args) => r(args);
const query = gql`
{
variables(int: $int, float: $float, string: $string, missing: $missing)
}
`;
const variables = {
int: 6,
float: 6.28,
string: 'varString',
};
const result = await graphql(resolver, query, null, null, variables);
expect(result).toEqual({
variables: {
int: 6,
float: 6.28,
string: 'varString',
missing: undefined,
},
});
});
it('can use skip and include', async () => {
const resolver = fieldName => r(fieldName);
const query = gql`
{
a {
b @skip(if: true)
c @include(if: true)
d @skip(if: false)
e @include(if: false)
}
}
`;
const result = await graphql(resolver, query, null, null);
expect(result).toEqual({
a: {
c: 'c',
d: 'd',
},
});
});
it('can use inline and named fragments', async () => {
const resolver = fieldName => r(fieldName);
const query = gql`
{
a {
... on Type {
b
c
}
...deFrag
}
}
fragment deFrag on Type {
d
e
}
`;
const result = await graphql(resolver, query, null, null, null);
expect(result).toEqual({
a: {
b: 'b',
c: 'c',
d: 'd',
e: 'e',
},
});
});
it('can resolve deeply nested fragments', async () => {
const resolver = (fieldName, root) => {
return r(root[fieldName]);
};
const query = gql`
{
stringField
numberField
nullField
... on Item {
nestedObj {
stringField
nullField
deepNestedObj {
stringField
nullField
}
}
}
... on Item {
nestedObj {
numberField
nullField
deepNestedObj {
numberField
nullField
}
}
}
... on Item {
nullObject
}
nestedObj {
inlinedObjectStringField
}
}
`;
const result: any = {
id: 'abcd',
stringField: 'This is a string!',
numberField: 5,
nullField: null,
nestedObj: {
id: 'abcde',
stringField: 'This is a string too!',
numberField: 6,
nullField: null,
deepNestedObj: {
stringField: 'This is a deep string',
numberField: 7,
nullField: null,
},
inlinedObjectStringField: 'This is a string of an inlined object',
},
nullObject: null,
};
const queryResult = await graphql(resolver, query, result);
// The result of the query shouldn't contain __data_id fields
expect(queryResult).toEqual({
stringField: 'This is a string!',
numberField: 5,
nullField: null,
nestedObj: {
stringField: 'This is a string too!',
numberField: 6,
nullField: null,
deepNestedObj: {
stringField: 'This is a deep string',
numberField: 7,
nullField: null,
},
inlinedObjectStringField: 'This is a string of an inlined object',
},
nullObject: null,
});
});
it('can resolve deeply nested fragments with arrays', async () => {
const resolver = (fieldName, root) => {
return r(root[fieldName]);
};
const query = gql`
{
... on Item {
array {
id
field1
}
}
... on Item {
array {
id
field2
}
}
... on Item {
array {
id
field3
}
}
}
`;
const result: any = {
array: [
{
id: 'abcde',
field1: 1,
field2: 2,
field3: 3,
},
],
};
const queryResult = await graphql(resolver, query, result);
expect(queryResult).toEqual({
array: [
{
id: 'abcde',
field1: 1,
field2: 2,
field3: 3,
},
],
});
});
it('passes info including isLeaf, resultKey, directives, and field', async () => {
const leafMap: { [s: string]: ExecInfo } = {};
const resolver: Resolver = (fieldName, root, args, context, info) => {
leafMap[fieldName] = info;
return r('continue');
};
const query = gql`
{
alias: a {
b
hasDirective @skip(if: false) @otherDirective(arg: $x)
}
}
`;
await graphql(resolver, query, null, null, { x: 'argument' });
expect(leafMap).toEqual({
a: {
directives: null,
isLeaf: false,
resultKey: 'alias',
field: expect.any(Object),
},
b: {
directives: null,
isLeaf: true,
resultKey: 'b',
field: expect.any(Object),
},
hasDirective: {
directives: {
skip: { if: false },
otherDirective: { arg: 'argument' },
},
isLeaf: true,
resultKey: 'hasDirective',
field: expect.any(Object),
},
});
});
it('can filter GraphQL results', async () => {
const data = {
alias: 'Bob',
name: 'Wrong',
height: 1.89,
avatar: {
square: 'abc',
circle: 'def',
triangle: 'qwe',
},
};
const fragment = gql`
fragment PersonDetails on Person {
alias: name
height(unit: METERS)
avatar {
square
... on Avatar {
circle
}
}
}
`;
const resolver: Resolver = (fieldName, root, args, context, info) => {
return r(root[info.resultKey]);
};
const filtered = await graphql(resolver, fragment, data);
expect(filtered).toEqual({
alias: 'Bob',
height: 1.89,
avatar: {
square: 'abc',
circle: 'def',
},
});
});
it('can handle mutations', async () => {
const resolver = (fieldName, root, args) => {
let value;
if (fieldName === 'operateOnNumbers') {
value = args;
} else if (fieldName === 'add') {
value = root.a + root.b;
} else if (fieldName === 'subtract') {
value = root.a - root.b;
} else if (fieldName === 'multiply') {
value = root.a * root.b;
} else if (fieldName === 'divide') {
value = root.a / root.b;
}
return r(value);
};
const query = gql`
mutation {
operateOnNumbers(a: 10, b: 2) {
add
subtract
multiply
divide
}
}
`;
const result = await graphql(resolver, query, '', null, null);
expect(result).toEqual({
operateOnNumbers: {
add: 12,
subtract: 8,
multiply: 20,
divide: 5,
},
});
});
it('does not error on subscriptions', async () => {
const data = {
user: {
id: 1,
name: 'Some User',
height: 1.89,
},
};
const resolver = (fieldName, root) => r(root[fieldName]);
const query = gql`
subscription {
user {
id
name
height
}
}
`;
const result = await graphql(resolver, query, data);
expect(result).toEqual({
user: {
id: 1,
name: 'Some User',
height: 1.89,
},
});
});
it('can handle documents with multiple fragments', async () => {
const data = {
user: {
id: 1,
name: 'Some User',
height: 1.89,
},
};
const resolver = (fieldName, root) => r(root[fieldName]);
const query = gql`
fragment A on User {
name
}
fragment B on User {
height
}
query {
user {
id
...A
...B
}
}
`;
const result = await graphql(resolver, query, data);
expect(result).toEqual({
user: {
id: 1,
name: 'Some User',
height: 1.89,
},
});
});
describe('examples', () => {
it('readme example', async () => {
// 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 = await graphql(resolver, query, gitHubAPIResponse);
expect(result).toEqual({
title: 'Found a bug',
user: { login: 'octocat' },
labels: [{ name: 'bug' }],
});
});
it('readme example 2', async () => {
// 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 = await graphql(resolver, query);
expect(result).toEqual({
author: {
name: 'This is a string',
age: 5,
address: { state: 'This is a string' },
},
});
});
it('read from Redux normalized store', async () => {
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 => {
return {
...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 {
...context.entities[targetType][rootValue[fieldName]],
__typename: targetType,
};
}
// This field is just a scalar
return rootValue[fieldName];
};
const result = await 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!
expect(result).toEqual({
result: [
{
title: 'Some Article',
author: { name: 'Dan' },
},
{
title: 'Other Article',
author: { name: 'Dan' },
},
],
});
});
});
};
describe('basic operations done sync', execute(require('../').default, x => x));
describe(
'basic operations done async',
execute(require('../async').graphql, x => Promise.resolve(x)),
);