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:
statusdetermines the visibility of our item. If an item has thepublishedstatus, requests without API tokens will be able to view the item's content (but not thejangleproperties).versionautomatically increments when we make an update to our document. Pretty neat, man.createdandupdatedare 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!
itemIdis the id of our puppy.versionlets us know which version we are looking at.statusplays an important role for the Publishing API, which we will see soon.updatedlets 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.