Using Paper with graphql-mocks
GraphQL Paper can be used on its own but has been designed and tested to integrate with graphql-mocks
.
For more features specific to GraphQL Paper and its capabilities check out the GraphQL Paper Documentation.
Installation
# npm
npm install --save-dev graphql-paper graphql
# yarn
yarn add --dev graphql-paper graphql
# pnpm
pnpm add --save-dev graphql-paper graphql
Setup
The only setup after installing the graphql-paper
package is to import it, create a new instance and add it to the GraphQL Handler's dependencies.
import { Paper } from 'graphql-paper';
import { GraphQLHandler } from 'graphql-mocks';
import graphqlSchema from './schema';
const paper = new Paper(graphqlSchema);
const handler = new GraphQLHandler({ dependencies: { graphqlSchema, paper }})
Using Paper
within Resolver Functions
Within a resolver function the paper
dependency can be extracted using extractDependencies
.
import { extractDependencies } from 'graphql-mocks/resolver';
function resolver(parent, args, context, info) {
const paper = extractDependencies(context, ['paper']);
// do something with the paper store...
// see below for query and mutation examples
}
Querying Data
Only top-level Query resolvers need to be specified for the GraphQL Paper Document. The rercursive data structure of Paper Documents from the store will follow Connections to other Paper Documents, automatically resolving the fields backed by Concrete Data. In the case of Derived Data there are some examples and patterns to follow below.
import { Paper } from "graphql-paper";
import { GraphQLHandler } from "graphql-mocks";
import { extractDependencies } from "graphql-mocks/resolver";
const graphqlSchema = `
schema {
query: Query
}
type Query {
films: [Film!]!
}
type Film {
title: String!
year: String!
actors: [Actor!]!
}
type Actor {
name: String!
}
`;
async function run() {
const paper = new Paper(graphqlSchema);
// seed with some data about the film "The Notebook"
await paper.mutate(({ create }) => {
const rachel = create("Actor", {
name: "Rachel McAdams",
});
const ryan = create("Actor", {
name: "Ryan Gosling",
});
create("Film", {
title: "The Notebook",
year: "2004",
actors: [rachel, ryan],
});
});
const resolverMap = {
Query: {
films(root, args, context, info) {
const { paper } = extractDependencies(context, ["paper"]);
// return all Documents of type `Film`
return paper.data.Film;
},
},
};
const handler = new GraphQLHandler({
resolverMap,
dependencies: { graphqlSchema, paper },
});
const result = await handler.query(`
query {
films {
title
year
actors {
name
}
}
}
`);
console.log(result);
}
// kick everything off!
run();
{ "data": { "films": [ { "title": "The Notebook", "year": "2004", "actors": [ { "name": "Rachel McAdams" }, { "name": "Ryan Gosling" } ] } ] } }
Mutating Data
Similar to Querying Data only the top-level Mutation resolvers need to be defined returning necessary documents and nested Concrete Data will be automatically resolved.
import { Paper } from "graphql-paper";
import { GraphQLHandler } from "graphql-mocks";
import { extractDependencies } from "graphql-mocks/resolver";
import { v4 as uuid } from "uuid";
const graphqlSchema = `
schema {
query: Query
mutation: Mutation
}
type Query {
noop: Boolean
}
type Mutation {
addFilm(input: AddFilmInput): Film!
}
type Film {
id: ID!
title: String!
year: String!
actors: [Actor!]!
}
type Actor {
id: ID!
name: String!
}
input AddFilmInput {
title: String!
year: String!
actorIds: [ID!]
}
`;
async function run() {
const paper = new Paper(graphqlSchema);
const { tomHanks, wilson } = await paper.mutate(({ create }) => {
const tomHanks = create("Actor", {
id: uuid(),
name: "Tom Hanks",
});
const wilson = create("Actor", {
id: uuid(),
name: "Wilson the Volleyball",
});
return { tomHanks, wilson };
});
const resolverMap = {
Mutation: {
addFilm(root, args, context, info) {
const { paper } = extractDependencies(context, ["paper"]);
// find Actor documents based on args.input.actorIds
const filmActors = (args.input.actorIds ?? [])
.map((actorId) =>
paper.data.Actor.find((actor) => actor.id === actorId)
)
.filter(Boolean);
// return created Film document, matching `addFilm` return type: Film!
const newFilm = paper.mutate(({ create }) => {
return create("Film", {
id: uuid(),
title: args.input.title,
year: args.input.year,
actors: filmActors,
});
});
return newFilm;
},
},
};
const handler = new GraphQLHandler({
resolverMap,
dependencies: { graphqlSchema, paper },
});
const result = await handler.query(
`
mutation($addFilmInput: AddFilmInput) {
addFilm(input: $addFilmInput) {
title
year
actors {
name
}
}
}
`,
{
addFilmInput: {
title: "Cast Away",
year: "2000",
actorIds: [tomHanks.id, wilson.id],
},
}
);
console.log(result);
}
// kick everything off!
run();
{ "data": { "addFilm": { "title": "Cast Away", "year": "2000", "actors": [ { "name": "Tom Hanks" }, { "name": "Wilson the Volleyball" } ] } } }
Separation of Concrete and Derived Data
One important thing to consider when using GraphQL Paper with graphql-mocks
is how data should be modeled. Most of the time the data is dealt with as Concrete Data and should be stored in the GraphQL Paper store. In other cases it's Derived Data and should be handled by graphql-mocks
and its tools. These definitions and examples are expanded on below.
Concrete Data
Concrete data usually represents a distinct entity, might have an ID and defined property values. In the example of GraphQL Paper concrete data is represented by a Paper Document and its properties. Not all GraphQL Types or fields on GraphQL Types should be reflected by concrete data, which is covered in the Derived Data sections below.
An example of concrete data could be a Film
which is unique and represents a singular entity.
type Film {
title: String!
}
represented concretely by a Paper Document:
{
title: 'The Notebook'
}
This is an example of Concrete Data represented by a GraphQL Paper Document where its properties mirror the GraphQL Types and its fields which allows for the data to resolve automatically, and recursively through connections and their fields.
Derived Data
Derived Data represents data that does not stand on its own and should be handled by graphql-mocks
in helping resolve a query with supporting data from the GraphQL Paper store. There are two main types of Derived Data when dealing and resolving GraphQL queries.
Derived Types
Derived types are types whose definition is derived by the concrete data it contains but on its own is not reflected in the store. The container type often acts a logical grouping.
In this example FilmSearchResults
represents a container containing the actual results of films and the count. This type should not have any documents stored in GraphQL Paper but can still be resolved by a Resolver or Resolver Wrapper.
type FilmSearchResults {
results: [Film!]!
count: Int!
}
Using Resolvers
A Resolver function can be used to resolve the correct shape of the Derived Data. In the case a Resolver function already exists then using a Resolver Wrapper is appropriate (see below).
This example uses the FilmSearchResults
type from above and assumes that we have a top-level query field searchFilms
:
type Query {
searchFilm(query: String!): FilmSearchResults!
}
The searchFilm
resolver function could look like:
const searchFilmResolver = (root, args, context, info) => {
const { paper } = extractDependencies(context, ["paper"]);
const films = paper.data.Film.filter((film) => film.title.includes(args.query));
// return the required shape of `FilmSearchResults`
return {
results: films,
count: films.length,
};
}
This resolver can be applied to the initial resolverMap
passed into the GraphQL Handler or applied via embed
.
Using Resolver Wrappers
In some special cases the data might already be represented by a field with a resolver that resolves the correct data but not in the supporting shape of the Derived Type. In this case a Resolver Wrapper can be used to retrieve the data of the original resolver and return a modified form. This has the added benefit of decoupling the resolver sourcing the data from the wrapper that transforms it to the shape expected, making the wrapper re-usable also in cases where this transform might be needed again.
In this example the films
property on an Actor
might already be returning the films
property but not in the shape of FilmSearchResults
.
type Actor {
films(query: String!): FilmSearchResults!
}
Assuming the original resolver returns an array of Film
documents we could use this wrapper:
import { createWrapper, WrapperFor } from 'graphql-mocks/resolver';
const wrapper = createWrapper('FilmSearchResults', WrapperFor.FIELD, function(originalResolver, wrapperOptions) {
return function filmSearchResultsWrapper(parent, args, context, info) {
const films = await originalResolver(parent, args, context, info);
return {
results: films,
count: films.length
};
};
});
Derived Fields
In other cases derived fields could be something that derives its value from other fields or when filtering the existing data based on the arguments.
In the case that the derived data is filtered or refined based on arguments, and the data exists by the resolver already, it's best to use a resolver wrapper. See Automatic Resolver Filtering with Wrappers for ideas and examples.
This is an example of a value being derived from other fields. We wouldn't want to store the speed
on the Paper Document since it can be determined from miles
and timeInHours
. The speed
data itself would would be best represented by a resolver.
type Trip {
miles: Float!
timeInHours: Float!
# miles per hour (miles / timeInHours)
speed: Float!
}
The resolver for the speed
field would look like:
function tripSpeedResolver(parent, args, context, info) {
const { miles, timeInHours } = parent;
return miles / timeInHours;
}
This resolver can be applied to the initial resolverMap
passed into the GraphQL Handler or applied via embed
.