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