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.
sudo npm install compound -g
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
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 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
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.
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
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
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
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
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
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
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']});
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');
});
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();
});
Inside controller you can use following reserved global functions to control response:
And here is a bunch of functions to control execution flow:
load functionLet's learn more about each of this functions.
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 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 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"}'
});
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 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.
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.
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
});
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'});
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).
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
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.
link_tolink_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>
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_forAccepts 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:
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_tagThis 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.inputTODO: describe input_tag
label_tag and form.labelTODO: 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/');
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.
{ "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.
Use define method to describe database entities, and property method to specify types of fields.
This method acceps 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 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;
});
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});
Currenty supported validations:
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 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:
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
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
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'
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()
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()
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.
Each generator can be invoked by command
compound generate GENERATOR_NAME
or, using shortcut
compound g GENERATOR_NAMEBuilt-in generators: model, controller, scaffold (crud)
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();
});
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
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.
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] }
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:
config/locales/*.yml)t helper)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.
Localization behavior can be configured using following settings:
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.
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.
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.
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
Coming soon. It's about railway.generators module and compound generate smth family of commands.
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
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
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);
});
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));
});