CompoundJS - MVC framework
Build well-organized web apps using CompoundJS.
JugglingDB: try in your browser

What is compound?

CompoundJS is the Node.JS MVC framework based on ExpressJS, fully ExpressJS-compatible. It allows you to build well-structured web applications.

The main objective of the framework - web development without pain.

Related links

1. Install

sudo npm install compound -g

2. Use

You can use app generator and the scaffold generator to build your first CompoundJS app in seconds

compound init blog && cd blog
npm install -l
compound generate crud post title content
compound server 8888
open http://127.0.0.1:8888/posts

What is under the hood

The purpose of routes is to connect URL with controller action. For example you can define the following route in config/routes.js file:

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

to link GET /signup with new action of users controller.

map.root('home#index');

Will link GET / to index action of home controller

Resource-based routing

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

map.resources('posts');

will provide 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 available routes you can run command compound routes.

First column in table is helper - you can use this identifier in views and controllers to get 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

Aliases for resourceful routes

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

{ as: 'helperName' }

Path helper aliasing:

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

will produce

     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 base path:

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

this will create 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

all together

And if you want alias both helper and path, use both:

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

and you will get:

    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 comments of the post using GET /post/1/comments

Let's describe route for 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 Admin area. All controllers within admin namespace should be located inside app/controllers/ dir.

For example, let's create admin namespace:

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

This routing rule will match with urls /admin/users, admin/users/new, and create appropriated 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 only option:

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

If you want to have all routes except some route, you can specify except option:

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

Custom actions in resourceful routes

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

map.resources('users', function (user) {
    user.get('avatar', 'users#avatar');
});

Synopsis

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

action('index', function () {
    render();
});

Features overview

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

  • render - render view template related to this action
  • send - send to client text, status code or json object
  • redirect - redirect client to specific location
  • header - send header to client
  • flash - display flash message

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

  • before - invoke this method before any action
  • after - invoke this method after any action
  • load - load another controller to use it methods
  • use or export - get method defined in another controller, loaded with load function
  • publish or import - allow method to be used in other controller

Let's learn more about each of this functions.

Response control

NOTE: each action should invoke one output method. This is the only requirement imposed by the asynchronous nature Node.JS. If output method wouldn't called, client will wait for server responce indefinitely.

render

render method accepts 0, 1, or 2 arguments. Called without agruments it just render view associated with this action. For example:

posts_controller.js

action('index', function () {
    render();
});

will render view app/views/posts/index.ejs.

If you want to pass some data to the view, there are two ways to do it. First:

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

and second:

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

And if you want to render another view, just put it's name as first argument:

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

or

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

send

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

This function can be called with number (status code):

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

or with string:

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

or with object:

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

redirect

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

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

flash

Flash function stores message in session for future displaying, this is regular expressjs function, refer to expressjs guide to learn how it works. 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 flash info on success and flash error on fail.

Execution flow control

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

To chain methods you can use 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 function userRequired will called only for order action, prepareBasket function will called for all actions except order, and loadProducts function will called only for actions products and featuredProducts.

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

Common execution context

There is one implicit feature in chaining: all functions in chain invoked in one context, so you can pass data between chain using 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 between controllers

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

You can define requireUser in application_controller.js and call publish, and then you will be able to use it in your controller:

application_controller.js

publish('requireUser', requireUser);
function requireUser () {
}

then load application controller and use requireUser function here:

products_controller.js

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

Other express.js features

To get familiar with compound controllers look to few examples available at github: coffee controller, javascript controller

All other expressjs features have no global shortcuts yet, but they still can be used, because object request (or, shorter req) and response (alias res), are available as global variables in controller context. In view context only available long aliases: response and request to provide better coding style (it is bad to use this variables in views, but it's possible).

Templating engines

By default railway use ejs templating engine, but you can switch to jade, using settings

app.set('view engine', 'jade');

and add line

require('jade-ext');

to ./npmfile.js

View rendering flow

Every controller action may call render method to display associated view. For example, action index in controller users will render view in file app/views/users/index.ejs.

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

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

Built-in Helpers

  • link_to
link_to('Users index', '/users');
// <a href="/users">Users index</a>
link_to('Users index', '/users', {class: 'menu-item'});
// <a href="/users" class="menu-item">Users index</a>
link_to('Users index', '/users', {remote: true});
// <a href="/users" data-remote="true">Users index</a>
  • First argument is link text.
  • Second argument is link path.
  • Third argument is object with link properties.

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

Here you can also specify jsonp param to handle response:

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

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

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

You can also specify anonymous function in 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");

This is why you should wrap anonimous function with parentheses.

  • form_for

Accepts three params: resource, params and block. Block is function that receives as single param special object, which provide helpers with pre-filled values. Available helpers:

  • input
  • label
  • textarea
  • submit
  • checkbox

    <% form_for(user, {action: path_to.users}, function (form) { %>

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

    <%- form.submit('Save') %>

    <% }); %>

will generate

<form action="/users/1" method="POST">
    <input type="hidden" name="_method" value="PUT" />
    <input type="hidden" name="authencity_token" value="qwertyuiop1234567890!@#$%^&*()" />
   <p>
       <label for="name">Username</label>
       <input id="name" name="name" value="Anatoliy" />
   </p>
   <p>
       <input type="submit" value="Save" />
   </p>
</form>
  • form_tag

This is "light" version of form_for helper. It accepts just two arguments: params and block. Block didn't receives any params. Use this helper when you have no any resource, but still want to be able to use simple method overriding and csrf protection tokens. TODO: add code sample

  • input_tag and form.input

TODO: describe input_tag

  • label_tag and form.label

TODO: describe label_tag

  • stylesheet_link_tag
<%- stylesheet_link_tag('reset', 'style', 'mobile') %>

will generate

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

Timestamps ?1306993455523 added to assets only in development mode in order to prevent browser from caching scripts and style in client-side

  • javascript_include_tag
<%- javascript_include_tag(
  '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?1306993455524"></script>
<script type="text/javascript" src="/javascripts/application.js?1306993455524"></script>

Timestamps ?1306993455524 added to assets only in development mode in order to prevent browser from caching scripts and style in client-side

By default compound expect assets to be located in public/javascripts and public/stylesheets directories, but this settings can be overwritten in config/environment.js file:

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

Defining your own helpers

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

module.exports = {
  my_helper: function () {
    return "This is my helper!";
  }
}
The function `my_helper` can be now used by any of the views used by the `users` controller.

Meet ORM: JugglingDB

1. Configure database

Describe which database adapter you going to use and how to connect with database in `config/database.json` file
{ "development":
  { "driver":   "redis"
  , "host":     "localhost"
  , "port":     6379
  }
, "test":
  { "driver":   "memory"
  }
, "staging":
  { "driver":   "mongoose"
  , "url":      "mongodb://localhost/test"
  }
, "production":
  { "driver":   "mysql"
  , "host":     "localhost"
  , "post":     3306
  , "database": "nodeapp-production"
  , "username": "nodeapp-prod"
  , "password": "t0ps3cr3t"
  }
}

Checkout list of available adapters in this test. Also possible to specify which database to use to directly in schema, using schema method:

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

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

All of this schemas will work at the same time, and you even can describe relationships between different schemas, such as User.hasMany(Post), cool, eh? This is why it called JugglingDB.

2. Define schema

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

  • name of property
  • type: Date, Number, Boolean, Text, String (can be omitted if String)
  • options: Object {default: 'default value', index: true}

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 custom schema (non-juggling), for example, mongoose. Please note, in case of custom schema all jugglingdb features of course will be disabled.

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;
});

3. Describe relations

Currently supported only few relations: hasMany, belongsTo,
of course set of possible relations between object will grow
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 also possible to use scopes inside has many associations, for example if you have scope for Post:

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

which is primarily just creates shortcut caller for all method:

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

So, you can use it with association:

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

4. Setup validations

Currenty supported validations:

  • presence
  • length
  • numericality
  • inclusion
  • exclusion
  • format

Validations invoked after create, save and updateAttributes, it also possible to skip validations when use save method:

obj.save({validate: false});

Validations can be called manually using isValid method of object

Normally all validations result in errors member of object, which is a hash of arrays of error messages:

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

It also can raise exception, if you want, just pass throws: true as param of save method:

// be carefull, now it will throw Error object
obj.save({throws: true});

To setup validation, call it configurator on class:

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

Each configurator accepts set of string arguments, and one optional last argument, which is actually specify how validator should work, of course it depends on each validator, but there's few common options:

  • if
  • unless
  • message
  • allowNull
  • allowBlank

if and unless methods is for skipping validations depending on conditions, it can be function, or string. Function invoked in context of object, where validation performed. If string passed, then one of object's member checked.

message member allows to configure error message, it can be string or object (depends on validator), see usage examples

allowNull and allowBlank is self explanatory :)

Examples of different types of validations:

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()

defining additional model functions

You can define additional functions to your models by editing the file app/models/modelname.js:

User.getActiveUsers = function getActiveUsers(callback) {
  Users.all({active: true}, callback);
}

User.prototype.getFullName = function getFullName() {
  return return [this.firstName, this.lastName].join(' ');
};

Functions that need access to a specific User instance need to be declared in the prototype, otherwise they will not be available.

CompoundJS generators - are automated tools allows you to create bunch of files automatically.

Each generator can be invoked by command

compound generate GENERATOR_NAME

or, using shortcut

compound g GENERATOR_NAME
Built-in generators: model, controller, scaffold (crud)

Generate controller

Usecase: you do not need standart RESTful controller, just few non-standart actions

Example call:

compound g controller controllername actionName anotherActionName

Generate 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

Generated contents of controller file:

load('application');

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

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

Generate model

Usecase: you want to access some database entity using ORM interface (mongoose by default)

Example call:

compound g model user name password

Will generate

exists  app/
exists  app/models/
create  app/models/user.js

Following mongoose schema will be created:

/**
 * User
 */
var UserSchema = new Schema;
UserSchema.add({
    name: { type: String },
    password: { type: String }
});
mongoose.model("User", UserSchema);
module.exports["User"] = mongoose.model("User");

If you need to specify field types (String by default):

compound g model user name password createdAt:date activated:boolean

Generate scaffold (crud)

The most commonly used generator. It creates ready-to-use resource controller with all actions, views, schema definitions, routes, and tests. RW also can generated scaffold 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

Using scaffold generator - fastest way to create prototype of application.

REPL console Learn, debug, investigate

To run REPL console use command

compound console

or it's shortcut

compound c

It just simple node-js console with some CompoundJS bindings, e.g. models. Just one note about working with console. Node.js is asynchronous by its nature, and it's great but it made console debugging much more complicated, because you should use callback to fetch result from database, for example. I have added one useful method to simplify async debugging using compound console. It's name c, you can pass it as parameter to any function requires callback, and it will store parameters passed to callback to variables _0, _1, ..., _N where N is index in 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] }

Write your app in CoffeeScript Drink coffee — do stupid things faster with more energy

Almost all parts of app can be written in CoffeeScript. If you like coding in Coffee, please do. Just put --coffee option to compound commands.

compound init blog --coffee
cd blog
npm install -l
compound g scaffold post title content --coffee

And then you can run compound server or coffee server.coffee to have server running on 3000 port

For example, here is generated coffee-controller:

before ->
    Post.findById req.params.id, (err, post) =>
        if err or not post
            redirect path_to.posts
        else
            @post = post
            next()
, only: ['show', 'edit', 'update', 'destroy']

# GET /posts/new
action 'new', ->
    @post = new Post
    render
        title: 'New post'

# POST /posts
action 'create', ->
    @post = new Post
    ['title', 'content'].forEach (field) =>
        @post[field] = req.body[field] if req.body[field]?

    @post.save (errors) ->
        if errors
            flash 'error', 'Post can not be created'
            render 'new',
                title: 'New post'
        else
            flash 'info', 'Post created'
            redirect path_to.posts

# GET /posts
action 'index', ->
    Post.find (err, posts) ->
        render
            posts: posts
            title: 'Posts index'

# GET /posts/:id
action 'show', ->
    render
        title: 'Post show'

# GET /posts/:id/edit
action 'edit', ->
    render
        title: 'Post edit'

# PUT /posts/:id
action 'update', ->
    ['title', 'content'].forEach (field) =>
        @post[field] = req.body[field] if req.body[field]?

    @post.save (err) =>
        if not err
            flash 'info', 'Post updated'
            redirect path_to.post(@post)
        else
            flash 'error', 'Post can not be updated'
            render 'edit',
                title: 'Edit post details'

# DELETE /posts/:id
action 'destroy', ->
    @post.remove (error) ->
        if error
            flash 'error', 'Can not destroy post'
        else
            flash 'info', 'Post successfully removed'
        send "'" + path_to.posts + "'"

Basic steps:

  1. create dictionary to translate tokens to natural language (config/locales/*.yml)
  2. Use tokens instead of natural language everywhere in app (t helper)
  3. Manually detect language of each user request (setLocale method)

CompoundJS allows you to create application translated to different languages: just place localization file in yaml format to config/locales dir:

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), that means variable substitution

Define user locale in 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 app views with t helper:

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

or inside controllers:

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

or inside models:

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

NOTE: when use t helpers inside models you must pass locale as second param to t helper.

Configuration

Localization behavior can be configured using following settings:

  • defaultLocale: name of default locale
  • translationMissing: what action perfor when translation is missing, possible values: default - display translation for default locale, display - show error like "Translation missing for email.activate"

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');
});

Any npm package is already extension for compound. Just put line to npmfile.js, for example:

require('railway-twitter');

Just one note: if package have init method, it will be invoked after application initialization.

For example you can check out twitter-auth extension for compound.

Installation script

If your extension have install.js script in the root, it will be invoked after installation with compound install command.

This script will be executed in application context, i.e. you can access global variables app and compound. You must call process.exit() when installation process has been completed.

CompoundJS extension API

All compound modules published as global singleton railway. Any module can be extended or monkey-patched. Let's take a look to most common use-cases.

Tools

Hash railway.tools contains commands that can be invoked using command line, for example compound routes command will call railway.tools.routes() method.

To write tool, just add method to railway.tools object, name of this method will become the command:

railway.tools.database = function () {
    switch (railway.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 for call:

compound database
compound database backup
compound database clean
compound database restore

If you want to see this command in help message compound help you can provide some information about tool using help hash:

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

Next time you will call compound help you'll see:

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

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

 railway db clean

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

Generators

Coming soon. It's about railway.generators module and compound generate smth family of commands.

Discussion in google groups

API is still in develop now, feel free to leave comments about it in the related google groups thread.

Heroku nodejs hosting is available for public using now. You can deploy your CompoundJS application as simple as git push.

To work with heroku you also need: ruby, heroku gem

First of all, create app

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

Then init git repo

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

Create heroku app

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 app

heroku open

If something went wrong, you can check out logs

heroku logs

But for me it useless. Better to run commands in node console:

heroku run node
> var app = require('compoundjs').createServer();

It helps to identify exact problems with app installation. Also you can access compound console using command

heroku run compound console

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

heroku config --long

Multiple workers compound server (node 0.6.0)

Example in coffeescript: server.coffee

#!/usr/bin/env coffee

app = module.exports = require('compoundjs').createServer()

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

port = process.env.PORT or 3000

if not module.parent
    if cluster.isMaster
        # Fork workers.
        cluster.fork() for i in [1..numCPUs]

        cluster.on 'death', (worker) ->
            console.log 'worker ' + worker.pid + ' died'
    else
        # Run server
        app.listen port
        console.log "CompoundJS server listening on port %d within %s environment", port, app.settings.env

Redis session store for Heroku deployment with redistogo addon

Hook REDISTOGO_URL environment variable in config/environment.js and pass it to RedisStore constructor.

var express    = require('express'),
    RedisStore = require('connect-redis')(express);

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

app.configure(function(){
    var cwd = process.cwd();
    app.use(express.static(cwd + '/public', {maxAge: 86400000}));
    app.set('views', cwd + '/app/views');
    app.set('view engine', 'ejs');
    app.set('jsDirectory', '/javascripts/');
    app.set('cssDirectory', '/css/');
    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 in 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));
});