Routing

The purpose of routes is to connect a URL with a controller action. For example, you can define the following route in config/routes.js to link GET /signup with new action of users controller:

map.get('signup', 'users#new');

The following route will link GET / to the index action of thehome controller:

map.root('home#index');

URL helpers

Another useful feature of routes: URL helpers. When you define route

map.get('bunny', 'bunny#show');

you can use pathTo.bunny in your controllers and views, which will generate

/bunny

path for you. You also can specify another helper name is you want:

map.get('bunny', 'bunny#show', {as: 'rabbit'});

and now pathTo.rabbit available.

If your route has param, for example

map.get('profile/:user', 'users#show');
map.get('posts/:post_id/comments/:comment_id', 'comments#show');

URL helper will accept parameter (String), so that:

pathTo.profile('Bugs_Bunny', 'users#show');
> '/profile/Bugs_Bunny'
pathTo.post_comment(2, 2383);
> '/posts/2/comments/2383'

Why use URL helpers?
First of all it's convenient and beauty. But what is more important url helpers take care about namespaces. In case if your application will be used as part of another application, mounted on some URL like "/foreign-app" URL helpers will return correct value: "/foreign-app/profile/Bugs_Bunny" instead of "/profile/Bugs_Bunny"

How to learn what helper name was generated? Read "Debugging" section.

Debugging

To debug routes of your compound application you can use compound routes command (or shortcut compound r). You can also specify optional argument for filtering by helper name or method, for example:

~: ) compound r post
     posts GET    /posts.:format?          posts#index
     posts POST   /posts.:format?          posts#create
  new_post GET    /posts/new.:format?      posts#new
 edit_post GET    /posts/:id/edit.:format? posts#edit
      post DELETE /posts/:id.:format?      posts#destroy
      post PUT    /posts/:id.:format?      posts#update
      post GET    /posts/:id.:format?      posts#show
~: ) compound r GET
     posts GET    /posts.:format?          posts#index
  new_post GET    /posts/new.:format?      posts#new
 edit_post GET    /posts/:id/edit.:format? posts#edit
      post GET    /posts/:id.:format?      posts#show
~: ) compound r new
 new_post GET    /posts/new.:format? posts#new

Resources

Resource-based routing provides standard mapping between HTTP verbs and controller actions:

map.resources('posts');

will provide the following routes:

  helper | method | path                   | controller#action
    posts GET      /posts                   posts#index
    posts POST     /posts                   posts#create
 new_post GET      /posts/new               posts#new
edit_post GET      /posts/:id/edit          posts#edit
     post DELETE   /posts/:id               posts#destroy
     post PUT      /posts/:id               posts#update
     post GET      /posts/:id               posts#show.

To list all available routes you can run the command compound routes.

The first column of the table represents the helper - you can use this identifier in views and controllers to get the route. Some examples:

path_to.new_post            # /posts/new
path_to.edit_post(1)        # /posts/1/edit
path_to.edit_post(post)     # /posts/1/edit (in this example post = {id: 1})
path_to.posts               # /posts
path_to.post(post)          # /posts/1.

Options

If you want to override default routes behaviour, you can use two options: as and path to specify a helper name and a path you want to have in the result.

{ as: 'helperName' }

Path helper aliasing:

map.resources('posts', { as: 'articles' });

This will create the following routes:

    articles GET    /posts.:format?          posts#index
    articles POST   /posts.:format?          posts#create
 new_article GET    /posts/new.:format?      posts#new
edit_article GET    /posts/:id/edit.:format? posts#edit
     article DELETE /posts/:id.:format?      posts#destroy
     article PUT    /posts/:id.:format?      posts#update
     article GET    /posts/:id.:format?      posts#show.

{ path: 'alternatePath' }

If you want to change the base path:

map.resources('posts', { path: 'articles' });

This will create the following routes:

    posts GET    /articles.:format?          posts#index
    posts POST   /articles.:format?          posts#create
 new_post GET    /articles/new.:format?      posts#new
edit_post GET    /articles/:id/edit.:format? posts#edit
     post DELETE /articles/:id.:format?      posts#destroy
     post PUT    /articles/:id.:format?      posts#update
     post GET    /articles/:id.:format?      posts#show

Both "as" and "path" together

If you want to alias both the helper and the path:

map.resources('posts', { path: 'articles', as: 'stories' });

This will create the following routes:

   stories GET    /articles.:format?          posts#index
   stories POST   /articles.:format?          posts#create
 new_story GET    /articles/new.:format?      posts#new
edit_story GET    /articles/:id/edit.:format? posts#edit
     story DELETE /articles/:id.:format?      posts#destroy
     story PUT    /articles/:id.:format?      posts#update
     story GET    /articles/:id.:format?      posts#show

Nested resources

Some resources may have nested sub-resources, for example Post has many Comments, and of course we want to get a post's comments using GET /post/1/comments.

Let's describe the route for our nested resource:

map.resources('post', function (post) {
  post.resources('comments');
});.

This routing map will provide the following routes:

$ compound routes
     post_comments GET      /posts/:post_id/comments          comments#index
     post_comments POST     /posts/:post_id/comments          comments#create
  new_post_comment GET      /posts/:post_id/comments/new      comments#new
 edit_post_comment GET      /posts/:post_id/comments/:id/edit comments#edit
      post_comment DELETE   /posts/:post_id/comments/:id      comments#destroy
      post_comment PUT      /posts/:post_id/comments/:id      comments#update
      post_comment GET      /posts/:post_id/comments/:id      comments#show
             posts GET      /posts                            posts#index
             posts POST     /posts                            posts#create
          new_post GET      /posts/new                        posts#new
         edit_post GET      /posts/:id/edit                   posts#edit
              post DELETE   /posts/:id                        posts#destroy
              post PUT      /posts/:id                        posts#update
              post GET      /posts/:id                        posts#show.

Using url helpers for nested routes

To use routes like post_comments you should call helper with param: parent resource or identifier before nested resource:

path_to.post_comments(post)               # /posts/1/comments
path_to.edit_post_comment(post, comment)  # /posts/1/comments/10/edit
path_to.edit_post_comment(2, 300)         # /posts/2/comments/300/edit

Namespaces

You may wish to organize groups of controllers under a namespace. The most common use-case is an administration area. All controllers within the admin namespace should be located inside the app/controllers/ directory.

For example, let's create an admin namespace:

map.namespace('admin', function (admin) {
  admin.resources('users');
});

This routing rule will match with /admin/users, /admin/users/new and will create appropriate url helpers:

    admin_users GET    /admin/users.:format?          admin/users#index
    admin_users POST   /admin/users.:format?          admin/users#create
 new_admin_user GET    /admin/users/new.:format?      admin/users#new
edit_admin_user GET    /admin/users/:id/edit.:format? admin/users#edit
     admin_user DELETE /admin/users/:id.:format?      admin/users#destroy
     admin_user PUT    /admin/users/:id.:format?      admin/users#update
     admin_user GET    /admin/users/:id.:format?      admin/users#show

Restricting routes

If you need routes only for several actions (e.g. index, show), you can specify the only option:

map.resources('users', { only: ['index', 'show'] });

If you want to have all routes except a specific route, you can specify the except option:

map.resources('users', { except: ['create', 'destroy'] });

Custom actions in resourceful routes

If you need some specific action to be added to your resource-based route, use this example:

map.resource('users', function (user) {
  user.get('avatar', 'users#avatar');               // /users/:user_id/avatar
  user.get('top', 'users#top', {collection: true}); // /users/top
});

Middleware

You may want to use middleware in routes. It's not recommended, but if you need it you can put it as second argument:

map.get('/admin', authenticate, 'admin#index');
map.get('/protected/resource', [ middleware1, middleware2 ], 'resource#access');

Subdomain

**experimental**

If you want to support subdomain filter, specify it as subdomain option:

map.get('/url', 'ctl#action', {subdomain: 'subdomain.tld'});

use \* as wildcard domain

map.get('/url', 'ctl#action', {subdomain: '*.example.com'});

This feature relies on host header, if your node process behind nginx or proxy, make sure you've passed this header to process.

Controllers

In CompoundJS, a controller is a module that receives user input and initiates a response. Controllers consists of a set of actions. Each action is called by the request of a particular route. To define an action, you should use the reserved global function action.

Features overview

Inside controllers you can use following reserved global functions to control response:

And here is a bunch of functions to control execution flow:

Let's learn more about each of this functions

Response control

NOTE: Each action should invoke exactly one output method. This is the only requirement imposed by the asynchronous nature of Node.js. If you don't call an output method, the client will infinitely wait for a server response.

render()

The render method accepts 0, 1 or 2 arguments. When called without any arguments, it just renders the view associated with this action. For example, this will render app/views/posts/index.ejs.

posts_controller.js
action('index', function () {
  render();
});

If you want to pass some data to the view, there are two ways to do it. The first is to simply pass a hash containing the data:

action('index', function () {
  render({ title: "Posts index" });
});

The second method is to set the properties of this:

action('index', function () {
  this.title = "Posts index";
  render();
});

If you want to render another view, just put its name as the first argument:

action('update', function () {
  this.title = "Update post";
  render('edit');
});

or:

action('update', function () {
  render('edit', { title: "Update post" });
});

send()

The send function is useful for debugging and one-page apps where you don't want to render a heavy template and just want to send text or JSON data.

This function can be called with a status code number:

action('destroy', function () {
  send(403); // client will receive statusCode = 403 Forbidden
});

or with a string:

action('sayHello', function () {
  send('Hello!'); // client will receive 'Hello!'
});

or with an object:

action('apiCall', function () {
  send({ hello: 'world' }); // client will receive '{"hello":"world"}'
});

redirect()

This function just sets the status code and Location header, so the client will be redirected to another location.

redirect('/'); // root redirection
redirect('http://example.com'); // redirect to another host

flash()

The flash function stores a message in the session to be displayed later. This is a regular expressjs function (refer to expressjs 2.0 guide to learn how it works). Here are a few examples:

posts_controller.js
action('create', function () {
    Post.create(req.body, function (err) {
        if (err) {
            flash('error', 'Error while post creation');
            render('new', {post: req.body});
        } else {
            flash('info', 'Post has been successfully created');
            redirect(path_to.posts);
        }
    });
});

This create action sends a flash info on success and a flash error on fail.

Execution flow control

To provide the ability of DRY-ing controller code and reusing common code parts, CompoundJS provides a few additional tools: method chaining and external controllers loading.

To chain methods, you can use the before and after methods.

checkout_controller.js
before(userRequired, { only: 'order' });
before(prepareBasket, { except: 'order' });
before(loadProducts, { only: ['products', 'featuredProducts'] });

action('products', function () { ... });
action('featuredProducts', function () { ... });
action('order', function () { ... });
action('basket', function () { ... });

function userRequired () { next() }
function prepareBasket () { next() }
function loadProducts () { next() }

In this example, userRequired will be called only for the order action, prepareBasket will be called for all actions except order, and loadProducts will be called only for the products and featuredProductsmethods.

Note, that the before-functions should call the global next method that will pass control to the next function in the chain.

Common execution context

There is one extra feature in flow control: All functions are invoked in the same context, so you can pass data between the functions using the this object:

function loadProducts () {
    Product.find(function (err, prds) {
        this.products = prds;
        next();
    }.bind(this));
}

action('products', function () {
    assert.ok(this.products, 'Products available here');
    render(); // also products will available in view
});

Sharing code across controllers

Some methods, like userRequired for example, can be used in different controllers. To allow cross-controller code sharing, CompoundJS provides a few methods: load, use and publish.

You can define requireUser in application_controller.js and call publish to make it accessible to all other controllers that inherit from this controller:

application_controller.js
publish('requireUser', requireUser);

function requireUser () {
  // ...
}
products_controller.js
load('application'); // note that _controller siffix omitted
before(use('userRequired'), { only: 'products' });

Other express.js features

To get familiar with CompoundJS controllers, look at a few examples available at github: coffee controller, javascript controller.

All other expressjs features have no global shortcuts yet, but they can still be used since request (alias req) and response (alias res) are available as global variables inside the controller context. In the view context, they are available as request and response.

Views

Templating engines

By default, CompoundJS uses ejs, but jade is also supported and can easily be enabled:

environment/development.js
app.set('view engine', 'jade')
npmfile.js
require('jade-ext');

View rendering flow

Every controller action can call the render method to display its associated view. For example, theindex action of theusers controller will render the view app/views/users/index.ejs.

This view will be rendered within the layout specified using the layout call in the controller. By default, the layout name is the same as the controller name, in this case app/views/layouts/users_layout.ejs. If this layout file does not exists, the application layout used.

If you need to render a view without a layout, you can call layout(false) inside of the controller, this will skip layout rendering.

Built-in helpers

linkTo

linkTo('Users index', '/users');
// <a href="/users">Users index</a>
linkTo('Users index', '/users', { class: 'menu-item' });
// <a href="/users" class="menu-item">Users index</a>
linkTo('Users index', '/users', { remote: true });
// <a href="/users" data-remote="true">Users index</a>

In the last example, the third argument is { remote: true }, and as you can see it will add a data-remote="true" attribute to the a tag. Clicking on this link will send an asynchronous GET request to /users. The result will be executed as Javascript.

Here you can also specify a jsonp parameter to handle the response:

linkTo('Users index', '/users', { remote: true, jsonp: 'renderUsers' });
// <a href="/users" data-remote="true" data-jsonp="renderUsers">Users index</a>

The server will send you a json{ users: [ {}, {}, {} ] }, and this object will be passed as an argument to the renderUsers function:

renderUsers({users: [{},{},{}]});

You can also specify an anonymous function in the jsonp param:

{ jsonp: '(function (url) { location.href = url; })' }

When server will send you "http://google.com/" following javascript will be evaluated:

(function (url) { location.href = url; })("http://google.com");

formFor

Accepts two params: resource, params and returns a form helper with the following helper functions:

An example:

<% var form = formFor(user, { action: path_to.users }); %>
<%- form.begin() %>

<%- form.label('name', 'Username') %> <%- form.input('name') %>
<%- form.submit('Save') %>

<%- form.end() %>

This will generate:

<form action="/users/1" method="POST">
  <input type="hidden" name="_method" value="PUT" />
  <input type="hidden" name="authenticity_token" value="RANDOM_TOKEN" />
  <p>
    <label for="name">Username</label>
    <input id="name" name="name" value="Anatoliy" />
  </p>
  <p>
    <input type="submit" value="Save" />
  </p>        
</form>

formTagBegin

This is the "light" version of the formFor helper which expects only one argument: params. Use this helper when you don't have a resource, but still want to be able to use simple method overriding and csrf protection tokens.

An example:

<%- formTagBegin({ action: path_to.users }); %>

<%- labelTag('First name', { name: 'name'}) %>
<%- inputTag('name', {value: 'Sascha'}) %>
<%- submitTag('Save') %>

<%- formTagEnd() %>

This will generate:

<form action="/users" method="POST">
  <input type="hidden" name="authenticity_token" value="RANDOM_TOKEN" />
  <p>
    <label for="name">Username</label>
    <input id="name" name="name" value="" />
  </p>
  <p>
    <input type="submit" value="Save" />
  </p>        
</form>

inputTag, form.input

To generate any tag use inputTag helper

<%- inputTag({name: 'creditCard', type: 'text', autocomplete: 'off'}) %>

This will procude

<input type="text" name="creditCard" autocomplete="off" />

When you have resource form object you can use shortcut version of this helper:

<%- form.input('name', {options}) %>

This helper doing the same job, but it takes in account value of resource passed to form, and specifies it as value="" html attribute:

<input name="name" value="Sascha" />

submitTag, form.submit

Same tags pair as inputTag and form.input, but for specific tag type: form submit button. Following example doing the same thing:

<%- submitTag('Submit data') %>
<%- form.submit('Submit data') %>

labelTag, form.label

Label tag

<%- labelTag('Text on label', {'for': 'attachedInput', style: 'font-size: 10px'}) %>
<%- form.label('attachedInput', 'Text on label', {style: 'font-size: 10px'}) %>

will both generate

<label for="attachedInput" style="font-size: 10px">Text on label</label>

One note about form.label('name'): when second argument is omitted and i18n is turned on, desired value from locale file used autimatically. For example we have ru.yml:

ru:
  models:
    User:
      fields:
        name: Имя пользователя

and form looks like

<% var form = formFor(user); %>
<%- form.label('name') %>

will create

<label for="name">Имя пользователя</label>

Hint: you can create en.yml with

en:
  models:
    User:
      fields:
        name: Name of user

and DRY your labels over the application views

stylesheetLinkTag

<%- stylesheetLinkTag('reset', 'style', 'mobile') %>

will generate

<link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/reset.css" />
<link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/style.css" />
<link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/mobile.css" />

javascriptIncludeTag

<%- javascriptIncludeTag(
  'https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js',
  'rails', 'application') %>

will generate

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> 
<script type="text/javascript" src="/javascripts/rails.js"></script>
<script type="text/javascript" src="/javascripts/application.js"></script>

By default, CompoundJS expects assets to be located in public/javascripts andpublic/stylesheets directories, but this settings can be overwritten in config/environment.js:

app.set('jsDirectory', '/js/');
app.set('cssDirectory', '/css/');

contentFor

Content for named section.

Called with one param acts as getter and returns all content pieces, collected before. Called with two params accumulates second param in named collection.

Examples:

In layout:

<%- contentFor('javascripts') %>

In view:

<% contentFor('javascripts', javascriptIncludeTag('view-specific')) %>

This will add some view-specific content to layout. This method also could be called from controller.

Defining your own helpers

You can define your own helpers for each controller in the file app/helpers/controllername_helper.js. For example, if you want to define a helper called my_helper to use it in the users controller, put the following in app/helpers/users_helper.js:

module.exports = {
  myHelper: function () {
    return "This is my helper!";
  }
}

The function myHelper can be now used by any of the views used by the users controller. Important thing: if you need to access controller object inside helper method, you can use this keyword. Inside helper method this points to controller context.

Models

By default models managed using [JugglingDB ORM](http://jugglingdb.co), but you can use any ORM you like. For example, if you prefer mongoose, check [mongoose on compound](https://github.com/anatoliychakkaev/mongoose-compound-example-app) example app.

Setup DB: config/database.js

Describe which database adapter you are going to use and how to connect with the database in config/database.js (.coffee, .json and .yml are also supported):

module.exports = {
  development:
  { driver:   "redis"
  , host:     "localhost"
  , port:     6379
  }
, test:
  { driver:   "memory"
  }
, staging:
  { driver:   "mongodb"
  , url:      "mongodb://localhost/test"
  }
, "production":
  { driver:   "mysql"
  , host:     "localhost"
  , post:     3306
  , database: "nodeapp-production"
  , username: "nodeapp-prod"
  , password: "t0ps3cr3t"
  }
}

Checkout the list of available adapters here. You can also specify the the adapter in the schema file using theschema method:

schema 'redis', url: process.env.REDISTOGO_URL, ->
    define 'User'
    # other definitions for redis schema

schema 'mongodb', url: process.env.MONGOHQ_URL, ->
    define 'Post'
    # other definitions for mongoose schema

All of these schemas can be used simultaneously and you can even describe relations between different schemas, for example User.hasMany(Post)

Define schema: db/schema.js

Use define to describe database entities and property to specify types of fields. This method accepts the following arguments:

JavaScript:

db/schema.js
var Person = define('Person', function () {
    property('email', { index: true });
    property('active', Boolean, { default: true });
    property('createdAt', Date);
});

var Book = define('Book', function () {
    property('title');
    property('ISBN');
});

CoffeeScript:

db/schema.coffee
Person = define 'Person', ->
    property 'email', index: true
    property 'active', Boolean, default: true
    property 'createdAt', Date, default: Date
    property 'bio', Text
    property 'name'

Book = define 'Book', ->
    property 'title'
    property 'ISBN'

or define a custom schema (non-juggling), fo example mongoose. Please note that in case of a custom schema, JugglingDB features will not work.

customSchema(function () {
    var mongoose = require('mongoose');
    mongoose.connect('mongodb://localhost/test');

    var Schema = mongoose.Schema, ObjectId = Schema.ObjectId;

    var BlogPost = new Schema({
        author    : ObjectId
        , title     : String
        , body      : String
        , date      : Date
    });

    var Post = mongoose.model('BlogPost', BlogPost);
    Post.modelName = 'BlogPost'; // this is for some features inside compound (helpers, etc)

    module.exports['BlogPost'] = Post;
});

Implement: app/models/name

Describe models

Models should be described in app/models/modelname.js files. Each model file should export function, which accepts two arguments:

module.exports = function(compound, ModelName) {

  ModelName.classMethod = function classMethod() {
    return 'hello from class method';
  };

  ModelName.prototype.instanceMethod = function instanceMethod() {
    return 'hello from instance method';
  };
};

If you need initialize database-independent model in model file, disregard second param of exported function, use this example:

module.exports = function(compound) {
  // define class
  function MyModel() {
    this.prop = '';
  }

  MyModel.prototype.method = function() {};

  // register model in compound
  compound.models.MyModel = MyModel;

  // optionally specify modelname (used in view helpers)
  MyModel.className = 'MyModel';
};

Describe relations

Currently, only a few relations are supported: hasManyand belongsTo

User.hasMany(Post,   {as: 'posts',  foreignKey: 'userId'});
// creates instance methods:
// user.posts(conds)
// user.posts.build(data) // like new Post({userId: user.id});
// user.posts.create(data) // build and save
// user.posts.find

Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
// creates instance methods:
// post.author(callback) -- getter when called with function
// post.author() -- sync getter when called without params
// post.author(user) -- setter when called with object

It's also possible to use scopes inside hasMany associations, for example if you have a scope for Post:

Post.scope('published', { published: true });

 Which is just a shortcut for the all method:

Post.published(cb); // same as Post.all({ published: true });

So you can use it with an association:

user.posts.published(cb); // same as Post.all({ published: true, userId: user.id });

Setup validations

Validations invoked after create, save and updateAttributes can also be skipped when using save:

obj.save({ validate: false });

Validations can be called manually by calling isValid() on the object.

After the validations are called, the validated object contains an errors hash containing error message arrays:

{
    email: [
        'can\'t be blank',
        'format is invalid'
    ],
    password: [ 'too short' ]
}

If you want your validations to raise exceptions, just call save like this:

obj.save({ throws: save });

To define a validation, call its configurator on your model's class:

Person.validatesPresenceOf('email', 'name');
Person.validatesLengthOf('password', { min: 5 });

Each configurator accepts a set of string arguments and an optional last argument representing the settings for the validation. Here are some common options:

if and unless can be strings or functions returning a boolean that defines whether a validation is being called. The functions are invoked in the resource context which means that you can access the resource properties using this.propertyName.

message allows you to define an error message that is being displayed when the validation fails.

Available validators

length

User.validatesLengthOf 'password', min: 3, max: 10, allowNull: true
User.validatesLengthOf 'state', is: 2, allowBlank: true
user = new User validAttributes

user.password = 'qw'
test.ok not user.isValid(), 'Invalid: too short'
test.equal user.errors.password[0], 'too short'

user.password = '12345678901'
test.ok not user.isValid(), 'Invalid: too long'
test.equal user.errors.password[0], 'too long'

user.password = 'hello'
test.ok user.isValid(), 'Valid with value'
test.ok not user.errors

user.password = null
test.ok user.isValid(), 'Valid without value'
test.ok not user.errors

user.state = 'Texas'
test.ok not user.isValid(), 'Invalid state'
test.equal user.errors.state[0], 'length is wrong'

user.state = 'TX'
test.ok user.isValid(), 'Valid with value of state'
test.ok not user.errors

numericality

User.validatesNumericalityOf 'age', int: true
user = new User validAttributes

user.age = '26'
test.ok not user.isValid(), 'User is not valid: not a number'
test.equal user.errors.age[0], 'is not a number'

user.age = 26.1
test.ok not user.isValid(), 'User is not valid: not integer'
test.equal user.errors.age[0], 'is not an integer'

user.age = 26
test.ok user.isValid(), 'User valid: integer age'
test.ok not user.errors

inclusion

User.validatesInclusionOf 'gender', in: ['male', 'female']
user = new User validAttributes

user.gender = 'any'
test.ok not user.isValid()
test.equal user.errors.gender[0], 'is not included in the list'

user.gender = 'female'
test.ok user.isValid()

user.gender = 'male'
test.ok user.isValid()

user.gender = 'man'
test.ok not user.isValid()
test.equal user.errors.gender[0], 'is not included in the list'

exclusion

User.validatesExclusionOf 'domain', in: ['www', 'admin']
user = new User validAttributes

user.domain = 'www'
test.ok not user.isValid()
test.equal user.errors.domain[0], 'is reserved'

user.domain = 'my'
test.ok user.isValid()

format

User.validatesFormatOf 'email', with: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
user = new User validAttributes

user.email = 'invalid email'
test.ok not user.isValid()

user.email = 'valid@email.tld'
test.ok user.isValid()

Models reloading

In development env models automatically reloaded on file changing. If you don't need this behavior and prefer restart webserver you can turn off this setting in config/environments/development.js:

app.disable('watch');

It also disables controllers reloading.

REPL console

To run the REPL console use this command:

compound console

or its shortcut:

compound c

The REPL console is just a simple Node.js console with some CompoundJS, for example models.

Just one note on working with the console: Node.js is asynchronous by nature which makes console debugging much more complicated, since you have to use a callback to fetch results from the database for instance. We have added one useful method to simplify asynchronous debugging using the REPL console. It's called c and you can pass it as a parameter to any function that requires a callback. It will store the parameters passed to the callback to variables called _0, _1, ..., _N where N is the length of arguments.

Example:

compound c
compound> User.find(53, c)
Callback called with 2 arguments:
_0 = null
_1 = [object Object]
compound> _1
{ email: [Getter/Setter],
  password: [Getter/Setter],
  activationCode: [Getter/Setter],
  activated: [Getter/Setter],
  forcePassChange: [Getter/Setter],
  isAdmin: [Getter/Setter],
  id: [Getter/Setter] }

Localization

Basic steps:

CompoundJS allows you to create localized applications: Just place a YAML-formatted file to config/locales directory:

config/locales/en.yml
en:
  session:
    new: "Sign in"
    destroy: "Sign out"
  user:
    new: "Sign up"
    destroy: "Cancel my account"
    welcome: "Hello %, howdy?
    validation:
      name: "Username required"

NOTE: Translations can contain % symbol(s) for variable substitution.

Define a user locale before filter to your application controller:

app/controllers/application_controller.js
before(setUserLocale);
function setUserLocale () {
    // define locale from user settings, or from headers or use default
    var locale = req.user ? req.user.locale : 'en';
    // call global function setLocale
    setLocale(locale);
}

And use localized tokens inside your app views using the t helper:

<%= t('session.new') %>
<%= t('user.new') %>
<%= t(['user.welcome', user.name]) %>

You can also use the t helper in controllers:

flash('error', t('user.validation.name'));

or in models:

return t('email.activate', 'en')

NOTE: When you use the t helper in models, you have to pass the locale as the second parameter.

Configuration

Localization behavior can be configured using the following settings:

Example:

app.configure(function () {
    app.set('defaultLocale', 'en');
});

app.configure('development', function(){
    app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
    app.set('translationMissing', 'display');
});

app.configure('production', function () {
    app.use(express.errorHandler()); 
    app.set('translationMissing', 'default');
});

Generators

CompoundJS generators are automated tools that allow you to create a bunch of files automatically. Each generator can be run via:

compound generate GENERATOR_NAME

or using the shortcut:

compound g GENERATOR_NAME

Built-in generators are: model, controller, scaffold (alias: crud), clientside

Generate model

Use case: You just need a model and schema.

Example:

compound g model user email password approved:boolean

Generated files:

exists  app/
exists  app/models/
create  app/models/user.js
patch   db/schema.js

The generated model file contains the following code:

module.exports = function (compound, User) {
  // define User here
};

The patched schema file contains the following code:

var User = describe('User', function () {
    property('email', String);
    property('password', String);
    property('approved', Boolean);
});

Generate controller

Use case: You don't need a standard RESTful controller, just a few non-standard actions.

Example:

compound g controller controllername actionName otherActionName

Generated files:

exists  app/
exists  app/controllers/
create  app/controllers/controllername_controller.js
exists  app/helpers/
create  app/helpers/controllername_helper.js
exists  app/views/
create  app/views/controllername/
create  app/views/controllername/actionName.ejs
create  app/views/controllername/anotherActionName.ejs

The generated controller file contains the following code:

load('application');

action("actionName", function () {
    render();
});

action("anotherActionName", function () {
    render();
});

Generate scaffold (crud)

The most commonly used generator. It creates a ready-to-use resource controller with all needed actions, views, schema definitions, routes and tests. Compound can also generate scaffolds in CoffeeScript.

Example call:

compound g scaffold post title content createdAt:date
exists  app/
exists  app/models/
create  app/models/post.js
exists  app/
exists  app/controllers/
create  app/controllers/posts_controller.js
exists  app/helpers/
create  app/helpers/posts_helper.js
create  app/views/layouts/posts_layout.ejs
create  public/stylesheets/scaffold.css
exists  app/views/
create  app/views/posts/
create  app/views/posts/_form.ejs
create  app/views/posts/new.ejs
create  app/views/posts/edit.ejs
create  app/views/posts/index.ejs
create  app/views/posts/show.ejs
patch   config/routes.js

Clientside

For using compound on clientside we have to create application bundle. This bundle then could be passed to browserify to create full bundle (application + framework + dependencies). This generator allows to create bundle.

# create full bundle (./public/javascripts/compound.js)
compound generate clientside

# create full bundle and regenerate on changes in any file
compound generate clientside --watch

# create full bundle and force quit after completion
compound generate clientside --quit

Use shortcuts to save your time:

compound g cs --watch

Compound API

This chapter describes internal API of compound application. Compound app designed as npm module that can be used as part of other modules.

Main entry point called server.js exports function for creating application. This function returns regular express application with one addition: compound object. This is object we are talking about. It contains some information about application, such as root directory path, MVC structure, models. Read this chapter to get familiar with this powerful tool.

Compound app

Let's start with the entry point, called server.js by default. If you want to rename it, update package.json with "main": "server.js" line. The purpose of that file: publish function that creates application. This function can create many instances of application which could be configured and used separately:

// load package
var instantiateApp = require('.');

// create different instances
var app1 = instantiateApp();
var app2 = instantiateApp(params);

// run on different ports/hosts
app1.listen(3000);
app2.listen(3001, '10.0.0.2', callback);

Instantiation method accepts optional hash of params. These params hash will be passed to express.

Tools

The compound.tools hash contains commands that can be invoked using the command line, for example compound routes will call compound.tools.routes() .

To write a tool, just add another method to the compound.tools object, the method name will become the command name:

compound.tools.database = function () {
    switch (compound.args.shift()) {
    case 'clean':
        // clean db
        break;
    case 'backup':
        // backup db
        break;
    case 'restore':
        // restore db
        break;
    default:
        console.log('Usage: compound database [clean|backup|restore]');
    }
};

Then the following commands will be available:

compound database
compound database backup
compound database clean
compound database restore

If you want to see this command when using compound helpyou can provide some information about the tool using the helphash:

compound.tools.db.help = {
    shortcut: 'db',
    usage: 'database [backup|restore|clean]',
    description: 'Some database features'
};

The next time you call compound, you will see:

Commands:
  ...
  db, database [backup|restore|clean]  Some database features

If you defined a shortcut, it can be used instead of the full command name:

compound db clean

To learn more, please check out the sources: lib/tools.js

Generators

Coming soon. It's about the compound.generators module and the compound generate commands.

Structure

Coming soon. This chapter about compound.structure api, overwriting internals of compound app without touching source code.

Extensions

Any npm package can be used as an extension for CompoundJS. If it should be loaded at compound app startup, it should export init method. This method will receive single argument: compound app.

Compound will initialize each extension listed in config/autoload.js file. Example of file generated on compound init command:

module.exports = function(compound) {
    return [
        require('ejs-ext'),
        require('jugglingdb'),
        require('seedjs')
    ];
};

We are trying to keep compound core tiny, some parts of framework now moved to separate modules:

Some of the modules still loaded from core, but in future everything will be moved to config/autoload. It means that every part of compound can be replaced with another module that should follow common API.

Heroku

Heroku's Node.js hosting is available for public use now. Deploying a CompoundJS application is as simple as git push.

To work with heroku you also need ruby as well as the heroku gem.

Deploying an application

First of all, create an application:

compound init heroku-app
cd heroku-app
sudo npm link
compound g crud post title content

Then initialize a git repository:

git init
git add .
git commit -m 'Init'

Create a Heroku application:

heroku create --stack cedar

Want to use MongoDB?

heroku addons:add mongohq:free

Want to use Redis?

heroku addons:add redistogo:nano

And deploy:

git push heroku master

Hook up Procfile (only once):

heroku ps:scale web=1

Check application state:

heroku ps

Visit your application:

heroku open

If something went wrong, you can check out the logs:

heroku logs

To access the CompoundJS REPL console, do:

heroku run compound console

MongoHQ provides a web interface for browsing your MongoDB database, to use it go to http://mongohq.com/, create an account, then click "Add remote connection" and configure the link to your database. You can retrieve details required for the connection using this command:

heroku config --long

Code snippets

Multiple workers compound server (node 0.8.16)

Example in CoffeeScript:

server.coffee
#!/usr/bin/env coffee

app = module.exports = (params) ->
  params = params || {}
  # specify current dir as default root of server
  params.root = params.root || __dirname
  return require('compound').createServer(params)

cluster = require('cluster')
numCPUs = require('os').cpus().length

if not module.parent
  port = process.env.PORT || 3000
  host = process.env.HOST || "0.0.0.0"
  server = app()
  if cluster.isMaster
    # Fork workers.
    cluster.fork() for i in [1..numCPUs]

    cluster.on 'exit', (worker, code, signal) ->
      console.log 'worker ' + worker.process.pid + ' died'
  else
    server.listen port, host, ->
      console.log(
        "Compound server listening on %s:%d within %s environment",
        host, port, server.set('env'))

Redis session store for Heroku deployment with redistogo addon

Hook the REDISTOGO_URL environment variable in config/environment.js and pass it to the RedisStore constructor. Example in CoffeeScript:


module.exports = (compound) ->

  express = require 'express'
  RedisStore = require('connect-redis')(express)

  if process.env['REDISTOGO_URL']
    url = require('url').parse(process.env['REDISTOGO_URL'])
    redisOpts =
      port: url.port
      host: url.hostname
      pass: url.auth.split(':')[1]
  else
    redisOpts = {}

  app.configure ->
    app.use compound.assetsCompiler.init()
    app.enable 'coffee'

    app.set 'cssEngine', 'stylus'

    app.use express.static(app.root + '/public', {maxAge: 86400000})
    app.use express.bodyParser()
    app.use express.cookieParser()
    app.use express.session secret: 'secret', store: new RedisStore(redisOpts)
    app.use express.methodOverride()
    app.use app.router

Upload file to compound server

var form = require('connect-form-sync');
app.configure(function(){
    ....
    app.use(form({ keepExtensions: true }));
    app.use(express.bodyParser());
    ....
});

And use it in the controller like that:

action('create', function () {
    this.file = new File();
    var tmpFile = req.form.files.file;
    this.file.upload(tmpFile.name, tmpFile.path, function (err) {
        if (err) {
            console.log(err);
            this.title = 'New file';
            flash('error', 'File can not be created');
            render('new');
        } else {
            flash('info', 'File created');
            redirect(path_to.files);
        }
    }.bind(this));
});

Documentation is maintained by @1602 (Anatoliy Chakkaev) and @rattazong (Sascha Gehlich).

CompoundJS is licensed under the MIT License. Documentation is licensed under CC BY 3.0.

The CompoundJS and JugglingDB projects are free, but you can leave a tip here: