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 thepublished
status, requests without API tokens will be able to view the item's content (but not thejangle
properties).version
automatically increments when we make an update to our document. Pretty neat, man.created
andupdated
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.