gramod

Modularize graphql app with ease

Usage no npm install needed!

<script type="module">
  import gramod from 'https://cdn.skypack.dev/gramod';
</script>

README

GRAMOD

Modularize graphql app with ease

Installation

$ npm i gramod --save
# or
$ yarn add gramod

Features

  • Modularizing supported
  • Middlewares supported
  • Overriding resolver supported
  • Reusing resolvers for REST API

Getting started

Let's say we have 2 modules: user, post

const typeDefs = `
    type Query {
        users: [User]
        user(id: ID!): User
        post(id: ID!): Post
    }

    type User {
        id: ID!
        name: String
        posts: [Post]
    }

    type Post {
        id: ID!
        title: String
        body: String
    }
`;

const resolvers = {
  Query: {
    users: () => {},
    user: () => {},
    post: () => {},
  },
  User: {
    posts: () => {},
  },
};

We can modularize our app like this with gramod

/modules/main.js

const user = require("./user");

module.exports = {
  require: [user],
};

/modules/user.js

const post = require("./post");

module.exports = {
  require: [post],
  types: {
    Query: `
        users: [User]
        user(id: ID!): User
        `,
    User: `
        id: ID!
        name: String
        posts: [Post]
        `,
  },
  resolvers: {
    Query: {
      users: () => {},
      user: () => {},
    },
    User: {
      posts: () => {},
    },
  },
};

/modules/post.js

module.exports = {
  types: {
    Query: `
        post(id: ID!): Post
        `,
    Post: `
        id: ID!
        title: String
        body: String
        `,
  },
  resolvers: {
    Query: {
      post: () => {},
    },
  },
};

Finally, we use gramod to generate typeDefs and resolvers for GraphQL server

const main = require("./modules/main");
const { typeDefs, resolvers } = gramod(main);
const server = new ApolloServer({ typeDefs, resolvers });

Defining extensible module

In this example, the user module has no posts property like previous one.

/modules/user.js

module.exports = {
  types: {
    Query: `
        users: [User]
        user(id: ID!): User
        `,
    User: `
        id: ID!
        name: String
        `,
  },
  resolvers: {
    Query: {
      users: () => {},
      user: () => {},
    },
  },
};

And the post module looks like this:

/modules/post.js

const user = require("./user");

module.exports = {
  require: user,
  types: {
    Query: `
        post(id: ID!): Post
        `,
    Post: `
        id: ID!
        title: String
        body: String
        `,
    // the posts property will be appended to User type declaration
    User: `
        posts: [Post]
        `,
  },
  resolvers: {
    Query: {
      post: () => {},
    },
    User: {
      // this resolver will bed injected to User type of user module
      posts: () => {},
    },
  },
};

/modules/main.js

module.exports = {
  require: [user], // user module has no post extension
  require: [user, post], // user module includes post extension
};

App Level Middleware

const logInput = async (resolve, root, args, context, info) => {
  console.log(`1. logInput: ${JSON.stringify(args)}`);
  const result = await resolve(root, args, context, info);
  console.log(`3. logInput`);
  return result;
};
const helloModule = {
  resolvers: {
    Query: {
      hello: (root, args, context, info) => {
        console.log(`2. resolver: hello`);
        return `Hello ${args.name ? args.name : "world"}!`;
      },
    },
  },
};

const { resolvers, typeDefs } = gramod(helloModule, logInput);

Module Level Middleware

const logInput = async (resolve, root, args, context, info) => {
  console.log(`1. logInput: ${JSON.stringify(args)}`);
  const result = await resolve(root, args, context, info);
  console.log(`3. logInput`);
  return result;
};
const helloModule = {
  middlewares: logInput,
  resolvers: {
    Query: {
      hello: (root, args, context, info) => {
        console.log(`2. resolver: hello`);
        return `Hello ${args.name ? args.name : "world"}!`;
      },
    },
  },
};

const { resolvers, typeDefs } = gramod(helloModule);

Overriding Resolver

const helloModule = {
  resolvers: {
    Query: {
      hello: () => "Hello World !!!",
    },
  },
};
const wrapHelloModule = {
  resolvers: {
    Query: {
      hello: () => (context) => {
        const result = context.next();
        return `*** ${result} ***`;
      },
    },
  },
};

gramod([helloModule, wrapHelloModule]);

Using resolver with REST API

const helloModule = {
  resolvers: {
    Query: {
      hello: (parent, { name }) => `Hello ${name}`,
    },
    Mutation: {
      createPost: (parent, post) => {},
    },
  },
};

const { query, mutate } = gramod(helloModule);

app.get("/hello", (req, res) =>
  res.send(query("hello", { name: req.query.name }))
);

app.post("/posts", (req, res) => {
  mutate("createPost", req.body);
  res.send("OK");
});

App Level Context Factory

const helloModule = {
  resolvers: {
    Query: {
      hello: (parent, { name }, context) =>
        `Hello ${name || context.defaultName}`,
    },
  },
};
const { typeDefs, resolvers, context } = gramod(helloModule, {
  context: () => ({ defaultName: "World" }),
});
const server = new ApolloServer({ typeDefs, resolvers, context });

Module Level Context Factory

const helloModule = {
  context: () => ({ defaultName: 'World' })
  resolvers: {
    Query: {
      hello: (parent, { name }, context) =>
        `Hello ${name || context.defaultName}`,
    },
  },
};
const { typeDefs, resolvers, context } = gramod(helloModule);
const server = new ApolloServer({ typeDefs, resolvers, context });