@jpwilliams/graphql-modular-loader
v0.1.0
Published
Load your GraphQL schemas, resolvers, and more from individual, organised files.
Downloads
7
Maintainers
Readme
@jpwilliams/graphql-modular-loader
Organise and load all of your GraphQL types
, resolvers
, loaders
, and middleware
with a single, easy function. This is an opinionated loader which supports only a specific export structure. Like anything opinionated, it's good for standardising, but it's not for everybody.
npm install --save @jpwilliams/graphql-modular-loader
// Load the './types' folder
const { loader } = require('@jpwilliams/graphql-modular-loader')
const { typeDefs, resolvers, getContextFns } = loader('./types')
// Use typeDefs and resolvers for your GraphQL server
const schema = makeExecutableSchema({
typeDefs,
resolvers
})
// getContextFns can be used to populate your context
// object with loaders and middleware specified throughout
// your types
// ...
const context = {
userId: 123,
database: MyDbConnection
}
Object.assign(context, getContextFns(context))
// ...
An example folder with your entire GraphQL set-up:
How it works
The package will load and parse folders and files as an object, meaning schemas and resolvers can be specified either as individual files, or as a single file with multiple exports. We'll show a valid single file compared to a valid directory structure later, but for now let's look at how things might be laid out in our tree.
Anything that exports multiple values can be loaded as a folder instead. For example:
// foo.js
module.exports.bar = 'bar'
module.exports.baz = 'baz
Is exactly the same as:
// foo/bar.js
module.exports = 'bar'
// foo/baz.js
module.exports = 'baz'
So how does the library expect you to lay out your schemas and resolvers? In their most separated form:
schema.graphql
Exports a GraphQL schema for the type you're creating.resolvers/
Contains any field resolvers for this type.author.js
Exports a function that receives(obj, args, context, info)
and returns the value for anauthor
field.
Query/
Contains any queries related to this type.books/
Contains the schema and resolver for thebooks
query.schema.graphql
Exports a GraphQL schema for thebooks
query.resolver.js
Exports a function that receives(obj, args, context, info)
and handles abooks
query.
Mutation/
Contains any mutations related to this type.addBook/
Contains the schema and resolver for theaddBook
mutation.schema.graphql
Exports a GraphQL schema for theaddBook
mutation.resolver.js
Exports a function that receives(obj, args, context, info)
and handles anaddBook
mutation.
Subscription/
Contains any subscriptions related to this type.bookAdded/
Contains the schema and resolver for thebookAdded
subscription.schema.graphql
Exports a GraphQL schema for thebookAdded
subscription.resolver.js
Exports a function that receives(obj, args, context, info)
and handles abookAdded
subscription.
loaders/
Contains any loaders related to this type. A specific export is recommended here so that loaders can be accessed from thecontext
object of any resolver.bookByName.js
Adds a loader namedbookByName
. Export a function which receives acontext
object and passes back aDataLoader
instance. The use of a wrapping function here allows loaders to usecontext
, but also means you can combatdataloader
's caching trap by returning a new loader on each run.
middleware/
Contains any middleware related to this type. A specific export is recommended here so that middleware can be accessed from thecontext
object of any resolver.hasAccessToBook.js
Adds a piece of middleware namedhasAccessToBook
. Export a function which receives acontext
object and passes back a function to run when called.
Using loaders and middleware with context
Regarding loaders and middleware, here's an example of what a loader called bookByName.js
would ideally look like:
// bookByName.js
const DataLoader = require('dataloader')
module.exports = (context) => new DataLoader(async (bookNames) => {
const books = await context.db.pseudoGetBooks(bookNames)
const bookMap = books.reduce((map, book) => {
map[book.name] = book
return map
}, {})
return bookNames.map(bookName => bookMap[bookName])
})
And here might be our middleware, hasAccessToBook.js
:
// hasAccessToBook.js
module.exports = (context) => async (bookId) => {
if (!context.user.isReader) {
throw new Error('Must be a reader to see that book!')
}
}
When loading types, getContextFns
function is exported too. This takes a context object and loads all loaders and middleware using that context. Along with that and using something like apollographql/apollo-server, we can add this (and any other loaders/middleware with the same format) to the context
object for every resolver like so:
const { loader } = require('@jpwilliams/graphql-modular-loader')
const { ApolloServer } = require('apollo-server')
const { typeDefs, resolvers, getContextFns } = loader('./types')
const server = new ApolloServer({
typeDefs,
resolvers
context: async ({ req }) => {
// set up some basic context here.
// maybe set up DB connections or get user data from the req.
const context = {
foo: 'bar',
baz: true,
dbConnection: '...'
}
Object.assign(context, getContextFns(context))
return context
}
})
Now, a resolver could access any loaders or middleware from our context
object!
module.exports = async ({ bookName }, args, context, info) => {
await context.middleware.hasAccessToBook(bookName)
return context.loaders.bookByName(bookName)
}
Splitting files
So all of this means we can now split up complex types in to nicely separated, bookmarked code, allowing really easy extensibility.
// types/Book/schema.graphql
type Book {
title: String
author: Author
}
// types/Book/Query/books/schema.graphql
extend type Query {
books: [Book]
}
// types/Book/Query/books/resolver.js
module.exports = (obj, args, context, info) => [{
title: 'Jurassic Park',
author: {name: 'Michael Crichton'}
}]
// types/Book/Mutation/addBook/schema.graphql
extend type Mutation {
addBook(input: AddBookInput!): AddBookOutput
}
input AddBookInput {
title: String!
author: String!
}
type AddBookOutput {
book: Book
}
// types/Book/Mutation/addBook/resolver.js
module.exports = (obj, args, context, info) => psuedoAddBook(input.title, input.author)
// types/Book/Subscription/bookAdded/schema.graphql
extend type Subscription {
bookAdded: BookAddedPayload
}
type BookAddedPayload {
book: Book
}
// types/Book/Subscription/bookAdded/resolver.js
module.exports = (obj, args, context, info) => psuedoAsyncIterator('bookAdded')
// types/Book/resolvers/author.js
module.exports = (obj, args, context, info) => psuedoGetAuthorData()
You could also define this entire type in a single file. It'd work just fine with the package, but could get pretty bloated the more you add to it! This is best for very simple types like imported scalars.
// types/Book.js
const schema = `
type Book {
title: String
author: Author
}
`
const Query = {
books: {
schema: `extend type Query {
books: [Book]
}`,
resolver: (obj, args, context, info) => [{
title: 'Jurassic Park',
author: {name: 'Michael Crichton'}
}]
}
}
const Mutation = {
addBook: {
schema: `extend type Mutation {
addBook(input: AddBookInput!): AddBookOutput
}
input AddBookInput {
title: String!
author: String!
}
type AddBookOutput {
book: Book
}`,
resolver: (obj, args, context, info) => psuedoAddBook(input.title, input.author)
}
}
const Subscription = {
bookAdded: {
schema: `extend type Subscription {
bookAdded: BookAddedPayload
}
type BookAddedPayload {
book: Book
}`,
resolver: (obj, args, context, info) => psuedoAsyncIterator('bookAdded')
}
}
const resolvers = {
author: (obj, args, context, info) => psuedoGetAuthorData()
}
const loaders = {
bookByName: () => new PsuedoDataLoader()
}
module.exports = {
schema,
Query,
Mutation,
Subscription,
resolvers,
loaders
}