Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the best practice for modularizing GraphQL schema? #750

Closed
helvenk opened this issue Apr 24, 2018 · 8 comments
Closed

What is the best practice for modularizing GraphQL schema? #750

helvenk opened this issue Apr 24, 2018 · 8 comments
Labels

Comments

@helvenk
Copy link

helvenk commented Apr 24, 2018

These days I am looking for the best practice to split schemas into files. My thought is:

  • one file one schema
  • each schema do its own things

@stubailo told me a new, simpler approach here: https://dev-blog.apollodata.com/modularizing-your-graphql-schema-code-d7f71d5ed5f2, which uses the type extension syntax:

type Query {
  _empty: String
}

extend type Query {
  books: [Book]
}

But I have another way to discuss here, which is more simpler and semantic.

Convention

I assume each schema file can export 3 properties(not necessary): schemas, resolvers, directives:

// author.js

// both Array and String are acceptable
export const schemas = [`
  directive @auth(
    requires: AuthRole = ADMIN
  ) on FIELD_DEFINITION | OBJECT

  type Author {
    name: String
    age: Int
  }

  type Query {
    author(name: String!): Author @auth(requires: USER)
    authors: [Author]
  }
`];

export const resolvers = {
  Query: {
    author(source, args, ctx) {
    }
  },
  Author: {
  }
};

export const directives = {
  auth: AuthDirective
};

Here I put the directive schema and the type schema together for simplicity. In production, I suggest to split directive schema, type schema and scalar schema into multiple files.

Merge

The schema definitions above are pretty simple and semantic:A schema has its type definition or directive declaring and resolvers in its own schema scope. Maybe the schema also has dependencies, but are not in its scope.

Now we need a way to merge those schemas, resolvers and directives.

I would like to write a function to load the files together:

const typeDefs = [];
const resolverMap = {};
const directiveMap = {};

const files = loadAllFiles('/path/to/app/graphql');
files.forEach(file => {
   const { schemas, resolvers, directives } = require(file);
   if (!!schemas) {
      typeDefs.push(schemas);
    }
    if (!!resolvers) {
      merge(resolverMap, resolvers);
    }
    if (!!directives) {
      merge(directiveMap, directives);
    }
 });

Then maybe we could call the api makeExecutableSchema to get a root schema with resolvers, but it will throw error shows "type Query defined more than once".

That is because makeExecutableSchema will not merge the multiply defined schemas by the same type name.

Here I hope the this feature would be implemented later. I have opened an issue here #708 for this feature.

Finally I find a npm module merge-graphql-schemas which could smartly merge the multiply defined schemas:

import { mergeTypes } from 'merge-graphql-schemas';

makeExecutableSchema({
  typeDefs: mergeTypes(flatten(typeDefs)),
  resolvers: resolverMap,
  schemaDirectives: directiveMap
});

We call flatten here in case the item of typeDefs is an Array.

Now, we've got an Merged Root Schema.

@ChopperLee2011
Copy link

ChopperLee2011 commented Jul 10, 2018

I am also thinking about this topic recently, what bothers me more is remote schemas. makeExecutableSchema seems not work cause it needs to separate typeDefs and resolvers, so I change to use mergeSchemas, but can not handle errors very well, something subquery fail will return a nested error, and something subquery fails will return a root error.

@b4dnewz
Copy link

b4dnewz commented Jul 31, 2018

Recently I was trying graphql and before I discovered other packages, I made a similar thing to load schemas from files, here the code with an example.

The interface that define the one-file graphql exports

interface GraphQLExport {
  inputs?: String
  types?: String
  queries?: String
  mutations?: String
  subscriptions?: String
  resolvers?: Object
}

An example Message.js schema export

const types = `
  type Message {
    id: ID
    chat: ID!
    message: String!
    author: User!
    timestamp: String!
  }
`

const queries = `
  message(id: ID!): Message
  messages(chat: ID): [Message]
`

const mutations = `
  addMessage(message: MessageInput!): Message!
  removeMessage(id: ID!): Message!
`

const subscriptions = `
  onMessage(chat: ID!): Message
`

const resolvers = {
  Query: {
    message: async (_, { id }, { Models }) => {},
    messages: async (_, args, { Models }) => {}
  }
}

export {
  types,
  queries,
  mutations,
  subscriptions,
  resolvers
}

The schema build file, which takes all the partials and merge them in a single schema

// Import schemas
import * as Message from './Message'

const partials: Array<GraphQLExport> = [
  Message
]

const inputs = []
const types = []
const queries = []
const mutations = []
const subscriptions = []

const merge = (target, source) => {
  for (let key of Object.keys(source)) {
    if (source[key] instanceof Object && target[key]) {
      Object.assign(source[key], merge(target[key], source[key]))
    }
  }
  Object.assign(target || {}, source)
  return target
}

// Concat all partial strings
partials.forEach(p => {
  p.inputs && inputs.push(p.inputs)
  p.types && types.push(p.types)
  p.queries && queries.push(p.queries)
  p.mutations && mutations.push(p.mutations)
  p.subscriptions && subscriptions.push(p.subscriptions)
})

const typeDefs = `
  ${inputs.join('\n')}
  ${types.join('\n')}

  ${queries.length ? `
    type Query {
      ${queries.join('\n')}
    }` : ''
  }

  type Mutation {
    ${mutations.join('\n')}
  }

  ${subscriptions.length ? `
    type Subscription {
      ${subscriptions.join('\n')}
    }` : ''
  }
`

const resolvers = partials.reduce((obj, p) => {
  return merge(obj, p.resolvers || {})
}, {
  Query: {},
  Mutation: {},
  Subscription: {}
})

export default {
  typeDefs,
  resolvers
}

I wrote it very quickly, I know is not elegant and probably does not handle all cases, but it worked for what I needed.

I'm thinking about switching to a dedicated module such as merge-graphql-schemas but the idea was to define a interface for the schema exports than put all the logic in the file (queries, resolvers, subscriptions, ...) the difference is in case of Query, Mutation, Subscription you just have to export the strings without the full declaration:

So this Query

type Query {
  message(id: ID!): Message
  messages(chat: ID): [Message]
}

in the export file becomes:

const queries = `
  message(id: ID!): Message
  messages(chat: ID): [Message]
`

Any feedback about it?

@hajocava
Copy link

Full Example Graphql, Mongo, Express, Apollo-Server
https://github.com/hajocava/Graphql-Template

@diogobsb81
Copy link

@hajocava excellent work!

@GermaVinsmoke
Copy link

Full Example Graphql, Mongo, Express, Apollo-Server
https://github.com/hajocava/Graphql-Template

Link is not working.

@carloschneider
Copy link

@GermaVinsmoke https://github.com/hajocava/Graphql-Template/

@yaacovCR
Copy link
Collaborator

yaacovCR commented Apr 1, 2020

Closing, see resources above.

@yaacovCR yaacovCR closed this as completed Apr 1, 2020
@tunchamroeun
Copy link

tunchamroeun commented Sep 22, 2020

Here is the example
https://github.com/tunchamroeun/graphql-merge.git

Screenshot

Screenshot

Code example in server file

const express = require('express');
const glob = require("glob");
const {graphqlHTTP} = require('express-graphql');
const {makeExecutableSchema, mergeResolvers, mergeTypeDefs} = require('graphql-tools');
const app = express();
//iterate through resolvers file in the folder "graphql/folder/folder/whatever*-resolver.js"
let resolvers = glob.sync('graphql/*/*/*-resolver.js')
let registerResolvers = [];
for (const resolver of resolvers){
// add resolvers to array
    registerResolvers = [...registerResolvers, require('./'+resolver),]
}
//iterate through resolvers file in the folder "graphql/folder/folder/whatever*-type.js"
let types = glob.sync('graphql/*/*/*-type.js')
let registerTypes = [];
for (const type of types){
// add types to array
    registerTypes = [...registerTypes, require('./'+type),]
}
//make schema from typeDefs and Resolvers with "graphql-tool package (makeExecutableSchema)"
const schema = makeExecutableSchema({
    typeDefs: mergeTypeDefs(registerTypes),//merge array types
    resolvers: mergeResolvers(registerResolvers,)//merge resolver type
})
// mongodb connection if you prefer mongodb
require('./helpers/connection');
// end mongodb connection
//Make it work with express "express and express-graphql packages"
app.use('/graphql', graphqlHTTP({
    schema: schema,
    graphiql: true,//test your query or mutation on browser (Development Only)
}));
app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

10 participants