Command-line utility to build production-ready servers with GraphQL.
JavaScript
Latest commit 488da2a Feb 22, 2017 @lucasbento v1.1.16

README.md

Create GraphQL

Create production-ready GraphQL servers

About

Check out the post on Medium! 😉

Install

npm install --global create-graphql

Update

npm update --global create-graphql

Usage

It can generate:

  • Mutations (create and edit);
  • Types;
  • Connections;
  • Loaders;
  • Tests (with Jest);
  • Generate from a Mongoose schema (with --schema option).

Examples

  • Generate a type, the file name will be automatically suffixed with Type:

    create-graphql generate --type Awesome # create `AwesomeType`.
  • Generate mutations, the files names will be automatically suffixed with Mutation:

    create-graphql generate --mutation Awesome # create `AwesomeAddMutation` & `AwesomeEditMutation`.
  • Generate mutations based on a Mongoose schema:

    create-graphql generate --mutation Awesome --schema Awesome

    Which will look for Awesome Mongoose schema file in ./src/model and generate the mutations fields based on it.

🔥 Tip: You may use aliases commands to create multiple files in one single command:

create-graphql -tm Awesome --schema Awesome
# Will create a GraphQL type and mutation based on `Awesome` schema.

😎 Mutations and types will be automatically generated with tests.

Options

init - Create a brand-new GraphQL server

create-graphql init AwesomeProject

Provides an easy way to create a GraphQL server based on @entria/graphql-dataloader-boilerplate which is a production-ready server.

We are currently using the same boilerplate on three applications running on production at @entria.

generate

-t, --type - Create a GraphQL type

The following command will generate the file AwesomeType under the folder ./src/type:

create-graphql --type Awesome

./src/type/AwesomeType.js

import {
  GraphQLObjectType,
  GraphQLString,
} from 'graphql';

export default new GraphQLObjectType({
  name: 'AwesomeType',
  description: 'Represents AwesomeType',
  fields: () => ({
    example: {
      type: GraphQLString,
      description: 'My example field',
      resolve: obj => obj.example,
    },
  }),
});

Using the --schema option with a Mongoose schema, the type would be generated like this:

./src/model/Comment.js

import mongoose from 'mongoose';
const { ObjectId } = mongoose.Schema.Types;

const Schema = new mongoose.Schema({
  content: {
    type: String,
    description: 'Comment content in the original language',
    required: true,
  },
  user: {
    type: ObjectId,
    ref: 'User',
    indexed: true,
    description: 'User that created this comment',
    required: true,
  },
  owner: {
    type: ObjectId,
    required: true,
    description: 'Object that owns of this product. References to Product, Posts or other comment.',
  },
}, {
  collection: 'comment',
  timestamps: {
    createdAt: 'createdAt',
    updatedAt: 'updatedAt',
  },
});

export default mongoose.model('Comment', Schema);

./src/type/CommentType.js

import {
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
} from 'graphql';

export default new GraphQLObjectType({
  name: 'CommentType',
  description: 'Represents CommentType',
  fields: () => ({
    content: {
      type: GraphQLString,
      description: 'Comment content in the original language',
      resolve: obj => obj.content,
    },
    user: {
      type: GraphQLID,
      description: 'User that created this comment',
      resolve: obj => obj.user,
    },
    owner: {
      type: GraphQLID,
      description: 'Object that owns of this product. References to Product, Posts or other comment.',
      resolve: obj => obj.owner,
    },
  }),
});

 

-m, --mutation - Create GraphQL mutations

The following command will generate the files AwesomeAddMutation & AwesomeEditMutation under the folder ./src/mutation:

create-graphql --mutation Awesome

./src/mutation/AwesomeAddMutation.js

import {
  GraphQLID,
  GraphQLString,
  GraphQLNonNull,
} from 'graphql';
import {
  mutationWithClientMutationId,
  toGlobalId,
} from 'graphql-relay';

import AwesomeLoader from '../loader/AwesomeLoader';
import AwesomeConnection from '../connection/AwesomeConnection';

export default mutationWithClientMutationId({
  name: 'AwesomeAdd',
  inputFields: {
    example: {
      type: GraphQLString,
      description: 'My example field',
    },
  },
  mutateAndGetPayload: async ({ example }) => {
    // TODO: mutation logic

    return {
      // id: id, // ID of the newly created row
      error: null,
    };
  },
  outputFields: {
    awesomeEdge: {
      type: AwesomeConnection.edgeType,
      resolve: async({ id }, args, { user }) => {
        // TODO: load new edge from loader

        const awesome = await AwesomeLoader.load(
          user, id
        );

        // Returns null if no node was loaded
        if (!awesome) {
          return null;
        }

        return {
          cursor: toGlobalId('awesome', awesome),
          node: awesome,
        };
      },
    },
    error: {
      type: GraphQLString,
      resolve: ({ error }) => error,
    },
  },
});

./src/mutation/AwesomeEditMutation.js

import {
  GraphQLID,
  GraphQLString,
  GraphQLNonNull,
} from 'graphql';
import {
  mutationWithClientMutationId,
  toGlobalId,
} from 'graphql-relay';

import AwesomeType from '../type/AwesomeType';
import AwesomeLoader from '../loader/AwesomeLoader';
import AwesomeConnection from '../connection/AwesomeConnection';

export default mutationWithClientMutationId({
  name: 'AwesomeEdit',
  inputFields: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    example: {
      type: GraphQLString,
    },
  },
  mutateAndGetPayload: async ({ id, example }) => {
    // TODO: mutation logic

    return {
      id: id,
      error: null,
    };
  },
  outputFields: {
    awesomeEdge: {
      type: AwesomeConnection.edgeType,
      resolve: async({ id }, args, { user }) => {
        // TODO: load new edge from loader

        const awesome = await AwesomeLoader.load(
          user, id
        );

        // Returns null if no node was loaded
        if (!awesome) {
          return null;
        }

        return {
          cursor: toGlobalId('awesome', awesome),
          node: awesome,
        };
      },
    },
    awesome: {
      type: AwesomeType,
      resolve: async ({ user, id }) => {
        if (!user || !id) {
          return null;
        }

        return await AwesomeLoader.load(user, id);
      },
    },
    error: {
      type: GraphQLString,
      resolve: ({ error }) => error,
    },
  },
});

Using the --schema option with a Mongoose schema, the mutations would be generated like this:

./src/model/Comment.js

import mongoose from 'mongoose';
const { ObjectId } = mongoose.Schema.Types;

const Schema = new mongoose.Schema({
  content: {
    type: String,
    description: 'Comment content in the original language',
    required: true,
  },
  user: {
    type: ObjectId,
    ref: 'User',
    indexed: true,
    description: 'User that created this comment',
    required: true,
  },
  owner: {
    type: ObjectId,
    required: true,
    description: 'Object that owns of this product. References to Product, Posts or other comment.',
  },
}, {
  collection: 'comment',
  timestamps: {
    createdAt: 'createdAt',
    updatedAt: 'updatedAt',
  },
});

export default mongoose.model('Comment', Schema);

 ./src/mutation/CommentAddMutation.js

import {
  GraphQLString,
  GraphQLID,
  GraphQLNonNull,
  GraphQLNonNull,
  GraphQLNonNull,
} from 'graphql';
import {
  mutationWithClientMutationId,
  toGlobalId,
} from 'graphql-relay';

import CommentLoader from '../loader/CommentLoader';
import CommentConnection from '../connection/CommentConnection';

export default mutationWithClientMutationId({
  name: 'CommentAdd',
  inputFields: {
    content: {
      type: new GraphQLNonNull(GraphQLString),
    },
    user: {
      type: new GraphQLNonNull(GraphQLID),
    },
    owner: {
      type: new GraphQLNonNull(GraphQLID),
    },
  },
  mutateAndGetPayload: async ({ example }) => {
    // TODO: mutation logic

    return {
      // id: id, // ID of the newly created row
      error: null,
    };
  },
  outputFields: {
    commentEdge: {
      type: CommentConnection.edgeType,
      resolve: async({ id }, args, { user }) => {
        // TODO: load new edge from loader

        const comment = await CommentLoader.load(
          user, id
        );

        // Returns null if no node was loaded
        if (!comment) {
          return null;
        }

        return {
          cursor: toGlobalId('comment', comment),
          node: comment,
        };
      },
    },
    error: {
      type: GraphQLString,
      resolve: ({ error }) => error,
    },
  },
});

 ./src/mutation/CommentEditMutation.js

import {
  GraphQLString,
  GraphQLID,
  GraphQLNonNull,
} from 'graphql';
import {
  mutationWithClientMutationId,
  toGlobalId,
} from 'graphql-relay';

import CommentType from '../type/CommentType';
import CommentLoader from '../loader/CommentLoader';
import CommentConnection from '../connection/CommentConnection';

export default mutationWithClientMutationId({
  name: 'CommentEdit',
  inputFields: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    content: {
      type: new GraphQLNonNull(GraphQLString),
    },
    user: {
      type: new GraphQLNonNull(GraphQLID),
    },
    owner: {
      type: new GraphQLNonNull(GraphQLID),
    },
  },
  mutateAndGetPayload: async ({ id, example }) => {
    // TODO: mutation logic

    return {
      id: id,
      error: null,
    };
  },
  outputFields: {
    commentEdge: {
      type: CommentConnection.edgeType,
      resolve: async({ id }, args, { user }) => {
        // TODO: load new edge from loader

        const comment = await CommentLoader.load(
          user, id
        );

        // Returns null if no node was loaded
        if (!comment) {
          return null;
        }

        return {
          cursor: toGlobalId('comment', comment),
          node: comment,
        };
      },
    },
    comment: {
      type: CommentType,
      resolve: async ({ user, id }) => {
        if (!user || !id) {
          return null;
        }

        return await CommentLoader.load(user, id);
      },
    },
    error: {
      type: GraphQLString,
      resolve: ({ error }) => error,
    },
  },
});

 

-c, --connection - Create a Relay connection

The following command will generate the file AwesomeConnection importing AwesomeType under the folder ./src/connection:

create-graphql --connection Awesome

./src/connection/AwesomeConnection.js

import { connectionDefinitions } from 'graphql-relay';

import AwesomeType from '../type/AwesomeType';

export default connectionDefinitions({
  name: 'Awesome',
  nodeType: AwesomeType,
});

 

-l, --loader - Create a GraphQL loader

The following command will generate the file AwesomeLoader importing AwesomeConnection under the folder ./src/loader:

create-graphql --loader Awesome

./src/loader/AwesomeLoader.js

import DataLoader from 'dataloader';
import ConnectionFromMongoCursor from '../connection/ConnectionFromMongoCursor';
import AwesomeModel from '../model/Awesome';

type AwesomeType = {
  id: string,
  exampleField: string,
}

export default class Awesome {
  id: string;
  exampleField: string;

  static AwesomeLoader = new DataLoader(
    ids => Promise.all(
      ids.map(id =>
        AwesomeModel.findOne({ _id: id })
      ),
    ),
  );

  constructor(data: AwesomeType) {
    this.id = data.id;
    this.exampleField = data.exampleField;
  }

  static viewerCanSee(viewer, data) {
    // TODO: handle security

    return true;
  }

  static async load(viewer, id) {
    const data = await Awesome.AwesomeLoader.load(id);

    return Awesome.viewerCanSee(viewer, data) ? new Awesome(data) : null;
  }

  static clearCache(id) {
    return Awesome.AwesomeLoader.clear(id);
  }

  static async loadAwesome(viewer, args) {
    // TODO: load multiple rows

    const Awesome = [];

    return ConnectionFromMongoCursor.connectionFromMongoCursor(
      viewer, Awesome, args, Awesome.load
    );
  }

}

 

-, --help - Output all the available commands

Configuration file

You may customize the folders that the generated files will be created on by using a .graphqlrc file on the root folder with the following content:

{
  "directories": {
    "source": "src",
    "connection": "graphql/connection",
    "loader": "graphql/loader",
    "model": "models/models",
    "mutation": "graphql/mutation",
    "type": "graphql/type"
  }
}

How to contribute

  1. Fork this repository;
  2. Clone the forked version of create-graphql:

    git clone git@github.com:<your_username>/create-graphql.git
    
  3. Install lerna/lerna

    npm install --global lerna@prerelease
    
  4. Install the main package depencies

    yarn
    

    or

    npm i
    
  5. Bootstrap all packages

    lerna bootstrap
    

    This will install all dependencies of all subpackages and link them properly

  6. Link the generator package

    cd packages/generator
    npm link
    
  7. Watch all packages (create-graphql and generator)

    npm run watch
    
  8. Create a new branch

    git checkout -b feature/more_awesomeness
    
  9. Make your changes

  10. Run the CLI with your changes

    node packages/create-graphql/dist --help
    
  11. Commit your changes and push your branch

    git add .
    git commit -m 'more awesome for create-graphql'
    git push origin feature/more_awesomeness
    
  12. Open your Pull Request

  13. Have your Pull Request merged! 😎

Feedback?

Open an issue, I will be glad to discuss your suggestions!

License

MIT © Lucas Bento