Jangle

a headless cms built on mongoose.

Quick Start


Make sure you have the latest versions of NodeJS and MongoDB.

Let's start by getting the jangle-cms npm module.

npm install jangle-cms

Next, we'll create a file called app.js and enter the following:

const jangle = require('jangle-cms')

jangle.start()

Now let's run our Jangle server with NodeJS:

node app.js

That's it! Your very own Jangle CMS is now running at http://localhost:3000/api


Adding in your own models

To create your own models, you'll first need to install mongoose via npm:

npm install mongoose

Let's update our app.js from before:

const jangle = require('jangle-cms')
const mongoose = require('mongoose')

const PuppySchema = new mongoose.Schema({
  name: String,
  favoriteToy: {
    type: String,
    enum: ['ball', 'frisbee', 'socks']
  }
})

jangle.start({
  schemas: {
    Puppy: PuppySchema
  }
})

Here we have created our first schema, PuppySchema , and passed it into Jangle's start function.

This allows Jangle to automatically create the following REST API endpoints:

GET     /api/puppies       # Gets all puppies.
GET     /api/puppies/:id   # Finds a puppy by id.
POST    /api/puppies       # Creates a puppy.
PUT     /api/puppies/:id   # Updates a puppy with a full item.
PATCH   /api/puppies/:id   # Updates some fields for a puppy.
DELETE  /api/puppies/:id   # Removes a puppy.

Let's run our server again, now that we've created the Puppy model:

node app.js

Using the API

For this part of the guide, we recommend using Postman. It will allow us to play with our new API.

Let's start with a GET request to http://localhost:3000/api/auth/status

We should see this response:

{
    "hasUsers": false
}

The /api/auth/status endpoint lets us know if we can anonymously sign up.

Most API endpoints will require a token for access. However, the Authentication API is an exception.

We need to be able to sign up the first user, as well as sign in later to receive our API token.

Let's start by creating that first user.

Make a POST request to http://localhost:3000/api/auth/sign-up

We'll need to provide the following JSON in our request body:

{
    "email": "[email protected]",
    "password": "password",
    "role": "admin"
}

This will create our first admin user and return a response like this:

{
    "error": false,
    "message": "Sign in successful!",
    "data": {
        "_id": "5a2e4bd2e58af4d186057f72",
        "email": "[email protected]",
        "role": "admin",
        "token": "<some-token>"
    }
}

Great job! You just created our first user.

Go ahead and copy that token, we'll be using it to create our first item!


Creating puppies

The last section gave us an API token for accessing our Jangle API.

That token will protect our data from unwanted updates, and help automatically track who is making updates and creating items in our lists.

Let's make a POST request to http://localhost:3000/api/puppies?token=<some-token> where <some-token> is replaced with our actual API token.

Let's also define the following JSON body for our request:

{
    "name": "Jangle",
    "toy": "socks",
    "jangle.status": "draft"
}

"Wait, what's the jangle.status thing?"

Jangle fully supports publishing to help us control our content's visibility.

Later sections will go into more details, so let's skip that for now.

That request we just made should return you the following response:

{
    "error": false,
    "message": "Created 1 item.",
    "data": {
        "name": "Jangle",
        "favoriteToy": "socks",
        "jangle": {
            "status": "draft",
            "version": 1,
            "created": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            },
            "updated": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            }
        },
        "_id": "<our-puppy-id>"
    }
}

Awesome! We just created a puppy with some additional information.

This might be a good time to go over what those jangle properties are:

  • status determines the visibility of our item. If an item has the published status, requests without API tokens will be able to view the item's content (but not the jangle properties).
  • version automatically increments when we make an update to our document. Pretty neat, man.
  • created and updated are useful for tracking changes, and seeing who renamed our puppy "Captain McFartface".

Jangle relies on these simple properties to empower her users.

Without using status, we couldn't save our rough drafts for later.

Without a version, any mistakes we made would be completely irreversible.

It gives us the ability to browse through our item history and easily revert mistakes.


Did you say "easily revert mistakes"?

Let's see how we can use Jangle History API to recover from a mistake.

To demonstrate this, we first need to make an update.

Let's use the PATCH method on http://localhost:3000/api/puppies/<our-puppy-id>?token=<our-token>

Make sure we use our actual API token and the Puppy ID we saw before. Set the request body to this:

{
    "name": "Captain McFartface",
    "jangle.status": "published"
}

The result of our modification returns the following response:

{
    "error": false,
    "message": "Patched 1 item.",
    "data": {
        "_id": "<our-puppy-id>",
        "name": "Jangle",
        "favoriteToy": "socks",
        "jangle": {
            "status": "draft",
            "version": 1,
            "created": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            },
            "updated": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            }
        }
    }
}

Note that the response gives us the old item in case we wanted to do something cool with it.

Let's check out the latest version of our puppy.

Make a GET request to http://localhost:3000/api/puppies/<our-puppy-id> (with our API token, as always)

Here is what we get as a result:

{
    "error": false,
    "message": "Found 1 item.",
    "data": {
        "_id": "5a2e4e0be58af4d186057f76",
        "name": "Captain McFartface",
        "favoriteToy": "socks",
        "jangle": {
            "version": 2,
            "status": "published",
            "created": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            },
            "updated": {
                "at": "2017-12-11T09:42:31.249Z",
                "by": "<our-user-id>"
            }
        }
    }
}

As you can see, jangle.version has been incremented and our changes are on the the item.

But oh dear... "Captain McFartface"? Let's restore our item back to the previous version.

We can look through our available versions with the History API.

Perform a GET request to http://localhost:3000/api/jangle/history/<our-puppy-id> with our API token:

{
    "error": false,
    "message": "Found 1 item.",
    "data": [
        {
            "_id": "<history-item-id>",
            "itemId": "<our-puppy-id>",
            "version": 1,
            "status": "draft",
            "updated": {
                "by": "<our-user-id>",
                "at": "2017-12-11T09:21:15.277Z"
            },
            "changes": [
                {
                    "field": "name",
                    "oldValue": "Jangle"
                }
            ]
        }
    ]
}

Oh cool! We have a list of all the previous versions for our puppy!

  • itemId is the id of our puppy.
  • version lets us know which version we are looking at.
  • status plays an important role for the Publishing API, which we will see soon.
  • updated lets us keep track of who made that version.

But the most important field changes tells us which fields changed, and what their old values were.

Given the latest item and a version number, Jangle uses this field to rebuild the version of the item we want to restore.

Let's preview what the restore operation would look like if we wanted to roll back to version 1.

GET http://localhost:3000/api/jangle/history/<our-puppy-id>/restore?list=puppies&version=1&token=<our-token>

Executing that request will show us what the restore operation will result in:

{
    "error": false,
    "message": "Found 1 item.",
    "data": {
        "_id": "5a2e4e0be58af4d186057f76",
        "name": "Jangle",
        "favoriteToy": "socks",
        "jangle": {
            "version": 3,
            "status": "published",
            "created": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            },
            "updated": {
                "at": "2017-12-11T09:42:31.249Z",
                "by": "<our-user-id>"
            }
        }
    }
}

Looks good to me!

Let's actually perform it by changing the GET method to POST

{
    "error": false,
    "message": "Updated 1 item.",
    "data": {
        "_id": "<our-puppy-id>",
        "name": "Captain McFartface",
        "favoriteToy": "socks",
        "jangle": {
            "version": 2,
            "status": "published",
            "created": {
                "at": "2017-12-11T09:21:15.277Z",
                "by": "<our-user-id>"
            },
            "updated": {
                "at": "2017-12-11T09:42:31.249Z",
                "by": "<our-user-id>"
            }
        }
    }
}

Just like before, the old item is returned to us on a successful update.

Let's check out our GET http://localhost:3000/api/puppies endpoint to see all our puppies:

{
    "error": false,
    "message": "Found 1 item.",
    "data": [
        {
            "_id": "5a2e4e0be58af4d186057f76",
            "jangle": {
                "updated": {
                    "by": "5a2e4bd2e58af4d186057f72",
                    "at": "2017-12-11T10:07:43.471Z"
                },
                "created": {
                    "by": "5a2e4bd2e58af4d186057f72",
                    "at": "2017-12-11T09:21:15.277Z"
                },
                "status": "published",
                "version": 3
            },
            "favoriteToy": "socks",
            "name": "Jangle"
        }
    ]
}

Perfect! "Jangle" is restored. As you might have noticed, our item's version number is 3 instead of 1 .

That is because the history for item is immutable.

That means no matter how many edits and restores you make each update will be completely reversible.

If we wanted to restore version 2 and reinstate "Captain McFartface", Jangle is more than happy to help.


Sharing puppies with the world

So far, all of our changes to the puppy list has been private.

That means viewing our puppies requires an API token. How depressing.

Let's make a POST request to http://localhost:3000/api/puppies/publish/<our-puppy-id> to publish a pup!

After donig so, we get a response back like this:

{
    "error": false,
    "message": "Published 1 item.",
    "data": {
        "_id": "<some-new-id>",
        "name": "Jangle",
        "favoriteToy": "socks",
        "jangle": {
            "itemId": "<our-puppy-id>"
        }
    }
}

The jangle metadata on the public lists is much simpler.

It doesn't track a status or a version but it is important to keep track of the itemId in case we want to easily unpublish our item later.

Before we do that, let's make a GET request at http://localhost:3000/api/puppies without our token:

{
    "error": false,
    "message": "Found 1 item.",
    "data": [
        {
            "_id": "<some-new-id>",
            "name": "Jangle",
            "favoriteToy": "socks"
        }
    ]
}

Hooray! Now everyone can see how much Jangle loves socks!

Notice the jangle metadata is excluded by default. Normal people accessing the API won't need to see that information.

When we decide it's time to unpublish our puppy, we can do so with another API endpoint.

Make a DELETE request to http://localhost:3000/api/puppies/unpublish/<our-puppy-id> with your access token.

{
    "error": false,
    "message": "Unpublished 1 item.",
    "data": {
        "_id": "<some-new-id>",
        "name": "Jangle",
        "favoriteToy": "socks"
    }
}

That's it. Making a GET request again to http://localhost:3000/api/puppies shows that there are no more publicly available results:

{
    "error": false,
    "message": "Found 0 items.",
    "data": []
}

Easy peasy! Looks like we have full control over when our content should be visible.


Removing and Permanent Deletion

With Jangle, allowing content editors to recover from their mistakes is incredibly important.

For this reason, calling DELETE http://localhost:3000/api/puppies/<our-puppy-id> will not permanently remove the puppy item and all of it's history.

Instead, it creates a new version with jangle.status set to deleted

This empowers Admin UIs the ability to easily implement an "undo" operation after delete.

This is a better alternative to confirmation prompts for every item removal.

Or even worse: removing content forever without prompting the editor at all.

In order to actually remove all data for an item (including it's history) just use the ?permanent=true query parameter at the end of the DELETE request.

That's it for now! Thanks for using Jangle!

results matching ""

    No results matching ""