db7a86321b | ||
---|---|---|
example | ||
lib | ||
test | ||
.gitignore | ||
README.md | ||
index.js | ||
package.json |
README.md
asteroid
v0.7.0
Install
slnode install asteroid -g
Server API
Client API
TODO
App
Create an asteroid application.
var asteroid = require('asteroid');
var app = asteroid();
app.get('/', function(req, res){
res.send('hello world');
});
app.listen(3000);
Notes:
- extends express
- see express docs for details
- supports express / connect middleware
app.model(Model)
Expose a Model
to remote clients.
var memory = asteroid.createDataSource({connector: asteroid.Memory});
var Color = memory.createModel('color', {name: String});
app.model(Color);
app.use(asteroid.rest());
Note: this will expose all shared methods on the model.
app.models()
Get the app's exposed models.
var models = app.models();
models.forEach(function (Model) {
console.log(Model.modelName); // color
});
Model
An Asteroid Model
is a vanilla JavaScript class constructor with an attached set of properties and options. A Model
instance is created by passing a data object containing properties to the Model
constructor. A Model
constructor will clean the object passed to it and only set the values matching the properties you define.
// valid color
var Color = asteroid.createModel('color', {name: String});
var red = new Color({name: 'red'});
console.log(red.name); // red
// invalid color
var foo = new Color({bar: 'bat baz'});
console.log(foo.bar); // undefined
Properties
A model defines a list of property names, types and other validation metadata. A DataSource uses this definition to validate a Model
during operations such as save()
.
Options
Some DataSources may support additional Model
options.
Define an asteroid model.
var User = asteroid.createModel('user', {
first: String,
last: String,
age: Number
});
Validation (expiremental)
Model.validatesPresenceOf(properties...)
Require a model to include a property to be considered valid.
User.validatesPresenceOf('first', 'last', 'age');
Model.validatesLengthOf(property, options)
Require a property length to be within a specified range.
User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}});
Model.validatesInclusionOf(property, options)
Require a value for property
to be in the specified array.
User.validatesInclusionOf('gender', {in: ['male', 'female']});
Model.validatesExclusionOf(property, options)
Require a value for property
to not exist in the specified array.
User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']});
Model.validatesNumericalityOf(property, options)
Require a value for property
to be a specific type of Number
.
User.validatesNumericalityOf('age', {int: true});
Model.validatesUniquenessOf(property, options)
Ensure the value for property
is unique.
User.validatesUniquenessOf('email', {message: 'email is not unique'});
Note: not available for all connectors.
myModel.isValid()
Validate the model instance.
user.isValid(function (valid) {
if (!valid) {
user.errors // hash of errors {attr: [errmessage, errmessage, ...], attr: ...}
}
});
Model.attachTo(dataSource)
Attach a model to a DataSource. Attaching a DataSource updates the model with additional methods and behaviors.
var oracle = asteroid.createDataSource({
connector: require('asteroid-connector-oracle'),
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
User.attachTo(oracle);
Note: until a model is attached to a data source it will not have any attached methods.
CRUD and Query Mixins
Mixins are added by attaching a vanilla model to a data source with a connector. Each connector enables its own set of operations that are attached to a Model
as methods. To see available methods for a data source with a connector call dataSource.operations()
.
Static Methods
Model.create([data], [callback])
Create an instance of Model with given data and save to the attached data source.
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
console.log(user instanceof User); // true
});
Model.count([query], callback)
Query count of Model instances in data source. Optional query param allows to count filtered set of Model instances.
User.count({approved: true}, function(err, count) {
console.log(count); // 2081
});
Model.all(filter, callback)
Find all instances of Model, matched by query. Fields used for filter and sort should be declared with {index: true}
in model definition.
filter
- where
Object
{ key: val, key2: {gt: 'val2'}} - include
String
,Object
orArray
. - order
String
- limit
Number
- skip
Number
Find the second page of 10 users over age 21 in descending order.
User.all({where: {age: {gt: 21}}, order: 'age DESC', limit: 10, skip: 10})
Note: See the specific connector's docs for more info.
Model.destroyAll(callback)
Delete all Model instances from data source. Note: destroyAll method does not perform destroy hooks.
Model.find(id, callback)
Find instance by id.
User.find(23, function(err, user) {
console.info(user.id); // 23
});
Model.findOne(filter, callback)
Find a single instance that matches the given filter.
User.find(23, function(err, user) {
console.info(user.id); // 23
});
Model.upsert(data, callback)
Update when record with id=data.id found, insert otherwise. Note: no setters, validations or hooks applied when using upsert.
Custom Static Methods
Define a static model method.
User.login = function (username, password, fn) {
var passwordHash = hashPassword(password);
this.findOne({username: username}, function (err, user) {
var failErr = new Error('login failed');
if(err) {
fn(err);
} else if(!user) {
fn(failErr);
} else if(user.password === passwordHash) {
MySessionModel.create({userId: user.id}, function (err, session) {
fn(null, session.id);
});
} else {
fn(failErr);
}
});
}
Setup the static model method to be exposed to clients as a remote method.
asteroid.remoteMethod(
User.login,
{
accepts: [
{arg: 'username', type: 'string', required: true},
{arg: 'password', type: 'string', required: true}
],
returns: {arg: 'sessionId', type: 'any'},
http: {path: '/sign-in'}
}
);
Instance Methods
model.save([options], [callback])
Save an instance of a Model to the attached data source.
var joe = new User({first: 'Joe', last: 'Bob'});
joe.save(function(err, user) {
if(user.errors) {
console.log(user.errors);
} else {
console.log(user.id);
}
});
model.updateAttributes(data, [callback])
Save specified attributes to the attached data source.
user.updateAttributes({
first: 'updatedFirst',
name: 'updatedLast'
}, fn);
model.destroy([callback])
Remove a model from the attached data source.
model.destroy(function(err) {
// model instance destroyed
});
Custom Instance Methods
Define an instance method.
User.prototype.logout = function (fn) {
MySessionModel.destroyAll({userId: this.id}, fn);
}
Define a remote model instance method.
asteroid.remoteMethod(User.prototype.logout);
Remote Methods
Both instance and static methods can be exposed to clients. A remote method must accept a callback with the conventional fn(err, result, ...)
signature.
asteroid.remoteMethod(fn, [options]);
Expose a remote method.
Product.stats = function(fn) {
myApi.getStats('products', fn);
}
asteroid.remoteMethod(
Product.stats,
{
returns: {arg: 'stats', type: 'array'},
http: {path: '/info', verb: 'get'}
}
);
Options
- accepts - (optional) an arguments description specifying the remote method's arguments. A
- returns - (optional) an arguments description specifying the remote methods callback arguments.
- http - (advanced / optional, object) http routing info
- http.path - the path relative to the model the method will be exposed at. May be a path fragment (eg. '/:myArg') which will be populated by an arg of the same name in the accepts description. For example the stats method above will be at the whole path
/products/stats
. - http.verb - (get, post, put, del, all) - the route verb the method will be available from.
Argument Description
An arguments description defines either a single argument as an object or an ordered set of arguments as an array.
// examples
{arg: 'myArg', type: 'number'}
[
{arg: 'arg1', type: 'number', required: true},
{arg: 'arg2', type: 'array'}
]
Types
Each argument may define any of the asteroid types.
Notes:
- The callback is an assumed argument and does not need to be specified in the accepts array.
- The err argument is also assumed and does not need to be specified in the returns array.
Remote Hooks
Run a function before or after a remote method is called by a client.
// *.save === prototype.save
User.beforeRemote('*.save', function(ctx, user, next) {
if(ctx.user) {
next();
} else {
next(new Error('must be logged in to update'))
}
});
User.afterRemote('*.save', function(ctx, user, next) {
console.log('user has been saved', user);
next();
});
Remote hooks also support wildcards. Run a function before any remote method is called.
// ** will match both prototype.* and *.*
User.beforeRemote('**', function(ctx, user, next) {
console.log(ctx.methodString, 'was invoked remotely'); // users.prototype.save was invoked remotely
next();
});
Other wildcard examples
// run before any static method eg. User.all
User.beforeRemote('*', ...);
// run before any instance method eg. User.prototype.save
User.beforeRemote('prototype.*', ...);
// prevent password hashes from being sent to clients
User.afterRemote('**', function (ctx, user, next) {
if(ctx.result) {
if(Array.isArray(ctx.result)) {
ctx.result.forEach(function (result) {
result.password = undefined;
});
} else {
ctx.result.password = undefined;
}
}
next();
});
Context
Remote hooks are provided with a Context ctx
object which contains transport specific data (eg. for http: req
and res
). The ctx
object also has a set of consistent apis across transports.
ctx.user
A Model
representing the user calling the method remotely. Note: this is undefined if the remote method is not invoked by a logged in user.
ctx.result
During afterRemote
hooks, ctx.result
will contain the data about to be sent to a client. Modify this object to transform data before it is sent.
Rest
When asteroid.rest is used the following ctx
properties are available.
ctx.req
The express ServerRequest object. See full documentation.
ctx.res
The express ServerResponse object. See full documentation.
Access the raw req
object for the remote method call.
Relationships
Model.hasMany(Model)
Define a "one to many" relationship.
// by referencing model
Book.hasMany(Chapter);
// specify the name
Book.hasMany('chapters', {model: Chapter});
Query and create the related models.
Book.create(function(err, book) {
// create a chapter instance
// ready to be saved in the data source
var chapter = book.chapters.build({name: 'Chapter 1'});
// save the new chapter
chapter.save();
// you can also call the Chapter.create method with
// the `chapters` property which will build a chapter
// instance and save the it in the data source
book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
// this callback is optional
});
// query chapters for the book using the
book.chapters(function(err, chapters) {
// all chapters with bookId = book.id
console.log(chapters);
});
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
console.log(chapters);
});
});
Model.hasAndBelongsToMany()
TODO: implement / document
Shared Methods
Any static or instance method can be decorated as shared
. These methods are exposed over the provided transport (eg. asteroid.rest).
Data Source
An Asteroid DataSource
provides Models with the ability to manipulate data. Attaching a DataSource
to a Model
adds instance methods and static methods to the Model
. The added methods may be remote methods.
Define a data source for persisting models.
var oracle = asteroid.createDataSource({
connector: 'oracle',
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
dataSource.createModel(name, properties, options)
Define a model and attach it to a DataSource
.
var Color = oracle.createModel('color', {name: String});
dataSource.discoverModelDefinitions([username], fn)
Discover a set of model definitions (table or collection names) based on tables or collections in a data source.
oracle.discoverModelDefinitions(function (err, models) {
models.forEach(function (def) {
// def.name ~ the model name
oracle.discoverSchema(null, def.name, function (err, schema) {
console.log(schema);
});
});
});
dataSource.discoverSchema([owner], name, fn)
Discover the schema of a specific table or collection.
Example schema from oracle connector:
{
"name": "Product",
"options": {
"idInjection": false,
"oracle": {
"schema": "BLACKPOOL",
"table": "PRODUCT"
}
},
"properties": {
"id": {
"type": "String",
"required": true,
"length": 20,
"id": 1,
"oracle": {
"columnName": "ID",
"dataType": "VARCHAR2",
"dataLength": 20,
"nullable": "N"
}
},
"name": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "NAME",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"audibleRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "AUDIBLE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"effectiveRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "EFFECTIVE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"rounds": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "ROUNDS",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"extras": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "EXTRAS",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"fireModes": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "FIRE_MODES",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
}
}
}
dataSource.enableRemote(operation)
Enable remote access to a data source operation. Each connector has its own set of set remotely enabled and disabled operations. You can always list these by calling dataSource.operations()
.
dataSource.disableRemote(operation)
Disable remote access to a data source operation. Each connector has its own set of set enabled and disabled operations. You can always list these by calling dataSource.operations()
.
// all rest data source operations are
// disabled by default
var oracle = asteroid.createDataSource({
connector: require('asteroid-connector-oracle'),
host: '...',
...
});
// or only disable it as a remote method
oracle.disableRemote('destroyAll');
Notes:
- disabled operations will not be added to attached models
- disabling the remoting for a method only affects client access (it will still be available from server models)
- data sources must enable / disable operations before attaching or creating models
dataSource.operations()
List the enabled and disabled operations.
console.log(oracle.operations());
Output:
{
find: {
remoteEnabled: true,
accepts: [...],
returns: [...]
enabled: true
},
save: {
remoteEnabled: true,
prototype: true,
accepts: [...],
returns: [...],
enabled: true
},
...
}
Connectors
Create a data source with a specific connector. See available connectors for specific connector documentation.
var memory = asteroid.createDataSource({
connector: asteroid.Memory
});
Available Connectors
- Oracle
- In Memory
- TODO - REST
- TODO - MySQL
- TODO - SQLite3
- TODO - Postgres
- TODO - Redis
- TODO - MongoDB
- TODO - CouchDB
- TODO - Firebird
Installing Connectors
Include the connector in your package.json dependencies and run npm install
.
{
"dependencies": {
"asteroid-connector-oracle": "latest"
}
}
GeoPoint TODO
Embed a latitude / longitude point in a Model.
var CoffeeShop = asteroid.createModel('coffee-shop', {
location: 'GeoPoint'
});
Asteroid Model's with a GeoPoint property and an attached DataSource may be queried using geo spatial filters and sorting.
Find the 3 nearest coffee shops.
CoffeeShop.attach(oracle);
var here = new GeoPoint({lat: 10.32424, long: 5.84978});
CoffeeShop.all({where: {location: {near: here}}}, function(err, nearbyShops) {
console.info(nearbyShops); // [CoffeeShop, ...]
});
geoPoint.distanceTo(geoPoint, options)
Get the distance to another GeoPoint
.
var here = new GeoPoint({lat: 10, long: 10});
var there = new GeoPoint({lat: 5, long: 5});
console.log(here.distanceTo(there, {type: 'miles'})); // 438
GeoPoint.distanceBetween(a, b, options)
Get the distance between two points.
GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
Distance Types
miles
radians
kilometers
geoPoint.lat
The latitude point in degrees. Range: -90 to 90.
geoPoint.long
The longitude point in degrees. Range: -180 to 180.
Asteroid Types
Various APIs in Asteroid accept type descriptions (eg. remote methods, asteroid.createModel()). The following is a list of supported types.
null
- JSON nullBoolean
- JSON booleanNumber
- JSON numberString
- JSON stringObject
- JSON objectArray
- JSON arrayDate
- a JavaScript date objectBuffer
- a node.js Buffer object- GeoPoint - an asteroid GeoPoint object. TODO
REST Router
Expose models over rest using the asteroid.rest
router.
app.use(asteroid.rest());
REST Documentation
View generated REST documentation by visiting: http://localhost:3000/_docs.
SocketIO Middleware Not Available
Coming Soon - Expose models over socket.io using the asteroid.sio()
middleware.
app.use(asteroid.sio);