A Node.js toolkit for building Minimum Viable Products
This toolkit lets you focus on the real, "business" code of your app. Instead of worrying about which database to use, how to structure your components, or how to manage dependencies, you can just start coding.
You write everything as a command. Your commands get called whenever they match a set of properties. Your calling code doesn't know, or care, which command gets the work done. A JavaScript object goes in, and one comes out, asynchronously.
For practical details, visit the Seneca github project page.
Here's a command that sums two numbers:
var seneca = require('seneca')()
seneca.add( {role:'math', cmd:'sum'}, function(args,callback) {
var sum = args.left + args.right
callback(null,{answer:sum})
})
seneca.act( {role:'math', cmd:'sum', left:1, right:2}, function(err,result) {
if( err ) return console.error( err )
console.log(result)
})
seneca.add adds a new command. seneca.act
acts on the provided pattern, running a command if it matches. Try the code:
$ node example.js
{ answer: 3 }
You registered the pattern {role:'math', cmd:'sum'}. The
input matched, and your code ran. The left and right arguments were
then used by your plugin to do some real work.
Let's add another command:
seneca.add( {role:'math', cmd:'product'}, function(args,callback) {
var product = args.left * args.right
callback(null,{answer:product})
})
seneca.act( {role:'math', cmd:'product', left:3, right:4},
function(err,result) {
if( err ) return console.error( err )
console.log(result)
})
$ node example.js
{ answer: 3 }
{ answer: 12 }
You can add new commands whenever you like, wherever you like.
This named argument style is fun, but if you want a real API, you can have that too:
function print(err,result) {console.log(result)}
var math = seneca.pin({role:'math',cmd:'*'})
math.sum( {left:1,right:2}, print )
math.product( {left:1,right:2}, print )
By pinning the pattern {role:'math',cmd:'*'} you created an object with methods that matched all the
cmd properties.
Why use Seneca?
Seneca provides plugins - sets of commands - that look after the foundations of your app for you:
- Organize your business logic
- Network API
- Data layer
- Caching
- Logging
- User management
- Distributed processing
Seneca is not a web framework, it's a business logic organizer. You can use it with any web framework.
Here's an example of how this helps you. Let's say the product command needs to live on a separate server. You can move it there with no changes to your calling code. First, set up the server:
seneca.add({role:'math', cmd:'product'}, function(args,callback) {
var product = args.left * args.right
callback(null,{answer:product})
})
seneca.use('transport')
var connect = require('connect')
var app = connect()
.use( connect.json() )
.use( seneca.service() )
.listen(10171)
The seneca.use method loads in a plugin. In this case,
the transport plugin, which can accept commands over a HTTP
interface. Seneca exposes any plugin HTTP interface by
returning a middleware function from the
seneca.service method. You can use this directly with the
connect or
express modules.
On the client side, just transport the remote patterns:
seneca.use('transport',{
pins:[ {role:'math',cmd:'product'} ]
})
seneca.act( {role:'math', cmd:'product', left:3, right:4},
function(err,result) {
if( err ) return console.error( err )
console.log(result)
})
The pins option to the transport plugin accepts a
set of patterns that will be sent over to the server (running on
localhost by default). The transport plugin just
calls seneca.add for each pin, and proxies the input and
output to the remote server.
Your calling code, the seneca.act call, does not
change. You now have a distributed system with no
refactoring. Fire up a few math.product servers, put them
behind a load balancer, and now you have a scalable system too.
But what about data storage?
You really need a common database layer so that plugins and commands can all deal with data in the same way. Let's go with an Active Record style - it's nice and easy.
var product = seneca.make('product')
product.name = 'apple'
product.price = 100
product.save$(function( err, product ) {
if( err ) return console.error( err )
console.log( 'saved: '+product )
// product.id was generated for you
product.load$({id:product.id},function( err, product ) {
if( err ) return console.error( err )
console.log( 'loaded: '+product )
})
})
The make method creates an "Active Record"-ish object for
you. The properties of this object store your data. Call
the save$ and load$ methods to save and load
your data from the database. You also get the list$
method for queries, and remove$ for deleting.
You get an in-memory data store automatically, so the above code works out of the box without any database configuration. Here's how you would use MongoDB:
seneca.use( 'mongo-store',
{name:'mydata',host:'127.0.0.1',port:27017} )
seneca.ready( function(err,seneca){
if( err ) return console.error( 'ERROR:'+err )
var product = seneca.make('product')
product.name = 'apple'
product.price = 100
product.save$(function( err, product ) {
if( err ) return console.error( err )
// product.id is a MongoDB id this time
product.load$({id:product.id},function( err, product ) {
if( err ) return console.error( err )
console.log( 'loaded: '+product )
})
})
})
The seneca.use method tells Seneca to load a
plugin, including any commands the plugin exposes. In this case,
you're loading the mongo-store plugin, which lets you talk to
the MongoDB database.
To use the mongo-store plugin, you'll need to install it:
npm install seneca-mongo-store
The seneca.ready method waits for the database connection
to be established. Apart from plugin configuration, your data logic
code is exactly the same. Swapping databases at any stage of your project is trivial.
How is the database layer implemented? It's just another set of commands. Here's the same code again:
var product = seneca.make('product')
product.name = 'apple'
product.price = 100
seneca.act(
{ role:'entity', cmd:'save', ent:product},
function( err, product ) {
if( err ) return console.error( err )
console.log( 'saved: '+product )
seneca.act(
// q is: the query - find matching property values
// qent is: entity type we want back
{ role:'entity', cmd:'load', q:{id:product.id}, qent:product},
function( err, product ) {
if( err ) return console.error( err )
console.log( 'loaded: '+product )
})
})
You can add support for your favorite database by writing a plugin with commands for
each of the entity operations: save, load,
etc. Here are some of the data stores Seneca supports:
- Memory (transient) - built in
- JSON files (on disk) - seneca-jsonfile-store
- JSON resources (REST API) - seneca-jsonrest-store
- MongoDB - seneca-mongo-store
- MySQL - seneca-mysql-store
- PostgreSQL - seneca-postgres-store
- Redis - seneca-redis-store
- CouchDB - seneca-couch-store
- SQLite - seneca-sqlite-store
You can even decide to save different entities into different databases - for example, put user profiles into MongoDB, purchase transactions into MySQL, and sessions into Redis. You do need to stay database independent by avoiding table joins, but you don't need them anyway!