-
Notifications
You must be signed in to change notification settings - Fork 2
Home
Nodulator is designed to make it more easy to create highly modulable REST APIs, with integrated ORM in CoffeeScript.
Open exemple.coffee to see a full working exemple
You must understand how express callback style works (req, res, next) ->
- Integrated ORM
- Integrated Routing system (with express, and highly linked with ORM)
- Multiple DB Systems
- Authentication with passport
- Permissions management
- Complex inheritance system
- Modulable
- Project generation
-
Nodulator-Assets:
- Automatic assets management
-
Nodulator-Socket:
- Socket.io implementation for Nodulator
-
Nodulator-Angular:
- Angular implementation for Nodulator
- Inheritance system
- Integrated and linked SocketIO
- Assets management
- Installation
- Quick Start
- Configuration
- Resource
- Overriding and Inheritance
- Route
- Auth
- Restriction
- DB Systems
- Other Stuff
- Project Generation
- Developers
- Contributors
- DOC
- TODO
Just run :
npm install nodulator
Or check the Project Generation section
After you can require Nodulator as a module :
Nodulator = require 'nodulator'Here is the quickiest way to play around Nodulator
_ = require 'underscore'
Nodulator = require 'nodulator'
class PlayerRoute extends Nodulator.Route.DefaultRoute
Config: ->
# We create: GET => /api/1/{resource_name}/usernames
# Get a list of every players' usernames
@Add 'get', '/usernames', (req, res) =>
# There is a @resource property, containing attached Resource class
@resource.ListUsernames (err, usernames) ->
return res.status(500).send err if err?
res.status(200).send usernames
# We call super() to apply Nodulator.Route.DefaultRoute behaviour
# We called '/usernames' route before, so it won't be override by
# default route GET => /api/1/{resource_name}/:id
super()
# We create: PUT => /api/1/{resource_name}/:id/levelUp
@Add 'put', '/:id/levelUp', (req, res) =>
# For DefaultRoute routes with '/:id/*',
# Fetch the corresponding Resource and put the instance in req[@resource.lname]
# (here it can be called 'req.player' but we want to stay generic)
req[@resource.lname].LevelUp (err, resource) ->
return res.status(500).send err if err?
res.status(200).send resource.ToJSON()
# We create a resource, and we attach the PlayerRoute
# The {account: true} config object tell us that it's an Account Resource
# It will hold all the authentication logic of the app
class PlayerResource extends Nodulator.Resource 'player', PlayerRoute, {account: true}
# We create a LevelUp method
LevelUp: (done) ->
@level++
@Save done
# And a class method to get a list of usernames
@ListUsernames: (done) ->
@List (err, players) ->
return done err if err?
done null, _(players).pluck 'username'
# And we Init()
PlayerResource.Init()
Go inside your project folder, copy this POC in a test.coffee file and type in:
$> coffee test.coffee
Then open your favorite REST API Client (Postman for Chrome is my favorite)
and try the following routes :
Each route is of the following form :
{VERB} {URL} ({PARAMS}) => (code) {ANSWER}
# Signup process
POST '/api/1/players' {username: 'test1', password: 'test1', level: 1} => (200) {id: 1, username: 'test1', password: 'test1', level: 1}
POST '/api/1/players' {username: 'test2', password: 'test2', level: 1} => (200) {id: 2, username: 'test2', password: 'test2', level: 1}
# Login process
POST '/api/1/players/login' {username: 'test1', password: 'test1'} => (200) {}
POST '/api/1/players/login' {username: 'test1', password: 'badpassword'} => (403) {}
GET '/api/1/players' => (200) [{id: 1, username: 'test1', password: 'test1', level: 1},
{id: 2, username: 'test2', password: 'test2', level: 1}]
GET '/api/1/players/1' => (200) {id: 1, username: 'test1', password: 'test1', level: 1}
GET '/api/1/players/2' => (200) {id: 2, username: 'test2', password: 'test2', level: 1}
PUT '/api/1/players/2/levelUp' {} => (200) {id: 2, username: 'test2', password: 'test2', level: 2}
PUT '/api/1/players/2/levelUp' {} => (200) {id: 2, username: 'test2', password: 'test2', level: 3}
GET '/api/1/players/usernames' => (200) ['test1', 'test2']
PUT '/api/1/players/2' {username: 'notAUsername'} => (200) {id: 2, username: 'notAUsername', level: 3}
GET '/api/1/players/usernames' => (200) ['test1', 'notAUsername']
DELETE '/api/1/players/1' {} => (200) {id: 1, username: 'test1', password: 'test1', level: 1}
GET '/api/1/players/usernames' => (200) ['notAUser,ame']
First of all, the configuration process is absolutly optional.
If you don't give Nodulator a config, it will assume you want to use SqlMem DB system, with no persistance at all. Usefull for heavy tests periods.
If you prefere to use a persistant system, here is the procedure :
Nodulator = require 'nodulator'
Nodulator.Config
dbType: 'Mongo' # You can select 'SqlMem' or 'Mongo' or 'Mysql'
dbAuth: # Fields needed if Mongo or Mysql
host: 'localhost'
database: 'test'
port: 27017 # From there, can be ignored. Default values taken
user: 'test' # |
pass: 'test' # |_Nodulator provides 2 main Objects :
Nodulator.Resource
Nodulator.RouteA Resource is a class permitting to retrive and save a model from a DB.
Here is an exemple of creating a Resource
PlayerResource = Nodulator.Resource 'player'
PlayerResource.Init()Here, it creates a PlayerResource, linked with a players table in DB (if any), and with /api/1/players routes (if any)
Note the 's' concatenated with the Resource name. Its the real Resource.name of a resource
For the same name without the 's', there is a Resource.lname property.
It's needed in order to prepare the Resource. All the Nodulator's magic is inside this call.
If you forget it :
- The
Resourcewill NOT be linked toRoute(if any) - It will NOT prepare
Accountsystem (if any) - It will NOT prepare inheritance system so you won't be able to inherit from it
- It will NOT be linked to a corresponding table in DB
- Nothing will work or happend. Ever.
You can pass several params to Nodulator.Resource :
Nodulator.Resource name [, Route] [, config]You must provide a name, that is different of 'user' (reserved)
You can attach a Route and/or a config object (see Auth) to a Resource.
Each Resource provides some 'Class methods' to manage the specific model in db :
PlayerResource.Fetch(id, done)
PlayerResource.FetchBy(field, value, done)
PlayerResource.List(id, done)
PlayerResource.ListBy(field, value, done)
PlayerResource.Deserialize(blob, done)The Fetch method take an id and return a PlayerResource intance to done callback :
PlayerResource.Fetch 1, (err, player) ->
return console.error err if err?
[...] # Do something with player instanceYou can also call FetchBy method to give a specific field to retrive.
It can be unique, or the first occurence in DB will return (depends on DB implementations)
You can list every models from this Resource thanks to List call :
PlayerResource.List (err, players) ->
return console.error err if err?
[...] # players is an array of PlayerResource instanceLike FetchBy, you can ListBy a specific field.
The Deserialize method allow to get an instance of a given Resource.
Never use new operator directly on a Resource, else you might bypass the relationning system.
Deserialize method is used to make pre-processing work (like fetching related models) before instantiation.
A player instance has some methods :
player.Save(done)
Save the model in DB. The callback take 2 arguments : (err, instance) ->
player.Delete(done)
Delete the model from the DB. The callback take 1 argument : (err) ->
player.Serialize()
Get every object properties, and return it in a new object.
Generaly used to get what to be saved in DB.
player.ToJSON()
By default, it calls Serialize().
Generaly used to get what to send to client.
You can inherit from a Resource to override or enhance its default behaviour, or to make a complex class inheritance system built on Resource
In CoffeeScript its pretty easy:
class UnitResource extends Nodulator.Resource 'unit'
# Here we override the constructor to attach a weapon resource
# Never forget to call super(blob), or the instance will never be populated by DB fields
constructor: (blob, @weapon) ->
super blob
# We create a new instance method
LevelUp: (done) ->
@level++
@Save done
# Here we override the Deserialize class method, to fetch the attached WeaponResource
@Deserialize: (blob, done) ->
# If the resource isnt deserialized from db, don't fetch attached resource
if !(blob.id?)
return super blob, done
WeaponResource.FetchByUserId blob.id, (err, weapon) =>
res = @
done(null, new res(blob, weapon))
UnitResource.Init()You can define an abstract class, that won't be attached to any model in DB or any Route
class UnitResource extends Nodulator.Resource 'unit', {abstract: true}
[...]
UnitResource.Init();Of course, abstract classes are only designed to be inherited. (Please note that they can't have a Route attached, and other config is ignored)
Given the last exemple, here is a class that inherits from UnitResource
# Note the call to 'Extend()' method
class PlayerResource extends UnitResource.Extend 'player'
# Give PlayerResource a new beheviour
NewBehaviour: (args, done) ->
[...]
# Overriding existing UnitResource LevelUp()
LevelUp: (done) ->
[...]
PlayerResource.Init();You can call the Extend() method either from a full Resource or from an abstract one.
Please note that if both parent and child are full Resource, both will have corresponding model available from ORM (here units and players)
So be carefull when creating extended Resource, and think about abstract !
Nodulator provides a Route object, to be attached to a Resource object in order to describe routing process.
class UnitResource extends Nodulator.Resource 'unit', Nodulator.RouteThere is no need of Init() here. Ever.
Default Nodulator.Route do nothing. You have to inherit from it to describe routes :
class UnitRoute extends Nodulator.Route
# Override the Config() method
Config: ->
# And never forget to call the super()
super()
# Here we define: GET => /api/1/{resource_name}/:id
@Add 'get', '/:id', (req, res) =>
# The @resource field points to attached Resource
@resource.Fetch req.params.id, (err, unit) ->
return res.status(500).send err if err?
res.status(200).send unit.ToJSON()
# Here we define: POST => /api/1/{resource_name}
@Add 'post', (req, res) ->
res.status(200).end()This Route, attached to a Resource (here UnitResource) add 2 endpoints :
GET => /api/1/units/:id
POST => /api/1/units
Each Route have to implement a Config() method, calling super() and defining routes thanks to @Add() call.
Here is the @Add() call definition :
Nodulator.Route.Add verb, [endPoint = '/'], [middleware, [middleware, ...]], callbackNodulator provides also a standard route system for lazy : Nodulator.Route.DefaultRoute.
It setup 5 routes (exemple when attached to a PlayerResource) :
GET => /api/1/players => List
GET => /api/1/players/:id => Get One
POST => /api/1/players => Create
PUT => /api/1/players/:id => Update
DELETE => /api/1/players/:id => Delete
You can inherit from any route object :
class TestRoute extends Nodulator.Route.DefaultRouteAnd you can override existing route by providing same association verb + url. Exemple :
class TestRoute extends Nodulator.Route.DefaultRoute
Config: ->
super()
# Here we override the default GET => /api/1/{resource_name}/:id
@Add 'get', '/:id', (req, res) =>
[...]Authentication is based on Passport
You can assign a Ressource as AccountResource :
config =
account: true
class PlayerResource extends Nodulator.Resource 'player', configDefaults fields for authentication are 'username' and 'password'
You can change them (optional) :
config =
account:
fields:
usernameField: 'login'
passwordField: 'pass'
class PlayerManager extends Nodulator.Resource 'player', configIt creates a custom method from usernameField
*FetchByUsername(username, done)
or if customized
*FetchByLogin(login, done)
* Class methods
It defines 2 routes (here when attached to a PlayerResource) :
POST => /api/1/players/login
POST => /api/1/players/logout
It setup session system, and thanks to Passport,
It fills req.user variable to handle public/authenticated routes
You have to extend yourself the post default route (for exemple) of your Resource to use it as a signup route.
USER:
You can restrict access to a Resource :
config =
account: true
restricted: 'user' #Can be 'user', 'auth', or an object
class PlayerResource extends Nodulator.Resource 'player', configThis code create a APlayer resource that is an account,
and only player itself can access to its resource (GET, PUT and DELETE on own /api/1/players/:id)
POST and GET-without-id are still accessible for anyone (you can override them)
/!\ 'user' keyword must only be used on account resource
AUTH:
You can restrict access to a Resource for authenticated users only :
config =
restricted: 'auth'
class HiddenResource extends Nodulator.Resource 'hidden', configThis code create a HiddenResource that can only be accessed by authenticated users
OBJECT:
You can restrict access to a Resource for users that have particular property set :
config =
restricted:
group: 1
x: 'test'
class HiddenResource extends Nodulator.Resource 'hidden', configIt will deny access to whole resource for any users that don't have theses properties set
It's not possible anymore to put a certain rule on a certain route. Theses rules apply to the whole resource.
We defined a driver interface for some DB implementations.
It's based on SQL Table concept. (see lib/connectors/sql/index.coffee)
Table.Find(id, done)
Table.FindWhere(fields, where, done)
Table.Select(fields, where, options, done)
Table.Save(blob, done)
Table.Insert(blob, done)
Table.Update(blob, where, done)
Table.Delete(id, done)Every Resource have an associated Table instance that links to the good table/document in the good DB driver system
Built-in MySQL implementation (node-mysql) for Nodulator
Check lib/connectors/sql/Mysql.coffee
Built-in MongoDB implementation (mongous) for Nodulator
Check lib/connectors/sql/Mongo.coffee
Special DB driver, built on RAM.
It provides same options as others systems do, but nothing is stored. When you stop the server, everything is deleted.
Check lib/connectors/sql/SqlMem.coffee
There is a Nodulator.bus object that is basicaly an EventEmitter. Every objects in Nodulator use this bus.
Here are the emitted events:
-
On a new
Resourcebeing inserted in DB, sends it after aSerialize()callNodulator.bus.emit 'new_' + resource_name, @Serialize()
-
On a
Resourcebeing updated in DB, sends it after aSerialize()callNodulator.bus.emit 'update_' + resource_name, @Serialize()
-
On a
Resourcebeing deleted from DB, sends it after aSerialize()callNodulator.bus.emit 'delete_' + resource_name, @Serialize()
Exemple
PlayerResource = Nodulator.Resource 'player'
Nodulator.on 'new_player', (player) ->
[...] # Do something with this brand new playerYou can override default Bus by setting new class to Nodulator.Bus :
Nodulator = require 'nodulator'
NewBus = require './NewBus'
Nodulator.Bus = NewBusAlways set new Bus before any Resource call or any added Module
You can get global Nodulator :
$> npm install -g nodulator
$> Nodulator
Usage: Nodulator (init) | (install (moduleName)| remove (moduleName))
Nodulator provides a way of installing modules easely
# If no arguments, install or remove Nodulator
$> Nodulator install
$> Nodulator remove
# Will install nodulator-assets
$> Nodulator install assets
# Will remove nodulator-socket
$> Nodulator remove socket
Then you can launch the init process :
$> Nodulator init
It creates the following structure :
main.coffee
package.json
settings/
server/
├── index.coffee
├── loadOrder.json
├── processors/
│ └── index.coffee
└── resources/
└── index.coffee
And then find for every Nodulator modules installed, and call their respective init method.
It generate a main.coffee and a package.json with every modules pre-loaded.
The server folder is auto-loaded (check server/index.coffee and every index.coffee in subfolders).
Folders load order is defined in server/loadOrder.json, and is automaticaly managed by new modules installed (they care of the order)
You can immediately start to write Resource in server/resources !
Never forget that I'm always available at champii.akronym@gmail.com for any questions
Nodulator
Properties :
Nodulator.app => the express app
Nodulator.express => the express module
Nodulator.passport => the passport module
Nodulator.server => the http server
Nodulator.authApp => if this app handle passport authentication
Nodulator.appRoot => the app root path
Nodulator.bus => official bus (EventEmitter)
Nodulator.Route => Route object
Nodulator.Resource(resourceName, [Route], [config])
Create the resource Class
Nodulator.Config(config)
Change config
Nodulator.Use(module)
Inject a module inside Nodulator
Nodulator.ExtendDefaultConfig(config)
Add some fields to default configuration
Nodulator.ExtendRunProcess(process)
Add a function to be executed at Nodulator's 'Run()' call
Nodulator.ListEndpoints(done)
DEBUG PURPOSE
List every api endpoint added by application
Nodulator.Run()
Launch Nodulator MainLoop and set last parameters after every Resource have Init()
Process every Module functions
Resource
(Uppercase for Class, lowercase for instance)
Resource.Fetch(id, done)
Take an id and return it from the DB in done callback: (err, resource) ->
Resource.FetchBy(field, value, done)
Take a field and a value, and return first row from the DB in done callback: (err, resource) ->
Resource.List(done)
Return every records in DB for this resource and give them to done: (err, resources) ->
Resource.ListBy(field, value, done)
Take a field and a value, and return every row from the DB in done callback: (err, resources) ->
Resource.Deserialize(blob, done)
Method that take the blob returned from DB to make a new instance
resource.Save(done)
Save the instance in DB
If the resource doesn't exists, it create and give it an id
It return to done the current instance
resource.Delete(done)
Delete the record in DB, and return affected rows in done
resource.Serialize()
Return every properties that aren't functions or objects or are undefined
This method is used to get what must be saved in DB
resource.ToJSON()
This method is used to get what must be send to client
Call @Serialize() by default, but can be overrided
Route
route.Add(type, [url], [middleware, [middleware, [...]]], done)
Create a route.
'type' can be 'all', 'get', 'post', 'put' and 'delete'
'url' will be concatenated with '/api/{VERSION}/{RESOURCE_NAME}'. Optional
'middleware' are optionals
'done' is the express app callback: (req, res, next) ->
route.Config()
Called when a Route is associated with a Resource.
This call prepare every routes, and must be inherited.
By order of priority
Better tests
Field validation
Better error management
Log system
Advanced Auth (Social + custom)
Separate Auth process from Nodulator
New Permission system
Better++ routing system (Auto add on custom method ?)
Relational models