Auto generating CMS based on your GraphQL schema


#1

second version of GraphQL CMS

I was really excited about capabilities graphQL gives you when I first started to work with it.
So far this pretty new but powerful tool doesn’t have that good support from developers community as I would like it to have.

In my last article I wrote about npm package mongoose-schema-to-graphql which will help any developers who works with Mongoose to short their code twice and save the same amount of time.

Today I will talk about my new package graphql-auto-generating-cms which probably could save you even more time, and let you think more about architecture and business processes then about building UI for admin-panel. You will find some screenshot on the end of the article.

As you already figured out from the name, this package will use your printed schema to automatically generate fully-functional CMS without any additional efforts.The most important is that without any changes in your app architecture and current code!

Package contains two modules: first is a simple React component you will use to render CMS, second is Node/Express middleware to provide schema and additional configurations if you needed.

Basically you have two ways of how to generate CMS: first way is with minimum efforts, you would just provide schema and insert CMS component wherever you want, in a separate rout or inside another component “wrapper”. For example to provide footer or header with login system or just to extend CMS functionality.

And second way is more detailed and advanced configuration. In both situation you have to follow the simple pattern we will talk about shortly.

Right now this package doesn’t support list and nested elements, but it will be provided in future releases. For now you can solve this problem just by extending autogenerated CMS. Will talk how to do that in this article.

####What does CMS support:

  • Pagination
  • Create new items
  • Update
  • Remove
  • Exclude some types from CMS
  • Detailed rules to generate CMS
  • Field validation based on your schema
  • Custom pages to extend CMS functionality

####What it doesn’t support [will be supported in future releases]:

  • Nested elements and lists [could be solved by using custom pages]
  • Routing [just not first priority]
  • File uploading

So far functionality provided by this module will be enough for most cases of projects, and will save you a good amount of time and money.

It’s good enough out of the box for some static content pages, and for any other solution you can just extend functionality by providing custom CMS pages.

####Based on the the above lets separate this article to following paragraphs:

  1. Common requirements
  2. Prepare to start
  3. Fast method of generating CMS
  4. More detail configured CMS generation
  5. Config file structure
  6. Extend CMS with your additional functionalities

###1. Common requirements

Actually this is pretty simple. All your Query methods has to have: {offset: Int!, limit: Int!} arguments which basically will be used for pagination, because without pagination if you would provide 60k documents to CMS component it will crushed.
In your resolver it could look like the following:

return new Promise((resolve, reject) => {
   Ingredients.find(query).skip(offset).limit(limit).exec((err, res) => err ? reject(err) : resolve(res));
});

Second requirements is in your Query method. You have to support “id” or “_id” argument with type “string” or “Int” based on which kind of storage you use. This argument will be used to query one item, so make sure that you provide this fields in graphql Type’s fields as well.

Also, if you use “findOne” instead of “find” or any other method to return one item, make sure that you wrapped it inside array, [{item}].

Keep in mind that CMS will use only graphql types which has one Query method and at least one Mutation method.

All types can only have one Query method [find] to query list of items or one item by providing id, and three Mutation method [create, update, remove].

###2. Prepare to start
npm i -S graphql-auto-generating-cms

On your server side, we have to insert middleware to provide schema, and endpoint url which we will use on client, in example it is '/graphql_cms_endpoint':

…
import express from 'express';
import graphqlCMS from 'graphql-auto-generating-cms/lib/middleware';
import schema from '../schema';
const printSchema = require('graphql/utilities/schemaPrinter').printSchema;
let app = express();

let config = {schema: printSchema(schema)}
app.use('/graphql_cms_endpoint', graphqlCMS(config));
…
app.listen(port)

After that we can use our React component to render CMS wherever we want. In the example below we will run it on a separate rout '/graphql-cms':

...
import GraphqlCMS from 'graphql-auto-generating-cms';

export default (
    <Router onUpdate={() => window.scrollTo(0, 0)} history={browserHistory}>
        <Route
           path='/graphql-cms'
           endpoint='/graphql_cms_endpoint'
           graphql='/graphql'
           components={GraphqlCMS}
        />
        …
</Router>

Or as child component:

<GraphqlCMS
   endpoint='/graphql_cms_endpoint'
   graphql='/graphql'
/>

In property “endpoint” we provide same endpoint URL we provided in middleware and in 'graphql' property we provide URL to our GraphQL API.

Basically that’s it! Based on which approach (simple or detailed) you will use on path '/graphql-cms' you will see your ready to use autogenerated CMS. Now let’s talk about approach how to make all this work.

###3. Fast method of generating CMS
To use this approach and leave all work to module you have to use following naming pattern in your graphql schema. Keep in mind by using second approach you don’t have to follow any pattern so you don’t have to change anything in your existing code! I prefer second approach but this one will be very useful for new project or in dev mode.

Your CMS structure will use the same structure you used in your schema, so if you have:

{
productType: {},
userType: {},
categoryType: {},
...
}

In your side menu of CMS it will have same order:

productType
userType
categoryType

It would be the same about properties. If you have following shape:

let productType = new GraphQLObjectType({
    name: 'productType',
    fields: {
        _id: {type: GraphQLString},
        title: {type: GraphQLString},
        shortDescription: {type: GraphQLString},
        price: {type: GraphQLString},
        isPublished: {type: GraphQLBoolean},
        createdAt: {type: GraphQLString},
        updatedAt: {type: GraphQLString},
        bulletPoints: {type: GraphQLString},
        scienceShort: {type: GraphQLString},
        scienceFull: {type: GraphQLString},
    }
});

In your UI of product item view page, fields will have exact same order.
So if you want to change the order in your CMS, you just have to make changes in your schema.

By the way, in the second approach with configuration, to sort the fields in CMS we will use same approach except instead of making any change in schema we will specify it in config object.

About naming. As we talk above about Query method [find] and Mutation methods [create, update, remove], In order to let module know which method is what, you have to use following pattern of naming Querys or Mutations methods:
[graphql Type name]_[action]
examples:

productType_find
productType_create
productType_update
productType_remove

The pattern is pretty easy and again in the second approach we won’t have to follow this pattern or do any changes in existing code. So let’s talk about it.

###4. More detail configured CMS generation
####Pros in compare with the first method:

  • You don’t have to change your current code at all
  • You don’t have to use naming pattern
  • You can deprecate some method you don’t want to use in CMS
  • You can change the order for the side menu or fields in the view without any change in current schema
  • You can provide different side menu and fields label. By default it will use your graphql Type name for side menu and props name for fields label in the view page
  • Disabling some fields in item view page
  • You can customize which fields will be used in the table list page for each column. By default it will use [id/_id] for first column and second field of graphql Type as the second column
  • You can specify custom input types and controllers, for example “date” or ‘textarea’.

By default all fields of graphql Type’s which is not mentioned in Mutation method as arguments will be shows as disabled, so you can’t edit it, it can be different fields based on your action. When you edit or create new item, each action will use arguments from its Mutation method to mark some fields disabled or to enable all of them.

So let’s see how it works.
On your server side we just have to extend config object. All config properties besides 'schema' are not required.

let config = {schema: printSchema(schema)}
app.use('/graphql_cms_endpoint', graphqlCMS(config));
Config file structure
let config = {
    schema: printSchema(schema), 
    // your printed schema [required]
    
    exclude: ['paymentType', 'invoiceType'], 
    //graphql Types which you don't want to show in CMS

    rules: {
    // rules object is a tree with rules for each or some 
    // graphql types 
    // CMS in addition will use the same order for side menu 
    // and fields 
    // in the view page as you will provide in “rules” object
    
        categoryType: { 
        // graphql Type name, by default will be used as name 
        // for side menu
        
            label: 'Categories', 
            // custom side menu name

            listHeader: {
            // data from this fields will be used to show on first 
            // column [id] and second [title] on list page. 
            // you can provide  couple of fields for each columns 
            // so it will shows as “String” + “ “ + “String”
            
                id: ['id'],
                title: ['description']
            },
            
            resolvers: {
            // if you don't want to use the naming pattern 
            // you have to provide 
            // Query's and Mutation's method name for each 
            // graphql Type.
                                    
                find: {
                    resolver: 'getCategories' 
                    // Query method name
                },
                create: {
                    resolver: 'addCategory'
                    // Mutation method name
                    
                    allowed: true
                },
                update: {
                    resolver: 'updateCategory'
                    // Mutation method name
                    
                    allowed: true
                },
                remove: {
                    allowed: false
                    // if you don't want to provide some method 
                    // to client 
                    // side you can depreciate it for any action  
                    // besides “find”
                }
            },
            fields: {
                _id: {}, 
                // never exclude this field or “id” you always 
                // have to provide id
                
                sortNumber: {
                    label: 'custom field name to show in UI',
                    inputControl: 'input', 
                    // can be “input” or “textarea”
                    
                    inputType: 'number', 
                    // can be any input type: date, text, file etc.
                    
                    disabled: true, 
                    // will disable field from editing
                    
                    exclude: false, 
                    // if true (by default false) it won't 
                    // provide this field
                    // to client side so you can't see it in UI
                },
                name: {}, 
                // you can also provide empty object 
                // if you want to just order fields
                
                createdAt: {},
                updatedAt: {},
                isPublished: {}
            }
        }
    }
}

###5. Extend CMS with your additional functionalities
You can also improve and extend CMS functionality by providing custom pages to CMS with your own logic. For example to handle some features auto-generator doesn’t support yet, like lists or file uploading.

I’m not going to teach you on how to build React components, so I will show you the logic of how to provide custom components to CMS.

All custom pages will have first priority in side menu and will show above other menu points.
On your client side we have to provide one more property 'newMenuItems' to component:

<Route
   path='/graphql-cms'
   endpoint='/graphql_cms_endpoint'
   graphql='/graphql'
   newMenuItems={customPages}
   components={GraphqlCMS}
/>

And in that property we provide array with new menu points “customPages” has following structure:

let customPages = [
    {
       label: 'Custom Page',
       secret: 'uniqeForEachComponentSecret',
       view: {
           secret: 'sameUniqeComponentSecret',
           component: CustomDashboard //your custom React component
       }
    }
]

Above code will look like screenshot below:

I hope this package will be helpful for you. If you like this project, you can star/fork it on GitHub to be up to date about new releases.
This is the first release so if you find any bugs please let me know.
####Also it will be very appreciated if you can help with any of the following:

  • Well written documentation
  • Your participation and contribution to this project to keep it alive
  • Any suggestion on what would you like to see in future releases

Thank you.

Currently I’m working on new version of module, so Star project on GitHub to be up to date.
What will new version support:

  • Nested properties in graphQL Type, any depth
  • graphQLList support
  • Routing
  • File uploading

So with new release auto-generated CMS will handle 90% of your needs from CMS, just by providing printed GraphQL schema!

example code
GitHub
Screenshots:


GraphQL CMS v2 is ready to use
#2

Just don’t want this to be lost in time for everyone


#3

I like your work very much! What license are you applying on it? Thinking on similar for a long time now. Will contribute for sure if can base my work on it.


#4

Thank you. MIT, so feel free to use! Also new version and documentation ready, you can find it on GitHub.


#5

Why should it be? Do you have some issues with using this module?


#6

Nice, we’re doing something a bit similar with Telescope but we’re generating everything from a SimpleSchema schema. I found that GraphQL schemas were a bit limited compared to a full JS object. For example, SimpleSchema lets you define functions to autopopulate fields with default values, things like this.


#7

Yes agree JS object is more informal, but at the same time some people can use different approach of building GraphQL JS object. For example I have project where I specified resolvers right in the schema and I have similar project with FeathersJs where you have to specify resolvers separate from schema.

But printed schema will be the same doesn’t matter which approach or language do you use to create your GraphQL API. So it was the main point why we are using printed schema.

And btw you can check GitHub, second version + documentation now available and it supports pre-populating of lists and much more)