Generate GraphQL MongoDB CRUD and Pagination Operations (10 minutes or less)

Ryan P. Hansen6/29/2020, 12:00:00 AM

Generate GraphQL MongoDB CRUD and Pagination Operations (10 minutes or less)

GraphQL and its schema driven development methodology allows for a lot of the common tasks around data resources to be abstracted.

ts-mongo-codegen uses your GraphQL schema and with the inclusion of (opt-in) directives generates all of the CRUD operations for a seamless integration between GraphQL types and MongoDB.

  • Insert Resource insertMountain
  • Insert Many Resources insertManyMountains
  • Find Resource By ObjectID findMountainById
  • Find Many Resources By ObjectIDs findMountainsByIds
  • Paginated and Sorted Queries findMountains
  • Filtered Totals findMountains
  • Update Resource By ObjectID updateMountain
  • Update Many Resources By ObjectIDs updateManyMountains
  • Remove Resource By ObjectID removeMountain
  • Remove Many Resources By ObjectIDs removeManyMountains

With these basic yet useful patters generated it allows the developer to focus on how the end user interacts with the data.

Less time as a result is spent doing error prone tasks like manipulating data types, and more time is spent delivering visible value.

Let's move some mountains!

You can follow along by forking the mountains-api.

Type Defs

Define the Mountain type that we will be augmenting.

import gql from 'graphql-tag'

export const mountainSchema = gql`
  type Mountain @collection(name: "mountains", crud: true) {
    id: ObjectId
    name: String @insert @set @filter
    meters: Float @insert @set @filter
    feet: Float @insert @set @filter
    location: String @insert @set @filter
  }
`

Directives

@collection

With this directive declared ts-mongo-codegen will generate interfaces and factory functions that can be made executable when starting the GraphQL server.

Options

  • name: The name of the mongo collection
  • crud: Should generate crud types and resolvers

@insert

The @insert directive tells ts-mongo-codegen which fields should be included in the CREATE types.

@filter

The @filter directive tells ts-mongo-codegen which fields should be included in the READ types.

@set

The @set directive tells ts-mongo-codegen which fields should be included in the UPDATE types.

Generate

Now that we have our Mountain type defined. We will set up our codegen.json which defines the options for our GraphQL Codegen. More documentation can be found here.

{
  "schema": ["./node_modules/ts-mongo-codegen/dist/types/mongo-types.ts", "./src/gql/*.ts"],
  "generates": {
    "./src/types.generated.ts": {
      "plugins": [
        "typescript",
        "typescript-operations",
        "typescript-resolvers",
        "ts-mongo-codegen"
      ]
    }
  }
}

Using the above configuration we are able to generate the necessary resolvers we will need to implement CRUD and fulfill our GraphQL schema.

yarn install
yarn generate

With the code generated we can put it all together into an executable schema.

import { makeExecutableSchema } from '@graphql-tools/schema'
import { addResolveFunctionsToSchema } from 'apollo-server'
import { graphqlTypeDate, graphqlTypeObjectId, makeAugmentedSchema, mongoTypeDefs } from 'ts-mongo-codegen'
// The following import is the file generated in previous step
import { mountainMutationResolvers, mountainQueryResolvers, mountainResolvers } from './types.generated'
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import { mountainSchema } from './gql/mountains'
import { isAuthenticated } from './auth'

// Make an executable schema with the mongo types and our custom mountain schema type
const executableSchema = makeExecutableSchema({
  typeDefs: [mongoTypeDefs, mountainSchema],
})

// Add CRUD operations to the Mountain type by augmenting the schema
export const schema = makeAugmentedSchema(executableSchema)

// The mountainResolvers, mountainMutationResolvers, and mountainQueryResolvers are generated types
// Run `yarn generate` to update types or add more
const resolvers = composeResolvers(
  {
    Date: graphqlTypeDate,
    Mountain: mountainResolvers,
    Mutation: {
      ...mountainMutationResolvers,
    },
    ObjectId: graphqlTypeObjectId,
    Query: {
      ...mountainQueryResolvers,
    },
  },
  {
    // Make sure mutation requests are authenticated
    Mutation: [isAuthenticated()],
  }
)

// Finally we add our generated resolvers to the schema
addResolveFunctionsToSchema({
  resolvers,
  schema,
})

And with that we are done. We can view our results in the GraphQL playground.

Playground

Conclusion

In this post we generated all of the basic CRUD operations with very little effort. Our clients whether that be web or mobile will now have a very predictable backend backed by MongoDB.

In this article we did not go into creating the context dependency necessary for the resolvers. We will go over that in a different post but if you are interested, you can find the implementation here.