Node.js Cookbook
Getting Started
To get started with the Node.js driver, please visit the Cloud CMS Node.js Driver Page.
One thing to keep in mind is that the Node.js driver is based on the JavaScript driver. As such, they're pretty similar. That said, the Node.js driver can do a few important tricks that you can't do in the JavaScript driver.
Connecting to Cloud CMS
To connect, create a file called gitana.json
in your application root. For information on how to acquire this file, please read up on Acquiring your API Keys.
You can then simply do:
Gitana.connect(function(err) {
var platform = this;
});
Alternatively, you can pass in your API keys as an argument to the connect()
method.
Gitana.connect({
"clientKey": "{clientKey}",
"clientSecret": "{clientSecret}",
"username": "{username}",
"password": "{password}",
"baseURL": "https://api.cloudcms.com"
}, function(err) {
var platform = this;
});
If a problem was encountered during the authentication, the err
variable will be populated. If not, the this
variable will the Platform
instance you can use to start doing interesting things!
If you supply an application
argument in your API Keys, you'll get back a helper object that you can use to access all of the resources on your application's associated Stack.
Gitana.connect({
"clientKey": "{clientKey}",
"clientSecret": "{clientSecret}",
"username": "{username}",
"password": "{password}",
"baseURL": "https://api.cloudcms.com"
}, function(err) {
var helper = this;
var repository = helper.datastore("content");
});
Asynchronous Chaining
The driver is unique and unusual in that it offers a Chaining based solution to making asynchronous series of API calls. It's similar to Promises (but pre-dates them!) and makes it possible to make a series of API calls without having to write as much code.
For example, with this driver, you can write the following:
platform.readRepository("1234567890abcdef1234").readBranch("master").then(function() {
this.createNode({"title": "Article #1"});
this.createNode({"title": "Article #2"});
this.createNode({"title": "Article #3"});
});
This might look similar to Promises but it has a few differences. One important difference is that the then()
method modifies the this
to be the result. This allows you to chain calls as shown above, or even like this:
platform.readRepository("1234567890abcdef1234").readBranch("master").createNode({"title": "Article #1"});
Which is effectively the same, in concept, as a callback approach where you might do something like:
readRepository(platform, function(err, repository) {
if (err) {
return handleError(err);
}
readBranch(repository, "master", function(err, branch) {
if (err) {
return handleError(err);
}
createNode(branch, {"title": "Article #1"}, function(err, node) {
if (err) {
return handleError(err);
}
// yay I made it!
});
})'
});
Not so with our driver. You can keep your code lean and clean. You just need to be mindful of the changes in scope (the this
variable). Which is unique. But there it is.
Code Samples
Here are some code samples to help you get started.
In these examples, we'll assume that you're connecting to Cloud CMS using API keys that either provide the application
value or they do not. The examples below show an essentially skeleton for a JS application that uses the driver for both cases.
In all of the examples that follow, assume that your code gets inserted into the TODO
section.
In Cloud CMS, you can have as many repositories as you'd like. And each repository can multiple branches (similar to Git). The first thing you should do is connect and get the branch that you want to work on.
For cases where the repository and branch must be explicitly referenced, we'll assume that the repository ID is 1234567890abcdef1234
and the branch ID is master
.
API Keys with application
Gitana.connect(function(err) {
if (err) {
return console.log("Failed to connect", err);
}
var helper = this;
var repository = helper.datastore("content");
repository.readBranch("master").then(function() {
var branch = this;
// TODO: add your custom code here
});
});
Standard API Keys (without application
)
Use the readRepository
method to fetch a specific repository. The user you've logged in as must have READ
permissions to the repository. If they do not have read rights, a null response is expected.
Use the readBranch
method to fetch a specific branch. The user you've logged in as must have READ
permissions to the branch. If they do not have read rights, a null response is expected.
Gitana.connect(function(err) {
if (err) {
return console.log("Failed to connect", err);
}
var platform = this;
platform.readRepository("1234567890abcdef1234").then(function() {
var repository = this;
this.readBranch("master").then(function() {
var branch = this;
// TODO: add your custom code here
});
});
});
Node Creation
Create a Node
Create a basic node of type n:node
(the default type).
var branch = this;
branch.createNode({
"title": "My First Node"
}).then(function() {
// NOTE: this = node
// add your custom code here
});
Create a Node for a Custom Content Type
Suppose we have a custom content type called custom:article
that looks like this:
{
"title": "Custom Article",
"_qname": "custom:article",
"type": "object",
"properties": {
"title": {
"type": "string"
},
"body": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"rating": {
"type": "number"
}
}
}
Create a content instance of this type like this:
var branch = this;
branch.createNode({
"_type": "custom:article",
"title": "Article #1",
"body": "A still more glorious dawn awaits",
"categories": ["category1", "category2", "category3"],
"rating": 5
}).then(function() {
// NOTE: this = node
// add your custom code here
});
Create a Folder
In Cloud CMS, folders are just nodes with the f:container
feature on them.
var branch = this;
branch.createNode({
"title": "My Folder",
"_features": {
"f:container": {}
}
}).then(function() {
// NOTE: this = node
// add your custom code here
});
If you want your folder to appear in the general folder hierarchy, you must link it to a parent folder that is already a member of the folder hierarchy. The main folder hierarchy descends from the branch root node.
Here is the long way of doing this:
var branch = this;
branch.rootNode().then(function() {
var rootNode = this;
Chain(branch).createNode({
"title": "My Folder",
"_features": {
"f:container": {}
}
}).then(function() {
var folder = this;
Chain(rootNode).associate(folder, {
"_type": "a:child"
}).then(function() {
// done!
});
});
});
An easier way to do this is to use the following special properties when the node is created:
_parentFolderPath
- the path to the parent folder where the created node should be linked. The folder must exist. If it does not exist, the operation will fail._fileName
- thef:filename
to be assigned to the node once it is created.
Alternatively, you can use:
_filePath
- an absolute path where the file should be created. This allows you to specify the parent folder path and the file name at the same time,.
You can also adjust the association type (between the discovered parent folder and your newly created item).
_associationTypeString
- the QName for the newly created association. The default isa:child
.
Thus, you could do:
var branch = this;
branch.createNode({
"title": "My Folder",
"_features": {
"f:container": {}
},
"_parentFolderPath": "/",
"_fileName": "my_folder"
}).then(function() {
var folder = this;
});
Or you could do:
var branch = this;
branch.createNode({
"title": "My Folder",
"_features": {
"f:container": {}
},
"_parentFolderPath": "/",
"_filePath": "/my_folder"
}).then(function() {
var folder = this;
});
Associations
In Cloud CMS, any two nodes can be connected via an association. You can use an out-of-the-box association type, such as a:linked
or a:owned
or you can create your own.
Let's imagine that we have two articles that look like this:
- Article #1
{
"_doc": "1234567890abcdef1111",
"_type": "custom:article",
"title": "Article #1",
"body": "a mote of dust suspended in a sunbeam",
"categories": ["category1", "category2"],
"rating": 1
}
- Article #2
{
"_doc": "1234567890abcdef2222",
"_doc": "",
"_type": "custom:article",
"title": "Article #2",
"body": "harvesting star light",
"categories": ["category2", "category3"],
"rating": 2
}
Let's create an a:linked
association that points from the first article (1234567890abcdef1111
) to the second article (1234567890abcdef2222
), like this:
var branch = this;
branch.associate("1234567890abcdef1111", "1234567890abcdef2222", "a:linked').then(function() {
var association = this;
});
Or we can do this using the nodes themselves:
var branch = this;
branch.readNode("1234567890abcdef1111").then(function() {
var article1 = this;
Chain(branch).readNode("1234567890abcdef2222").then(function() {
var article2 = this;
Chain(article1).associate(article2, "a:linked").then(function() {
var association = this;
});
});
});
You can also find all associations around article1
. This will include associations that are INCOMING
, OUTGOING
and MUTUAL
.
article1.associations().each(function() {
console.log("Found association: " + this._doc);
});
Or find only the INCOMING
associations:
article1.associations({
"direction": "incoming"
}).each(function() {
console.log("Found incoming association: " + this._doc);
});
Or find only the OUTGOING
associations of type a:linked
:
article1.associations({
"direction": "outgoing",
"type": "a:linked"
}).each(function() {
console.log("Found outgoing linked association: " + this._doc);
});
Query for Nodes
Let's now look at querying for content. This makes use of MongoDB's query language to express some pretty powerful queries. We recommend further reading on Query within Cloud CMS.
Let's assume that we have the following:
- Article #1
{
"_type": "custom:article",
"title": "Article #1",
"body": "a mote of dust suspended in a sunbeam",
"categories": ["category1", "category2"],
"rating": 1
}
- Article #2
{
"_type": "custom:article",
"title": "Article #2",
"body": "harvesting star light",
"categories": ["category2", "category3"],
"rating": 2
}
- Article 3
{
"_type": "custom:article",
"title": "Article #3",
"body": "we are star stuff",
"categories": ["category3", "category1"],
"rating": 3
}
Basic Querying
Find all of the articles that have a category
with the value category1
.
var branch = this;
branch.queryNodes({
"_type": "custom:article",
"category": "category1"
}).count(function(c) {
console.log("The size was: " + c);
}).each(function() {
console.log("Found a result, title: " + this.title + ", rating: " + this.rating);
}).then(function() {
// done!
});
Pagination
If you have a lot of potential query hits, you'll want to paginate. Here is an example where we return results starting at index 2 and hand back at most 5 results.
var branch = this;
branch.queryNodes({
"_type": "custom:article",
"category": "category1"
}, {
"skip": 2,
"limit": 5
}).each(function() {
console.log("Found a result, title: " + this.title + ", rating: " + this.rating);
}).then(function() {
console.log("The size was: " + this.size()); // 1
console.log("The offset was: " + this.offset()); // 2
console.log("The totalRows was: " + this.totalRows()); // 3
});
Sorting
Use the pagination option to sort the results as you see fit. Here we sort descending on rating
.
var branch = this;
branch.queryNodes({
"_type": "custom:article",
"category": "category1"
}, {
"sort": {
"rating": -1
}
}).each(function() {
console.log("Found a result, title: " + this.title + ", rating: " + this.rating);
});
The results will look like:
Found a result, title: Article #3, rating: 3
Found a result, title: Article #2, rating: 2
Found a result, title: Article #1, rating: 1
Query for values in a range
Find all of the articles where the rating is greater than or equal to 2.
This demonstrates the power of MongoDB's query language.
var branch = this;
branch.queryNodes({
"_type": "custom:article",
"rating": {
"$gte": 2
}
}).each(function() {
console.log("Found a result, title: " + this.title + ", rating: " + this.rating);
});
Search for Nodes
Let's now look at searching for content. This makes use of Elastic Search's DSL to express some very powerful full text and structured searches. We recommend further reading on Search within Cloud CMS.
We also suggest reading up on Query Strings to understand how you can write searches textually (in addition to structuring them as JSON objects):
Let's assume that we have the following:
- Article #1
{
"_type": "custom:article",
"title": "Article #1",
"body": "a mote of dust suspended in a sunbeam",
"categories": ["category1", "category2"],
"rating": 1
}
- Article #2
{
"_type": "custom:article",
"title": "Article #2",
"body": "harvesting star light",
"categories": ["category2", "category3"],
"rating": 2
}
- Article 3
{
"_type": "custom:article",
"title": "Article #3",
"body": "we are star stuff",
"categories": ["category3", "category1"],
"rating": 3
}
Full Text Search
Find all of the nodes with the word star
. Simple enough!
var branch = this;
branch.searchNodes("star").each(function() {
console.log("Found a result, title: " + this.title + ", rating: " + this.rating);
});
The results will be:
Found a result, title: Article #2, body: harvesting star light
Found a result, title: Article #3, body: we are star stuff
Node Attachments
You can store binary files onto Nodes as attachments.
Note: If you're working with the JavaScript driver in the browser, there are some limitations in terms of the kinds of payloads you can send to the Cloud CMS API. This is because the browser does not let you stream files from local disk. You are limited to the kinds of content that can be expressed within JavaScript itself (usually text, JSON and similar formats).
Attachments have the following methods:
getId()
returns the ID of the attachmentgetLength()
returns the length of the attachment in bytesgetContentType()
returns the mimetype of the attachmentgetFilename()
returns the optional filename of the attachmentgetUri()
returns a datastore-relative path to the attachmentgetDownloadUri()
returns a driver baseURI-relative path to the attachmentdel()
deletes the attachmentdownload()
downloads the attachment and hands it back to you. The exact format depends on the mimetype. For text files, this will be a string. For JSON documents, this will be a JavaScript object literal. Otherwise, it will be a byte array.stream()
streams back the attachment (only available in Node.js)
Listing Attachments of a Node
To list the attachments of a Node, do the following:
var node = ...;
node.listAttachments().each(function() {
console.log("The node: " + node._doc + " has an attachment: " + this.getId() + " (mimetype = " + this.getContentType() + ", length: " + this.getLength() + ")");
});
Uploading an Attachment to a Node
To upload an attachment, do the following:
var node = ...;
node.attach("default", "text/plain", "Hello World", "helloworld.txt");
Downloading an Attachment of a Node
To download an attachment:
var node = ...;
node.attachment("default").download(function(data) {
console.log("Downloaded: " + data);
});
Attachments as Streams
To upload an attachment using a stream:
const fs = require("fs");
var readStream = fs.createReadStream("./images/photo.png");
var node = ...;
node.attach("default", "image/png", readStream, "photo.png").then(function() {
console.log("Attachment created");
});
To read a stream for an attachment, try this:
const fs = require("fs");
var node = ...;
node.attachment("default").stream(function(err, readStream) {
var writeStream = fs.createWriteStream("./downloads/photo.png");
readStream.pipe(writeStream);
console.log("Attachment downloaded");
});
Note that stream support is only available when running inside of Node.js.