Merge branch 'refactor/api'

Conflicts:
	lib/application.js
This commit is contained in:
Ritchie 2013-06-17 08:01:22 -07:00
commit 654a89147c
56 changed files with 1855 additions and 1831 deletions

10
.gitignore vendored
View File

@ -9,12 +9,4 @@
*.pid
*.swp
*.swo
/node_modules/*/node_modules
/node_modules/debug
/node_modules/express
/node_modules/jugglingdb*
/node_modules/mocha
/node_modules/sl-module-loader
/node_modules/sl-remoting
/node_modules/merge
/node_modules/inflection
node_modules

689
README.md
View File

@ -1,61 +1,684 @@
# asteroid
v0.0.1
v0.7.0
## Install
slnode install asteroid -g
## API
## Server API
### app
- [App](#app)
- [Model](#model)
- [DataSource](#data-source)
- [Connectors](#connectors)
- [GeoPoint](#geo-point)
- [Asteroid Types](#asteroid-types)
- [REST Router](#rest-router)
Create an asteroid app.
## Client API
var asteroid = require('asteroid')
, app = asteroid();
_TODO_
### app.dataSource()
### App
Attach a remote data source to your app.
Create an asteroid application.
app.dataSource('color-db', {
adapter: 'oracle',
host: 'localhost',
port: 2345,
user: 'test',
password: 'test'
var asteroid = require('asteroid');
var app = asteroid();
app.get('/', function(req, res){
res.send('hello world');
});
### app.define(name)
app.listen(3000);
Define a [Model](node_modules/model).
**Notes:**
var Color = app.define('color');
- extends [express](http://expressjs.com/api.html#express)
- see [express docs](http://expressjs.com/api.html) for details
- supports [express / connect middleware](http://expressjs.com/api.html#middleware)
### app.use(asteroid.rest);
#### app.model(Model)
Expose your models over a REST api.
Expose a `Model` to remote clients.
// node
app.use(asteroid.rest);
var memory = asteroid.createDataSource({connector: asteroid.Memory});
var Color = memory.createModel('color', {name: String});
// http
GET /colors
app.model(Color);
app.use(asteroid.rest());
**Note:** this will expose all [shared methods](#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](#data-source) uses this definition to validate a `Model` during operations such as `save()`.
**Options**
Some [DataSources](#data-source) may support additional `Model` options.
Define an asteroid model.
var User = asteroid.createModel('user', {
first: String,
last: String,
age: Number
});
#### 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](#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](#data-source). Attaching a [DataSource](#data-source) updates the model with additional methods and behaviors.
var oracle = asteroid.createDataSource({
connector: require('asteroid-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**.
#### Attached Methods
Attached methods are added by attaching a vanilla model to a data source with a connector. Each [connector](#connectors) 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()`.
##### 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.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.upsert(data, callback)
Update when record with id=data.id found, insert otherwise. **Note:** no setters, validations or hooks applied when using upsert.
##### model.destroy([callback])
Remove a model from the attached data source.
model.destroy(function(err) {
// model instance destroyed
});
##### 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.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` or `Array`.
- **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](#connectors) for more info.
##### 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
});
#### 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](#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
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 relative path 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.
- **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'}
200 OK
[
{name: 'red'},
{name: 'blue'},
{name: 'green'}
{arg: 'arg1', type: 'number', required: true},
{arg: 'arg2', type: 'array'}
]
## Asteroid Modules
**Types**
- [Asteroid Module Base Class](node_modules/asteroid-module)
- [Route](node_modules/route)
- [Model Route](node_modules/model-route)
- [Model](node_modules/model)
- [Data Source](node_modules/data-source)
Each argument may define any of the [asteroid types](#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](#asteroidrest) is used the following `ctx` properties are available.
###### ctx.req
The express ServerRequest object. [See full documentation](http://expressjs.com/api.html#req).
###### ctx.res
The express ServerResponse object. [See full documentation](http://expressjs.com/api.html#res).
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) {
// using 'chapters' scope for build:
var c = book.chapters.build({name: 'Chapter 1'});
// same as:
c = new Chapter({name: 'Chapter 1', bookId: book.id});
// using 'chapters' scope for create:
book.chapters.create();
// same as:
Chapter.create({bookId: book.id});
// using scope for querying:
book.chapters(function(err, chapters) {
/* all chapters with bookId = book.id */
});
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
});
});
##### 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](#rest)).
### Data Source
An Asteroid `DataSource` provides [Models](#model) with the ability to manipulate data. Attaching a `DataSource` to a `Model` adds [instance methods](#instance-methods) and [static methods](#static-methods) to the `Model`. The added methods may be [remote methods](#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.discoverAndBuildModels(owner, tableOrView, options, fn)
Discover a set of models based on tables or collections in a data source.
oracle.discoverAndBuildModels('MYORG', function(err, models) {
var ProductModel = models.Product;
});
**Note:** The `models` will contain all properties and options discovered from the data source. It will also automatically discover and create relationships.
#### dataSource.discoverAndBuildModelsSync(owner, tableOrView, options)
Synchronously Discover a set of models based on tables or collections in a data source.
var models = oracle.discoverAndBuildModelsSync('MYORG');
var ProductModel = models.Product;
#### dataSource.defineOperation(name, options, fn)
Define a new operation available to all model's attached to the data source.
var maps = asteroid.createDataSource({
connector: require('asteroid-rest'),
url: 'http://api.googleapis.com/maps/api'
});
rest.defineOperation('geocode', {
url: '/geocode/json',
verb: 'get',
accepts: [
{arg: 'address', type: 'string'},
{arg: 'sensor', default: 'true'}
],
returns: {arg: 'location', type: asteroid.GeoPoint, transform: transform},
json: true,
enableRemote: true
});
function transform(res) {
var geo = res.body.results[0].geometry;
return new asteroid.GeoPoint({lat: geo.lat, long: geo.lng});
}
var GeoCoder = rest.createModel('geocoder');
GeoCoder.geocode('123 fake street', function(err, point) {
console.log(point.lat, point.long); // 24.224424 44.444445
});
#### dataSource.enableRemote(operation)
Enable remote access to a data source operation. Each [connector](#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](#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-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](http://github.com/strongloop/asteroid-connectors/oracle)
- [In Memory](http://github.com/strongloop/asteroid-connectors/memory)
- TODO - [REST](http://github.com/strongloop/asteroid-connectors/rest)
- TODO - [MySQL](http://github.com/strongloop/asteroid-connectors/mysql)
- TODO - [SQLite3](http://github.com/strongloop/asteroid-connectors/sqlite)
- TODO - [Postgres](http://github.com/strongloop/asteroid-connectors/postgres)
- TODO - [Redis](http://github.com/strongloop/asteroid-connectors/redis)
- TODO - [MongoDB](http://github.com/strongloop/asteroid-connectors/mongo)
- TODO - [CouchDB](http://github.com/strongloop/asteroid-connectors/couch)
- TODO - [Firebird](http://github.com/strongloop/asteroid-connectors/firebird)
**Installing Connectors**
Include the connector in your package.json dependencies and run `npm install`.
{
"dependencies": {
"asteroid-oracle": "latest"
}
}
### GeoPoint
Embed a latitude / longitude point in a [Model](#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](#remote-methods), [asteroid.createModel()](#model)). The following is a list of supported types.
- `null` - JSON null
- `Boolean` - JSON boolean
- `Number` - JSON number
- `String` - JSON string
- `Object` - JSON object
- `Array` - JSON array
- `Date` - a JavaScript date object
- `Buffer` - a node.js Buffer object
- [GeoPoint](#geopoint) - an asteroid GeoPoint object.
### 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](http://localhost:3000/_docs).
### SocketIO Middleware **Not Available**
**Coming Soon** - Expose models over socket.io using the `asteroid.sio()` middleware.
app.use(asteroid.sio);

144
gen-tests.js Normal file
View File

@ -0,0 +1,144 @@
/**
* Generate asteroid unit tests from README...
*/
fs = require('fs')
readme = fs.readFileSync('../README.md').toString();
alias = {
myModel: 'Model',
model: 'Model',
ctx: 'Model',
dataSource: 'DataSource',
geoPoint: 'GeoPoint'
};
function getName(line) {
var name = line
.split('.')[0]
.replace(/#+\s/, '');
return alias[name] || name;
}
function Doc(line, lineNum, docIndex) {
this.name = getName(line);
line = line.replace(/#+\s/, '');
this.line = line;
this.lineNum = lineNum;
this.docIndex = docIndex;
}
Doc.prototype.nextDoc = function () {
return docs[this.docIndex + 1];
}
Doc.prototype.contents = function () {
var nextDoc = this.nextDoc();
var endIndex = lines.length - 1;
var contents = [];
if(nextDoc) {
endIndex = nextDoc.lineNum;
}
for(var i = this.lineNum; i < endIndex; i++) {
contents.push(lines[i]);
}
return contents;
}
Doc.prototype.example = function () {
var content = this.contents();
var result = [];
content.forEach(function (line) {
if(line.substr(0, 4) === ' ') {
result.push(line.substr(4, line.length))
}
});
return result;
}
Doc.prototype.desc = function () {
var content = this.contents();
var result = [];
var first;
content.forEach(function (line) {
if(first) {
// ignore
} else if(line[0] === '#' || line[0] === ' ') {
// ignore
} else {
first = line;
}
});
// only want the first sentence (to keep it brief)
if(first) {
first = first.split(/\.\s|\n/)[0]
}
return first;
}
lines = readme.split('\n')
docs = [];
lines.forEach(function (line, i) {
if(!(line[0] === '#' && ~line.indexOf('.'))) return;
var doc = new Doc(line, i, docs.length);
docs.push(doc);
});
var _ = require('underscore');
var sh = require('shelljs');
var byName = _.groupBy(docs, function (doc) {
return doc.name;
});
sh.rm('-rf', 'g-tests');
sh.mkdir('g-tests');
Object.keys(byName).forEach(function (group) {
var testFile = [
"describe('" + group + "', function() {",
""];
byName[group].forEach(function (doc) {
var example = doc.example();
var exampleLines = example && example.length && example;
testFile = testFile.concat([
" describe('" + doc.line + "', function() {",
" it(\"" + doc.desc() + "\", function(done) {"]);
if(exampleLines) {
exampleLines.unshift("/* example - ");
exampleLines.push("*/")
testFile = testFile.concat(
exampleLines.map(function (l) {
return ' ' + l;
})
)
}
testFile.push(
" done(new Error('test not implemented'));",
" });",
" });",
"});"
);
});
testFile.join('\n').to('g-tests/' + group + '.test.js');
});

View File

@ -2,4 +2,11 @@
* asteroid ~ public api
*/
module.exports = require('./lib/asteroid');
var asteroid = module.exports = require('./lib/asteroid');
/**
* Connectors
*/
asteroid.Connector = require('./lib/connectors/base-connector');
asteroid.Memory = require('./lib/connectors/memory');

View File

@ -2,12 +2,10 @@
* Module dependencies.
*/
var Model = require('../node_modules/model/lib/model')
, DataSource = require('jugglingdb').DataSource
var DataSource = require('jugglingdb').DataSource
, ModelBuilder = require('jugglingdb').ModelBuilder
, assert = require('assert')
, RemoteObjects = require('sl-remoting')
, i8n = require('inflection');
, RemoteObjects = require('sl-remoting');
/**
* Export the app prototype.
@ -50,124 +48,33 @@ app.modelBuilder = function () {
}
/**
* Define a model.
*
* @param name {String}
* @param options {Object}
* @returns {Model}
* App models.
*/
app.model =
app.defineModel =
app.define = function (name, properties, options) {
var modelBuilder = this.modelBuilder();
var ModelCtor = modelBuilder.define(name, properties, options);
app._models = [];
ModelCtor.dataSource = function (name) {
var dataSource = app.dataSources[name];
/**
* Expose a model.
*
* @param Model {Model}
*/
dataSource.attach(this);
var hasMany = ModelCtor.hasMany;
if(!hasMany) return;
// override the default relations to add shared proxy methods
// cannot expose the relation methods since they are only defined
// once you get them (eg. prototype[name])
ModelCtor.hasMany = function (anotherClass, params) {
var origArgs = arguments;
var thisClass = this, thisClassName = this.modelName;
params = params || {};
if (typeof anotherClass === 'string') {
params.as = anotherClass;
if (params.model) {
anotherClass = params.model;
} else {
var anotherClassName = i8n.singularize(anotherClass).toLowerCase();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
app.model = function (Model) {
this._models.push(Model);
Model.app = this;
if(Model._remoteHooks) {
Model._remoteHooks.emit('attached', app);
}
}
}
}
var pluralized = i8n.pluralize(anotherClass.modelName);
var methodName = params.as ||
i8n.camelize(pluralized, true);
var proxyMethodName = 'get' + i8n.titleize(pluralized, true);
// create a proxy method
var fn = this.prototype[proxyMethodName] = function () {
// this cannot be a shared method
// because it is defined when you
// inside a property getter...
this[methodName].apply(thisClass, arguments);
};
fn.shared = true;
fn.http = {verb: 'get', path: '/' + methodName};
hasMany.apply(this, arguments);
};
};
ModelCtor.shared = true;
ModelCtor.sharedCtor = function (id, fn) {
if(id) {
ModelCtor.find(id, fn);
} else {
fn(null, new ModelCtor(data));
}
};
ModelCtor.sharedCtor.accepts = [
// todo... models need to expose what id type they need
{arg: 'id', type: 'any'},
{arg: 'data', type: 'object'}
];
ModelCtor.sharedCtor.http = [
{path: '/'},
{path: '/:id'}
];
return (app._models[ModelCtor.pluralModelName] = ModelCtor);
}
/**
* Get all models.
* Get all exposed models.
*/
app.models = function () {
var models = this._models;
var result = {};
var dataSources = this.dataSources;
// add in any model from a data source
Object.keys(this.dataSources).forEach(function (name) {
var dataSource = dataSources[name];
Object.keys(dataSource.models).forEach(function (className) {
var model = dataSource.models[className];
result[exportedName(model)] = model;
});
});
// add in defined models
Object.keys(models).forEach(function (name) {
var model = models[name];
result[exportedName(model)] = model;
});
function exportedName(model) {
return model.pluralModelName || i8n.pluralize(model.modelName);
}
return result;
return this._models;
}
/**
* Get all remote objects.
*/
@ -177,26 +84,16 @@ app.remoteObjects = function () {
var models = this.models();
// add in models
Object.keys(models)
.forEach(function (name) {
var ModelCtor = models[name];
models.forEach(function (ModelCtor) {
// only add shared models
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
result[name] = ModelCtor;
result[ModelCtor.pluralModelName] = ModelCtor;
}
});
return result;
}
/**
* App data sources and models.
*/
app._models = {};
app.dataSources = {};
/**
* Get the apps set of remote objects.
*/
@ -204,16 +101,3 @@ app.dataSources = {};
app.remotes = function () {
return this._remotes || (this._remotes = RemoteObjects.create());
}
/**
* Attach a remote data source.
*
* @param name {String}
* @param options {Object}
* @returns {DataSource}
*/
app.dataSource = function (name, options) {
var dataSources = this.dataSources || (this.dataSources = {});
return (dataSources[name] = new DataSource(options.adapter, options));
}

View File

@ -4,9 +4,14 @@
var express = require('express')
, fs = require('fs')
, EventEmitter = require('events').EventEmitter
, path = require('path')
, proto = require('./application')
, utils = require('express/node_modules/connect').utils;
, utils = require('express/node_modules/connect').utils
, DataSource = require('jugglingdb').DataSource
, ModelBuilder = require('jugglingdb').ModelBuilder
, assert = require('assert')
, i8n = require('inflection');
/**
* Expose `createApplication()`.
@ -69,7 +74,173 @@ fs.readdirSync(path.join(__dirname, 'middleware')).forEach(function (m) {
asteroid.errorHandler.title = 'Asteroid';
/**
* Define model api.
* Create a data source with passing the provided options to the connector.
*
* @param {String} name (optional)
* @param {Object} options
*
* - connector - an asteroid connector
* - other values - see the specified `connector` docs
*/
asteroid.createDataSource = function (name, options) {
var ds = new DataSource(name, options);
ds.createModel = function (name, properties, settings) {
var ModelCtor = asteroid.createModel(name, properties, settings);
ModelCtor.attachTo(ds);
var hasMany = ModelCtor.hasMany;
if(hasMany) {
ModelCtor.hasMany = function (anotherClass, params) {
var origArgs = arguments;
var thisClass = this, thisClassName = this.modelName;
params = params || {};
if (typeof anotherClass === 'string') {
params.as = anotherClass;
if (params.model) {
anotherClass = params.model;
} else {
var anotherClassName = i8n.singularize(anotherClass).toLowerCase();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
}
}
}
}
var pluralized = i8n.pluralize(anotherClass.modelName);
var methodName = params.as ||
i8n.camelize(pluralized, true);
var proxyMethodName = 'get' + i8n.titleize(pluralized, true);
// create a proxy method
var fn = this.prototype[proxyMethodName] = function () {
// this[methodName] cannot be a shared method
// because it is defined inside
// a property getter...
this[methodName].apply(thisClass, arguments);
};
fn.shared = true;
fn.http = {verb: 'get', path: '/' + methodName};
fn.accepts = {arg: 'where', type: 'object'};
hasMany.apply(this, arguments);
};
}
return ModelCtor;
}
return ds;
}
/**
* Create a named vanilla JavaScript class constructor with an attached set of properties and options.
*
* @param {String} name - must be unique
* @param {Object} properties
* @param {Object} options (optional)
*/
asteroid.createModel = function (name, properties, options) {
assert(typeof name === 'string', 'Cannot create a model without a name');
var mb = new ModelBuilder();
var ModelCtor = mb.define(name, properties, arguments);
ModelCtor.shared = true;
ModelCtor.sharedCtor = function (data, id, fn) {
if(typeof data === 'function') {
fn = data;
data = null;
id = null;
} else if (typeof id === 'function') {
fn = id;
if(typeof data !== 'object') {
id = data;
data = null;
} else {
id = null;
}
}
if(id && data) {
var model = new ModelCtor(data);
model.id = id;
fn(null, model);
} else if(data) {
fn(null, new ModelCtor(data));
} else if(id) {
ModelCtor.find(id, fn);
} else {
fn(new Error('must specify an id or data'));
}
};
ModelCtor.sharedCtor.accepts = [
{arg: 'data', type: 'object'},
{arg: 'id', type: 'any'}
];
ModelCtor.sharedCtor.http = [
{path: '/'},
{path: '/:id'}
];
// before remote hook
ModelCtor.beforeRemote = function (name, fn) {
var self = this;
if(this.app) {
var remotes = this.app.remotes();
remotes.before(self.pluralModelName + '.' + name, function (ctx, next) {
fn(ctx, ctx.instance, next);
});
} else {
var args = arguments;
this._remoteHooks.once('attached', function () {
self.beforeRemote.apply(ModelCtor, args);
});
}
}
// after remote hook
ModelCtor.afterRemote = function (name, fn) {
var self = this;
if(this.app) {
var remotes = this.app.remotes();
remotes.after(self.pluralModelName + '.' + name, function (ctx, next) {
fn(ctx, ctx.instance, next);
});
} else {
var args = arguments;
this._remoteHooks.once('attached', function () {
self.afterRemote.apply(ModelCtor, args);
});
}
}
// allow hooks to be added before attaching to an app
ModelCtor._remoteHooks = new EventEmitter();
return ModelCtor;
}
/**
* Add a remote method to a model.
* @param {Function} fn
* @param {Object} options (optional)
*/
asteroid.remoteMethod = function (fn, options) {
fn.shared = true;
if(typeof options === 'object') {
Object.keys(options).forEach(function (key) {
fn[key] = options[key];
});
}
fn.http = fn.http || {verb: 'get'};
}

View File

@ -0,0 +1,54 @@
/**
* Expose `Connector`.
*/
module.exports = Connector;
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, debug = require('debug')('connector')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `Connector` with the given `options`.
*
* @param {Object} options
* @return {Connector}
*/
function Connector(options) {
EventEmitter.apply(this, arguments);
this.options = options;
debug('created with options', options);
}
/**
* Inherit from `EventEmitter`.
*/
inherits(Connector, EventEmitter);
/*!
* Create an adapter instance from a JugglingDB adapter.
*/
Connector._createJDBAdapter = function (jdbModule) {
var fauxSchema = {};
jdbModule.initialize(fauxSchema, function () {
// connected
});
}
/*!
* Add default crud operations from a JugglingDB adapter.
*/
Connector.prototype._addCrudOperationsFromJDBAdapter = function (adapter) {
}

39
lib/connectors/memory.js Normal file
View File

@ -0,0 +1,39 @@
/**
* Expose `Memory`.
*/
module.exports = Memory;
/**
* Module dependencies.
*/
var Connector = require('./base-connector')
, debug = require('debug')('memory')
, util = require('util')
, inherits = util.inherits
, assert = require('assert')
, JdbMemory = require('jugglingdb/lib/adapters/memory');
/**
* Create a new `Memory` connector with the given `options`.
*
* @param {Object} options
* @return {Memory}
*/
function Memory() {
// TODO implement entire memory adapter
}
/**
* Inherit from `DBConnector`.
*/
inherits(Memory, Connector);
/**
* JugglingDB Compatibility
*/
Memory.initialize = JdbMemory.initialize;

View File

@ -1,67 +0,0 @@
/**
* Module dependencies.
*/
var ModuleLoader = require('sl-module-loader')
, path = require('path');
/**
* Export the middleware.
*/
module.exports = configure;
/**
* Load application modules based on the current directories configuration files.
*/
function configure(root) {
var moduleLoader = configure.createModuleLoader(root);
var app = this;
process.__asteroidCache = {};
return function configureMiddleware(req, res, next) {
var modules = req.modules = res.modules = moduleLoader;
moduleLoader.load(function (err) {
if(err) {
next(err);
} else {
var models = modules.instanceOf('ModelConfiguration');
var dataSources = modules.instanceOf('DataSource');
// define models from config
models.forEach(function (model) {
app.models[model.options.name] = model.ModelCtor;
});
// define data sources from config
dataSources.forEach(function (dataSource) {
app.dataSources[dataSources.options.name] = dataSource;
});
next();
}
});
}
}
configure.createModuleLoader = function (root) {
var options = {
alias: BUNDLED_MODULE_ALIAS
};
return ModuleLoader.create(root || '.', options);
};
/**
* Turn asteroid bundled deps into aliases for the module loader
*/
var BUNDLED_MODULE_ALIAS = require('../../package.json')
.bundleDependencies
.reduce(function (prev, cur) {
prev[cur] = path.join('asteroid', 'node_modules', cur);
return prev;
}, {});

1
node_modules/.bin/_mocha generated vendored
View File

@ -1 +0,0 @@
../mocha/bin/_mocha

1
node_modules/.bin/express generated vendored
View File

@ -1 +0,0 @@
../express/bin/express

1
node_modules/.bin/mocha generated vendored
View File

@ -1 +0,0 @@
../mocha/bin/mocha

View File

@ -1,10 +0,0 @@
.DS_Store
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.swp
*.swo
node_modules/

View File

@ -1,73 +0,0 @@
# asteroid-module
v0.0.1
## About
Asteroid applications are a combination of regular Node.js modules and Asteroid modules. Asteroid modules may be initialized using JavaScript or by writing config files.
## Using Asteroid Modules
There are two distinct ways to use an Asteroid Module in your application.
### App API
The `app` API allows you to define [data sources](../data-source) and [models](../model) in regular Node JavaScript. [See the docs for more info](../../readme.md#API).
### Config Files
You may also define [data sources](../data-source), [models](../model) and other asteroid modules by writing `config.json` files. See the documentation for a given module to see what config data it requires.
## Extending Asteroid
The core of asteroid is very lightweight and unopionated. All features are added on as `AsteroidModule`s. This means you can add your own functionality, modify existing functionality, or extend existing functionality by creating your own `AsteroidModule` class.
An `AsteroidModule` is an abstract class that provides a base for all asteroid modules. Its constructor takes an `options` argument provided by a `config.json`. It is also supplied with dependencies it lists on its constructor based on information in the `config.json` file.
See [model](../model) for an example.
### AsteroidModule.dependencies
An asteroid module may define dependencies on other modules that can be configured in `config.json`. Eg. the [collection](../collection/lib/collection.js) module defines a [model](../model) dependency.
Collection.dependencies = {
model: 'model'
}
A configuration then must define:
{
"dependencies": {
"model": "some-model-module"
}
}
Where `some-model-module` is an existing `model` instance.
### AsteroidModule.options
Asteroid Modules may also describe the options they accept. This will validate the configuration and make sure users have supplied required information and in a way that the module can use to construct a working instance.
Here is an example options description for the [oracle database connection module](../connections/oracle-connection).
OracleConnection.options = {
'hostname': {type: 'string', required: true},
'port': {type: 'number', min: 10, max: 99999},
'username': {type: 'string'},
'password': {type: 'string'}
};
**key** the option name given in `config.json`.
**type** must be one of:
- string
- boolean
- number
- array
**min/max** depend on the type
{
min: 10, // minimum length or value
max: 100, // max length or value
}

View File

@ -1,5 +0,0 @@
/**
* asteroid-module ~ public api
*/
module.exports = require('./lib/asteroid-module');

View File

@ -1,37 +0,0 @@
/**
* Expose `AsteroidModule`.
*/
module.exports = AsteroidModule;
/**
* Module dependencies.
*/
var Module = require('sl-module-loader').Module
, debug = require('debug')('asteroid-module')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `AsteroidModule` with the given `options`.
*
* @param {Object} options
* @return {AsteroidModule}
*/
function AsteroidModule(options) {
Module.apply(this, arguments);
// throw an error if args are not supplied
assert(typeof options === 'object', 'AsteroidModule requires an options object');
debug('created with options', options);
}
/**
* Inherit from `Module`.
*/
inherits(AsteroidModule, Module);

View File

@ -1,14 +0,0 @@
{
"name": "asteroid-module",
"description": "asteroid-module",
"version": "0.0.1",
"scripts": {
"test": "mocha"
},
"dependencies": {
"debug": "latest"
},
"devDependencies": {
"mocha": "latest"
}
}

View File

@ -1,24 +0,0 @@
var AsteroidModule = require('../');
describe('AsteroidModule', function(){
var asteroidModule;
beforeEach(function(){
asteroidModule = new AsteroidModule;
});
describe('.myMethod', function(){
// example sync test
it('should <description of behavior>', function() {
asteroidModule.myMethod();
});
// example async test
it('should <description of behavior>', function(done) {
setTimeout(function () {
asteroidModule.myMethod();
done();
}, 0);
});
});
});

View File

@ -1,5 +0,0 @@
/**
* asteroid-module test setup and support.
*/
assert = require('assert');

10
node_modules/data-source/.gitignore generated vendored
View File

@ -1,10 +0,0 @@
.DS_Store
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.swp
*.swo
node_modules/

11
node_modules/data-source/README.md generated vendored
View File

@ -1,11 +0,0 @@
# data-source
## About
A `DataSource` is the base `AsteroidModule` class for all data sources. Data sources provide APIs for reading and writing remote data from databases and various http apis.
### Creating a custom Data Source
To create a custom data source you must define a class that inherits from `DataSource`. This class should define all the required options using the [asteroid module option api](../asteroid-module) (eg. host, port, username, etc).
The inherited class must provide a property `adapter` that points to a [jugglingdb adapter](https://github.com/1602/jugglingdb#jugglingdb-adapters).

5
node_modules/data-source/index.js generated vendored
View File

@ -1,5 +0,0 @@
/**
* connection ~ public api
*/
module.exports = require('./lib/data-source');

View File

@ -1,40 +0,0 @@
/**
* Expose `DataSource`.
*/
module.exports = DataSource;
/**
* Module dependencies.
*/
var AsteroidModule = require('asteroid-module')
, Schema = require('jugglingdb').Schema
, debug = require('debug')('connection')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `DataSource` with the given `options`.
*
* @param {Object} options
* @return {DataSource}
*/
function DataSource(options) {
AsteroidModule.apply(this, arguments);
this.options = options;
// construct a schema with the available adapter
// or use the default in memory adapter
this.schema = new Schema(this.adapter || require('./memory'), options);
debug('created with options', options);
}
/**
* Inherit from `AsteroidModule`.
*/
inherits(DataSource, AsteroidModule);

View File

@ -1,260 +0,0 @@
exports.initialize = function initializeSchema(schema, callback) {
schema.adapter = new Memory();
schema.adapter.connect(callback);
};
function Memory(m) {
if (m) {
this.isTransaction = true;
this.cache = m.cache;
this.ids = m.ids;
this._models = m._models;
} else {
this.isTransaction = false;
// use asteroid cache, otherwise state will be reset during configuration
this.cache = process.__asteroidCache.memoryStore || (process.__asteroidCache.memoryStore = {});
this.ids = {};
this._models = {};
}
}
Memory.prototype.connect = function(callback) {
if (this.isTransaction) {
this.onTransactionExec = callback;
} else {
process.nextTick(callback);
}
};
Memory.prototype.define = function defineModel(descr) {
var m = descr.model.modelName;
this._models[m] = descr;
// allow reuse of data
this.cache[m] = this.cache[m] || {};
this.ids[m] = 1;
};
Memory.prototype.create = function create(model, data, callback) {
var id = data.id || this.ids[model]++;
data.id = id;
this.cache[model][id] = JSON.stringify(data);
process.nextTick(function() {
callback(null, id);
});
};
Memory.prototype.updateOrCreate = function (model, data, callback) {
var mem = this;
this.exists(model, data.id, function (err, exists) {
if (exists) {
mem.save(model, data, callback);
} else {
mem.create(model, data, function (err, id) {
data.id = id;
callback(err, data);
});
}
});
};
Memory.prototype.save = function save(model, data, callback) {
this.cache[model][data.id] = JSON.stringify(data);
process.nextTick(function () {
callback(null, data);
});
};
Memory.prototype.exists = function exists(model, id, callback) {
process.nextTick(function () {
callback(null, this.cache[model].hasOwnProperty(id));
}.bind(this));
};
Memory.prototype.find = function find(model, id, callback) {
process.nextTick(function () {
callback(null, id in this.cache[model] && this.fromDb(model, this.cache[model][id]));
}.bind(this));
};
Memory.prototype.destroy = function destroy(model, id, callback) {
delete this.cache[model][id];
process.nextTick(callback);
};
Memory.prototype.fromDb = function(model, data) {
if (!data) return null;
data = JSON.parse(data);
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
var val = data[key];
if (typeof val === 'undefined' || val === null) {
return;
}
if (props[key]) {
switch(props[key].type.name) {
case 'Date':
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
break;
case 'Boolean':
val = new Boolean(val);
break;
}
}
data[key] = val;
});
return data;
};
Memory.prototype.all = function all(model, filter, callback) {
var self = this;
var nodes = [];
var data = this.cache[model];
var keys = Object.keys(data);
var scanned = 0;
while(scanned < keys.length) {
nodes.push(this.fromDb(model, data[keys[scanned]]));
scanned++;
}
if (filter) {
// do we need some sorting?
if (filter.order) {
var props = this._models[model].properties;
var orders = filter.order;
if (typeof filter.order === "string") {
orders = [filter.order];
}
orders.forEach(function (key, i) {
var reverse = 1;
var m = key.match(/\s+(A|DE)SC$/i);
if (m) {
key = key.replace(/\s+(A|DE)SC/i, '');
if (m[1].toLowerCase() === 'de') reverse = -1;
}
orders[i] = {"key": key, "reverse": reverse};
});
nodes = nodes.sort(sorting.bind(orders));
}
// do we need some filtration?
if (filter.where) {
nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes;
}
// skip
if(filter.skip) {
nodes = nodes.slice(filter.skip, nodes.length);
}
if(filter.limit) {
nodes = nodes.slice(0, filter.limit);
}
}
process.nextTick(function () {
if (filter && filter.include) {
self._models[model].model.include(nodes, filter.include, callback);
} else {
callback(null, nodes);
}
});
function sorting(a, b) {
for (var i=0, l=this.length; i<l; i++) {
if (a[this[i].key] > b[this[i].key]) {
return 1*this[i].reverse;
} else if (a[this[i].key] < b[this[i].key]) {
return -1*this[i].reverse;
}
}
return 0;
}
};
function applyFilter(filter) {
if (typeof filter.where === 'function') {
return filter.where;
}
var keys = Object.keys(filter.where);
return function (obj) {
var pass = true;
keys.forEach(function (key) {
if (!test(filter.where[key], obj[key])) {
pass = false;
}
});
return pass;
}
function test(example, value) {
if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
return value.match(example);
}
if (typeof example === 'undefined') return undefined;
if (typeof value === 'undefined') return undefined;
if (typeof example === 'object') {
if (example.inq) {
if (!value) return false;
for (var i = 0; i < example.inq.length; i += 1) {
if (example.inq[i] == value) return true;
}
return false;
}
}
// not strict equality
return (example !== null ? example.toString() : example) == (value !== null ? value.toString() : value);
}
}
Memory.prototype.destroyAll = function destroyAll(model, callback) {
Object.keys(this.cache[model]).forEach(function (id) {
delete this.cache[model][id];
}.bind(this));
this.cache[model] = {};
process.nextTick(callback);
};
Memory.prototype.count = function count(model, callback, where) {
var cache = this.cache[model];
var data = Object.keys(cache)
if (where) {
data = data.filter(function (id) {
var ok = true;
Object.keys(where).forEach(function (key) {
if (JSON.parse(cache[id])[key] != where[key]) {
ok = false;
}
});
return ok;
});
}
process.nextTick(function () {
callback(null, data.length);
});
};
Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) {
data.id = id;
var base = JSON.parse(this.cache[model][id]);
this.save(model, merge(base, data), cb);
};
Memory.prototype.transaction = function () {
return new Memory(this);
};
Memory.prototype.exec = function(callback) {
this.onTransactionExec();
setTimeout(callback, 50);
};
function merge(base, update) {
if (!base) return update;
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
return base;
}

View File

@ -1,15 +0,0 @@
{
"name": "data-source",
"description": "data-source",
"version": "0.0.1",
"scripts": {
"test": "mocha"
},
"dependencies": {
"debug": "latest",
"jugglingdb": "~0.2.0-30"
},
"devDependencies": {
"mocha": "latest"
}
}

View File

@ -1,24 +0,0 @@
var DataSource = require('../');
describe('DataSource', function(){
var connection;
beforeEach(function(){
dataSource = new DataSource;
});
describe('.myMethod', function(){
// example sync test
it('should <description of behavior>', function() {
dataSource.myMethod();
});
// example async test
it('should <description of behavior>', function(done) {
setTimeout(function () {
dataSource.myMethod();
done();
}, 0);
});
});
});

View File

@ -1,5 +0,0 @@
/**
* connection test setup and support.
*/
assert = require('assert');

10
node_modules/model/.gitignore generated vendored
View File

@ -1,10 +0,0 @@
.DS_Store
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.swp
*.swo
node_modules/

255
node_modules/model/README.md generated vendored
View File

@ -1,255 +0,0 @@
# model
## About
A `Model` represents the data of your application. Asteroid `Model`s are mostly used for validating interactions with a [DataSource](../data-source). Usually your models will correspond to a namespace in your data source (database table, http url, etc). The bulk of your application's business logic will be in your `Model` or Node.js scripts that require your model.
## Data Definition Language
TODO ~ document
## API
### Defining Models
The following assumes your have reference to a class that inherits from `Model`. The simplest way to get this is by using the [app API](../../readme.md#API).
// define a model class using the app api
var Color = app.define('color');
// provide an exact plural name
var Color = app.define('color', {plural: 'colors'});
**Note:** If a plural name is not defined, the model will try to pluralize the singular form.
#### MyModel.defineSchema(schema)
Define the data the model represents using the data definition language.
// define the color model
var Color = app.define('color');
// define the schema for the Color model
Color.defineSchema({
name: 'string',
id: 'uid',
tweets: 'array'
});
##### MyModel.dataSource(name, namespace)
Set the data source for the model. Must provide a name of an existing data source. If the `namespace` is not provided the plural model name (eg. `colors`) will be used.
// set the data source
Color.dataSource('color-db', 'COLOR_NAMES');
**Note:** If you do not set a data source or a map (or both) the default data source will be used (an in memory database).
#### MyModel.defineMap(map)
Define a mapping between the data source representation of your data and your app's representation.
// manually map Color to existing table columns
Color.defineMap({
dataSource: 'color-db', // optional, will use model's data source
table: 'COLOR_NAMES', // required
map: { // optional if schema defined
id: 'COLOR_ID',
name: 'COLOR_NAME'
}
});
// mix in a mapping from another data source
Color.defineMap({
dataSource: 'twitter',
url: function(color) {
return '/search?limit=5&q=' + color.name;
},
map: {
// provides the color.tweets property
tweets: function(tweets) {
return tweets;
}
}
});
**Note:** You may define multiple maps for a single model. The model will combine the data for you.
#### MyModel.discoverSchema(fn)
Using the mapped data source, try to discover the schema of a table or other namespace (url, collection, etc).
// use existing schema to map to desired properties
Color.dataSource('color-db', 'COLOR_NAMES');
Color.discoverSchema(function (err, oracleSchema) {
var schema = {tweets: 'array'};
var map = {dataSource: 'color-db', table: 'COLOR_NAMES'};
// inspect the existing table schema to create a mapping
Object
.keys(oracleSchema)
.forEach(function (oracleProperty) {
// remove prefix
var property = oracleProperty.toLowerCase().split('_')[0];
// build new schema
schema[property] = oracleProperty[oracleProperty];
// create mapping to existing schema
map[property] = oracleProperty;
});
Color.defineSchema(schema);
Color.defineMap(map);
});
### Custom Methods
There are two types of custom methods. Static and instance.
**Static**
Static methods are available on the Model class itself and are used to operate on many models at the same time.
**Instance**
Instance methods are available on an instance of a Model and usually act on a single model at a time.
#### Defining a Static Method
The following example shows how to define a simple static method.
Color.myStaticMethod = function() {
// only has access to other static methods
this.find(function(err, colors) {
console.log(colors); // [...]
});
}
#### Defining an Instance Method
The following is an example of a simple instance method.
Color.prototype.myInstanceMethod = function() {
console.log(this.name); // red
}
#### Remotable Methods
Both types of methods may be set as `remotable` as long as they conform to the remotable requirements. Asteroid will expose these methods over the network for you.
##### Remotable Requirements
Static and instance methods must accept a callback as the last argument. This callback must be called with an error as the first argument and the results as arguments afterward.
You must also define the input and output of your remoteable method. Describe the input or arguments of the function by providing an `accepts` array and describe the output by defining a `returns` array.
// this method meets the remotable requirements
Color.getByName = function (name, callback) {
Color.find({where: {name: name}}, function (err, colors) {
// if an error occurs callback with the error
if(err) {
callback(err);
} else {
callback(null, colors);
}
});
}
// accepts a name of type string
Color.getByName.accepts = [
{arg: 'name', type: 'String'} // data definition language
];
// returns an array of type Color
Color.getByName.returns = [
{arg: 'colors', type: ['Color']} // data definition language
];
**Note:** any types included in `accepts`/`returns` must be native JavaScript types or Model classes.
### Working with Models
The following assumes you have access to an instance of a `Model` class.
// define a model
var Color = app.define('color');
// create an instance
var color = new Color({name: red});
#### myModel.save([options], [callback])
**Remoteable**
Save the model using its configured data source.
var color = new Color();
color.name = 'green';
// fire and forget
color.save();
// callback
color.save(function(err, color) {
if(err) {
console.log(err); // validation or other error
} else {
console.log(color); // updated with id
}
});
#### myModel.destroy([callback])
**Remoteable**
Delete the instance using attached data source. Invoke callback when ready.
var color = Color.create({id: 10});
color.destroy(function(err) {
if(err) {
console.log(err); // could not destroy
} else {
console.log('model has been destroyed!');
}
});
#### MyModel.all()
#### MyModel.find()
#### MyModel.count()
### Model Relationships
## Config
### Options
#### namespace
A table, collection, url, or other namespace.
#### properties
An array of properties describing the model's schema.
"properties": [
{
"name": "title",
"type": "string"
},
{
"name": "done",
"type": "boolean"
},
{
"name": "order",
"type": "number"
}
]
}
### Dependencies
#### data source
The name of a data source [data-source](../data-source) for persisting data.

View File

@ -1,12 +0,0 @@
/**
* A generated `Model` example...
*
* Examples should show a working module api
* and be used in tests to continously check
* they function as expected.
*/
var Model = require('../');
var model = Model.create();
model.myMethod();

5
node_modules/model/index.js generated vendored
View File

@ -1,5 +0,0 @@
/**
* model ~ public api
*/
module.exports = require('./lib/model-configuration');

View File

@ -1,79 +0,0 @@
/**
* Expose `ModelConfiguration`.
*/
module.exports = ModelConfiguration;
/**
* Module dependencies.
*/
var AsteroidModule = require('asteroid-module')
, Model = require('./model')
, debug = require('debug')('model-configuration')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `ModelConfiguration` with the given `options`.
*
* @param {Object} options
* @return {Model}
*/
function ModelConfiguration(options) {
AsteroidModule.apply(this, arguments);
this.options = options;
var dependencies = this.dependencies;
var dataSource = dependencies['data-source'];
var schema = this.schema = dataSource.schema;
assert(Array.isArray(options.properties), 'the ' + options._name + ' model requires an options.properties array');
// define model
var ModelCtor = this.ModelCtor = this.BaseModel.extend(options);
assert(dataSource.name, 'cannot map a model to a datasource without a name');
// define provided mappings
if(options.maps) {
options.maps.forEach(function (config) {
assert(config.dataSource, 'Model config.options.maps requires a `dataSource` when defining maps');
assert(config.map, 'Model config.options.maps requires a `map` when defining maps');
ModelCtor.defineMap(dataSource.name, config);
});
}
}
/**
* Inherit from `AsteroidModule`.
*/
inherits(ModelConfiguration, AsteroidModule);
/**
* The model to extend (should be overridden in sub classes).
*/
ModelConfiguration.prototype.BaseModel = Model;
/**
* Dependencies.
*/
ModelConfiguration.dependencies = {
'data-source': 'data-source'
};
/**
* Options.
*/
ModelConfiguration.options = {
'name': {type: 'string'},
'properties': {type: 'array'},
'maps': {type: 'array'}
}

121
node_modules/model/lib/model.js generated vendored
View File

@ -1,121 +0,0 @@
/**
* Expose `Model`.
*/
module.exports = Model;
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, debug = require('debug')('model')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `Model` with the given `options`.
*
* @param {Object} options
* @return {Model}
*/
function Model(data) {
EventEmitter.call(this);
var ModelCtor = this.constructor;
var schema = ModelCtor.schema;
// get properties that match the schema
var matchedProperties = schema.getMatchedProperties(data);
// set properties that match the schema
Object.keys(matchedProperties).forEach(function (property) {
this[property] = matchedProperties[property];
}.bind(this));
}
/**
* Inherit from `EventEmitter`.
*/
inherits(Model, EventEmitter);
/**
* Create a new Model class from the given options.
*
* @param options {Object}
* @return {Model}
*/
Model.extend = function (options) {
var Super = this;
// the new constructor
function Model() {
Super.apply(this, arguments);
}
Model.options = options;
assert(options.name, 'must provide a name when extending from model');
// model namespace
Model.namespace = options.name;
// define the remote namespace
Model.remoteNamespace = options.plural || pluralize(Model.namespace);
// inherit all static methods
Object.keys(Super).forEach(function (key) {
if(typeof Super[key] === 'function') {
MyModel[key] = Super[key];
}
});
// inherit all other things
inherits(MyModel, Super);
return Model;
}
/**
* Construct a model instance for remote use.
*/
Model.sharedCtor = function (data, fn) {
var ModelCtor = this;
fn(null, new ModelCtor(data));
}
/**
* Define the data the model represents using the data definition language.
*/
Model.defineSchema = function (schema) {
throw new Error('not implemented');
}
/**
* Set the data source for the model. Must provide a name of an existing data source.
* If the namespace is not provided the plural model name (eg. colors) will be used.
*
* **Note:** If you do not set a data source or a map (or both) the default data source
* will be used (an in memory database).
*/
Model.dataSource = function (dataSourceName, namespace) {
namespace = namespace || this.namespace;
throw new Error('not implemented');
}
/**
* Define a mapping between the data source representation of your data and your app's representation.
*/
Model.defineMap = function (mapping) {
// see: https://github.com/strongloop/asteroid/tree/master/node_modules/model#mymodeldefinemapmap
throw new Error('not implemented');
}

14
node_modules/model/package.json generated vendored
View File

@ -1,14 +0,0 @@
{
"name": "model",
"description": "model",
"version": "0.0.1",
"scripts": {
"test": "mocha"
},
"dependencies": {
"debug": "latest"
},
"devDependencies": {
"mocha": "latest"
}
}

View File

@ -1,24 +0,0 @@
var Model = require('../');
describe('Model', function(){
var model;
beforeEach(function(){
model = new Model;
});
describe('.myMethod', function(){
// example sync test
it('should <description of behavior>', function() {
model.myMethod();
});
// example async test
it('should <description of behavior>', function(done) {
setTimeout(function () {
model.myMethod();
done();
}, 0);
});
});
});

5
node_modules/model/test/support.js generated vendored
View File

@ -1,5 +0,0 @@
/**
* model test setup and support.
*/
assert = require('assert');

View File

@ -1,10 +0,0 @@
.DS_Store
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.swp
*.swo
node_modules/

View File

@ -1,12 +0,0 @@
# oracle-data-source
## About
Configures the oracle adapter for use in a [data store](../../store).
### Options
#### hostname
#### port
#### username
#### password

View File

@ -1,5 +0,0 @@
/**
* connection ~ public api
*/
module.exports = require('./lib/oracle-data-source');

View File

@ -1,51 +0,0 @@
/**
* Expose `OracleDataSource`.
*/
module.exports = OracleDataSource;
/**
* Module dependencies.
*/
var DataSource = require('data-source')
, debug = require('debug')('oracle-data-source')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `OracleDataSource` with the given `options`.
*
* @param {Object} options
* @return {DataSource}
*/
function OracleDataSource(options) {
DataSource.apply(this, arguments);
debug('created with options', options);
}
/**
* Inherit from `AsteroidModule`.
*/
inherits(OracleDataSource, DataSource);
/**
* Define options.
*/
OracleDataSource.options = {
'database': {type: 'string', required: true},
'host': {type: 'string', required: true},
'port': {type: 'number', min: 10, max: 99999},
'username': {type: 'string'},
'password': {type: 'string'}
};
/**
* Provide the oracle jugglingdb adapter
*/
OracleDataSource.prototype.adapter = require('jugglingdb-oracle');

View File

@ -1,15 +0,0 @@
{
"name": "oracle-data-source",
"description": "oracle-data-source",
"version": "0.0.1",
"scripts": {
"test": "mocha"
},
"dependencies": {
"jugglingdb-oracle": "latest",
"debug": "latest"
},
"devDependencies": {
"mocha": "latest"
}
}

10
node_modules/route/.gitignore generated vendored
View File

@ -1,10 +0,0 @@
.DS_Store
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.swp
*.swo
node_modules/

73
node_modules/route/README.md generated vendored
View File

@ -1,73 +0,0 @@
# asteroid.Route
## About
A `Route` inherits from the [asteroid module](../asteroid-module) class. It wraps an asteroid application so that it can be used as a sub application initialized by a configuration file.
This example shows the basic usage of a `Route` as a sub application. You should never have to write this code since the route will be created and mounted for you by asteroid.
var asteroid = require('asteroid');
var Route = require('route');
var app = asteroid();
var subApp = new Route({root: '/my-sub-app'});
subApp.mount(app);
subApp.get('/', function (req, res) {
res.send(req.url); // /my-sub-app
});
app.listen(3000);
## route.app
Each route is constructed with a asteroid/express sub app at the path provided in the route's `config.json` options.
### myRoute.app.VERB(path, [callback...], callback)
The `myRoute.VERB()` methods provide routing functionality inherited from [Express](http://expressjs.com/api.html#app.get), where **VERB** is one of the HTTP verbs, such as `myRoute.post()`. See the [Express docs](http://expressjs.com/api.html#app.get) for more info.
**Examples**
myRoute.get('/hello-world', function(req, res) {
res.send('hello world');
});
### myRoute.app.use([path], middleware)
Use the given middleware function.
**Examples**
// a logger middleware
myRoute.use(function(req, res, next){
console.log(req.method, req.url); // GET /my-route
next();
});
## Config
### Options
#### path
The `asteroid.Route` path where the route will be mounted.
**Examples**
{
"options": {
"path": "/foo" // responds at /foo
}
}
<!-- ... -->
{
"options": {
"path": "/foo/:bar" // provides :bar param at `req.param('bar')`.
}
}

5
node_modules/route/index.js generated vendored
View File

@ -1,5 +0,0 @@
/**
* resource ~ public api
*/
module.exports = require('./lib/route');

View File

@ -1,114 +0,0 @@
/**
* Expose `HttpContext`.
*/
module.exports = HttpContext;
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, debug = require('debug')('http-context')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `HttpContext` with the given `options`.
*
* @param {Object} options
* @return {HttpContext}
*/
function HttpContext(resource, req, res, next) {
EventEmitter.apply(this, arguments);
this.resource = resource;
this.req = req;
this.res = res;
this.next = next;
}
/**
* Inherit from `EventEmitter`.
*/
inherits(HttpContext, EventEmitter);
/**
* Override the default emitter behavior to support async or sync hooks before and after an event.
*/
HttpContext.prototype.emit = function (ev) {
var ctx = this;
var resource = this.resource;
var origArgs = arguments;
var args = Array.prototype.slice.call(arguments, 0)
var success = arguments[arguments.length - 1];
assert(typeof success === 'function', 'ctx.emit requires a callback');
args.pop();
var evName = ev;
assert(typeof evName === 'string');
args.shift();
var listeners = resource.listeners(evName);
var listener;
// start
next();
function next(err) {
if(err) return fail(err);
try {
if(listener = listeners.shift()) {
var expectsCallback = listener._expects === args.length + 2;
// if a listener expects all the `args`
// plus ctx, and a callback
if(expectsCallback) {
// include ctx (this) and pass next to continue
listener.apply(resource, args.concat([this, next]));
} else {
// dont include the callback
listener.apply(resource, args.concat([this]));
// call next directly
next();
}
} else {
success(done);
}
} catch(e) {
fail(e);
}
}
function fail(err) {
ctx.done(err);
}
function done(err, result) {
if(err) {
return fail(err);
}
ctx.emit.apply(ctx,
['after:' + evName] // after event
.concat(args) // include original arguments/data
.concat([function () { // success callback
ctx.done.call(ctx, err, result);
}])
);
};
}
HttpContext.prototype.done = function (err, result) {
if(err) {
this.next(err);
} else {
this.res.send(result);
}
}

83
node_modules/route/lib/route.js generated vendored
View File

@ -1,83 +0,0 @@
/**
* Expose `Route`.
*/
module.exports = Route;
/**
* Module dependencies.
*/
var asteroid = require('asteroid')
, AsteroidModule = require('asteroid-module')
, HttpContext = require('./http-context')
, debug = require('debug')('asteroid:resource')
, util = require('util')
, inherits = util.inherits
, assert = require('assert');
/**
* Create a new `Route` with the given `options`.
*
* @param {Object} options
* @return {Route}
*/
function Route(options) {
AsteroidModule.apply(this, arguments);
// throw an error if args are not supplied
assert(typeof options === 'object', 'Route requires an options object');
assert(options.path, 'Route requires a path');
// create the sub app
var app = this.app = asteroid();
this.options = options;
debug('created with options', options);
this.on('destroyed', function () {
app.disuse(this.options.path);
});
}
/**
* Inherit from `AsteroidModule`.
*/
inherits(Route, AsteroidModule);
/**
* Mount the sub app on the given parent app at the configured path.
*/
Route.prototype.mount = function (parent) {
this.parent = parent;
parent.use(this.options.path, this.app);
}
/**
* Create an http context bound to the current resource.
*/
Route.prototype.createContext = function (req, res, next) {
return new HttpContext(this, req, res, next);
}
/**
* Override `on` to determine how many arguments an event handler expects.
*/
Route.prototype.on = function () {
var fn = arguments[arguments.length - 1];
if(typeof fn === 'function') {
// parse expected arguments from function src
// fn.listener handles the wrapped function during `.once()`
var src = (fn.listener || fn).toString();
fn._expects = src.split('{')[0].split(',').length;
}
AsteroidModule.prototype.on.apply(this, arguments);
}

14
node_modules/route/package.json generated vendored
View File

@ -1,14 +0,0 @@
{
"name": "resource",
"description": "resource",
"version": "0.0.1",
"scripts": {
"test": "mocha"
},
"dependencies": {
"debug": "latest"
},
"devDependencies": {
"mocha": "latest"
}
}

View File

@ -1,95 +0,0 @@
var HttpContext = require('../lib/http-context.js');
var Resource = require('../lib/resource.js');
describe('HttpContext', function(){
var ctx;
var resource;
function createRequest() {
return {};
}
function createResponse() {
return {};
}
beforeEach(function(){
resource = new Resource({path: '/foo'});
ctx = new HttpContext(resource, createRequest(), createResponse());
});
describe('.emit(ev, arg, done)', function(){
it('should emit events on a resource', function(done) {
var emitted, data;
resource.once('foo', function (arg, ctx, fn) {
emitted = true;
data = arg;
fn();
});
ctx.emit('foo', {bar: true}, function () {
assert(emitted, 'event should be emitted');
assert(data, 'arg should be supplied');
assert(data.bar, 'arg should be the correct object');
done();
});
});
it('should handle multiple args', function(done) {
var emitted, data;
resource.once('foo', function (arg1, arg2, arg3, arg4, ctx, fn) {
emitted = true;
assert(arg1 === 1, 'arg1 should equal 1');
assert(arg2 === 2, 'arg2 should equal 2');
assert(arg3 === 3, 'arg3 should equal 3');
assert(arg4 === 4, 'arg4 should equal 4');
fn();
});
ctx.emit('foo', 1, 2, 3, 4, function (fn) {
assert(emitted, 'event should be emitted');
done();
});
});
it('should have an after event', function(done) {
var emitted, emittedAfter;
ctx.done = done;
resource.once('foo', function (arg1, arg2, arg3, arg4, ctx, fn) {
emitted = true;
fn();
});
resource.once('after:foo', function (arg1, arg2, arg3, arg4, ctx, fn) {
emittedAfter = true;
fn();
});
ctx.emit('foo', 1, 2, 3, 4, function (fn) {
assert(emitted, 'event should be emitted');
fn();
});
});
it('should be able to emit synchronously', function(done) {
var emitted, data;
resource.once('foo', function (arg1, arg2, arg3, arg4, ctx) {
emitted = true;
assert(arg1 === 1, 'arg1 should equal 1');
assert(arg2 === 2, 'arg2 should equal 2');
assert(arg3 === 3, 'arg3 should equal 3');
assert(arg4 === 4, 'arg4 should equal 4');
});
ctx.emit('foo', 1, 2, 3, 4, function () {
assert(emitted);
done();
});
});
});
});

5
node_modules/route/test/support.js generated vendored
View File

@ -1,5 +0,0 @@
/**
* resource test setup and support.
*/
assert = require('assert');

View File

@ -1,30 +1,23 @@
{
"name": "asteroid",
"description": "asteroid",
"version": "0.0.1",
"version": "0.7.0",
"scripts": {
"test": "mocha"
"test": "mocha -R spec"
},
"dependencies": {
"debug": "latest",
"express": "~3.1.1",
"jugglingdb": "git+ssh://git@github.com:strongloop/jugglingdb.git",
"merge": "~1.1.0",
"sl-module-loader": "git+ssh://git@github.com:strongloop/sl-module-loader.git",
"sl-remoting": "git+ssh://git@github.com:strongloop/sl-remoting.git",
"inflection": "~1.2.5"
},
"devDependencies": {
"mocha": "latest"
"mocha": "latest",
"sl-task-emitter": "0.0.x",
"supertest": "latest"
},
"bundleDependencies": [
"asteroid-module",
"data-source",
"model",
"model-route",
"oracle-data-source",
"route"
],
"optionalDependencies": {
"jugglingdb-oracle": "git+ssh://git@github.com:strongloop/jugglingdb-oracle.git"
}

21
test/app.test.js Normal file
View File

@ -0,0 +1,21 @@
describe('app', function() {
describe('app.model(Model)', function() {
it("Expose a `Model` to remote clients.", function() {
var memory = asteroid.createDataSource({connector: asteroid.Memory});
var Color = memory.createModel('color', {name: String});
app.model(Color);
assert.equal(app.models().length, 1);
});
});
describe('app.models()', function() {
it("Get the app's exposed models.", function() {
var Color = asteroid.createModel('color', {name: String});
var models = app.models();
assert.equal(models.length, 1);
assert.equal(models[0].modelName, 'color');
});
});
});

View File

@ -1,2 +1,34 @@
var Asteroid = require('../');
describe('asteroid', function() {
describe('asteroid.createDataSource(options)', function(){
it('Create a data source with a connector.', function() {
var dataSource = asteroid.createDataSource({
connector: asteroid.Memory
});
assert(dataSource.connector());
});
});
describe('asteroid.remoteMethod(Model, fn, [options]);', function() {
it("Setup a remote method.", function() {
var Product = asteroid.createModel('product', {price: Number});
Product.stats = function(fn) {
// ...
}
asteroid.remoteMethod(
Product.stats,
{
returns: {arg: 'stats', type: 'array'},
http: {path: '/info', verb: 'get'}
}
);
assert.equal(Product.stats.returns.arg, 'stats');
assert.equal(Product.stats.returns.type, 'array');
assert.equal(Product.stats.http.path, '/info');
assert.equal(Product.stats.http.verb, 'get');
assert.equal(Product.stats.shared, true);
});
});
});

120
test/data-source.test.js Normal file
View File

@ -0,0 +1,120 @@
describe('DataSource', function() {
var memory;
beforeEach(function(){
memory = asteroid.createDataSource({
connector: asteroid.Memory
});
});
describe('dataSource.createModel(name, properties, settings)', function() {
it("Define a model and attach it to a `DataSource`.", function() {
var Color = memory.createModel('color', {name: String});
assert.isFunc(Color, 'all');
assert.isFunc(Color, 'create');
assert.isFunc(Color, 'updateOrCreate');
assert.isFunc(Color, 'upsert');
assert.isFunc(Color, 'findOrCreate');
assert.isFunc(Color, 'exists');
assert.isFunc(Color, 'find');
assert.isFunc(Color, 'findOne');
assert.isFunc(Color, 'destroyAll');
assert.isFunc(Color, 'count');
assert.isFunc(Color, 'include');
assert.isFunc(Color, 'relationNameFor');
assert.isFunc(Color, 'hasMany');
assert.isFunc(Color, 'belongsTo');
assert.isFunc(Color, 'hasAndBelongsToMany');
assert.isFunc(Color.prototype, 'save');
assert.isFunc(Color.prototype, 'isNewRecord');
assert.isFunc(Color.prototype, 'destroy');
assert.isFunc(Color.prototype, 'updateAttribute');
assert.isFunc(Color.prototype, 'updateAttributes');
assert.isFunc(Color.prototype, 'reload');
});
});
// describe('dataSource.discover(options, fn)', function() {
// it("Discover an object containing properties and settings for an existing data source.", function(done) {
// /* example -
// oracle.discover({owner: 'MYORG'}, function(err, tables) {
// var productSchema = tables.PRODUCTS;
// var ProductModel = oracle.createModel('product', productSchema.properties, productSchema.settings);
// });
//
// */
// done(new Error('test not implemented'));
// });
// });
//
// describe('dataSource.discoverSync(options)', function() {
// it("Synchronously discover an object containing properties and settings for an existing data source tables or collections.", function(done) {
// /* example -
// var tables = oracle.discover({owner: 'MYORG'});
// var productSchema = tables.PRODUCTS;
// var ProductModel = oracle.createModel('product', productSchema.properties, productSchema.settings);
//
// */
// done(new Error('test not implemented'));
// });
// });
// describe('dataSource.discoverModels(options, fn) ', function() {
// it("Discover a set of models based on tables or collections in a data source.", function(done) {
// /* example -
// oracle.discoverModels({owner: 'MYORG'}, function(err, models) {
// var ProductModel = models.Product;
// });
//
// */
// done(new Error('test not implemented'));
// });
// });
//
// describe('dataSource.discoverModelsSync(options)', function() {
// it("Synchronously Discover a set of models based on tables or collections in a data source.", function(done) {
// /* example -
// var models = oracle.discoverModels({owner: 'MYORG'});
// var ProductModel = models.Product;
// */
// done(new Error('test not implemented'));
// });
// });
describe('dataSource.operations()', function() {
it("List the enabled and disabled operations.", function() {
// assert the defaults
// - true: the method should be remote enabled
// - false: the method should not be remote enabled
// -
existsAndShared('_forDB', false);
existsAndShared('create', true);
existsAndShared('updateOrCreate', false);
existsAndShared('upsert', false);
existsAndShared('findOrCreate', false);
existsAndShared('exists', true);
existsAndShared('find', true);
existsAndShared('all', true);
existsAndShared('findOne', true);
existsAndShared('destroyAll', false);
existsAndShared('count', true);
existsAndShared('include', false);
existsAndShared('relationNameFor', false);
existsAndShared('hasMany', false);
existsAndShared('belongsTo', false);
existsAndShared('hasAndBelongsToMany', false);
existsAndShared('save', true);
existsAndShared('isNewRecord', false);
existsAndShared('_adapter', false);
existsAndShared('destroy', true);
existsAndShared('updateAttribute', true);
existsAndShared('updateAttributes', true);
existsAndShared('reload', true);
function existsAndShared(name, isRemoteEnabled) {
var op = memory.getOperation(name);
assert(op.remoteEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled');
}
});
});
});

41
test/geo-point.test.js Normal file
View File

@ -0,0 +1,41 @@
// describe('GeoPoint', function() {
//
// describe('geoPoint.distanceTo(geoPoint, options)', function() {
// it("Get the distance to another `GeoPoint`.", function(done) {
// /* example -
// var here = new GeoPoint({lat: 10, long: 10});
// var there = new GeoPoint({lat: 5, long: 5});
// console.log(here.distanceTo(there, {type: 'miles'})); // 438
// */
// done(new Error('test not implemented'));
// });
// });
//
// describe('GeoPoint.distanceBetween(a, b, options)', function() {
// it("Get the distance between two points.", function(done) {
// /* example -
// GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
// */
// done(new Error('test not implemented'));
// });
// });
//
// describe('geoPoint.lat', function() {
// it("The latitude point in degrees", function(done) {
// done(new Error('test not implemented'));
// });
// });
//
// describe('geoPoint.long', function() {
// it("The longitude point in degrees", function(done) {
// /* example -
// app.use(asteroid.rest());
//
//
// app.use(asteroid.sio);
//
// */
// done(new Error('test not implemented'));
// });
// });
// });

501
test/model.test.js Normal file
View File

@ -0,0 +1,501 @@
describe('Model', function() {
var User, memory;
beforeEach(function () {
memory = asteroid.createDataSource({connector: asteroid.Memory});
User = memory.createModel('user', {
'first': String,
'last': String,
'age': Number,
'password': String,
'gender': String,
'domain': String,
'email': String
});
})
describe('Model.validatesPresenceOf(properties...)', function() {
it("Require a model to include a property to be considered valid.", function() {
User.validatesPresenceOf('first', 'last', 'age');
var joe = new User({first: 'joe'});
assert(joe.isValid() === false, 'model should not validate');
assert(joe.errors.last, 'should have a missing last error');
assert(joe.errors.age, 'should have a missing age error');
});
});
describe('Model.validatesLengthOf(property, options)', function() {
it("Require a property length to be within a specified range.", function() {
User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}});
var joe = new User({password: '1234'});
assert(joe.isValid() === false, 'model should not be valid');
assert(joe.errors.password, 'should have password error');
});
});
describe('Model.validatesInclusionOf(property, options)', function() {
it("Require a value for `property` to be in the specified array.", function() {
User.validatesInclusionOf('gender', {in: ['male', 'female']});
var foo = new User({gender: 'bar'});
assert(foo.isValid() === false, 'model should not be valid');
assert(foo.errors.gender, 'should have gender error');
});
});
describe('Model.validatesExclusionOf(property, options)', function() {
it("Require a value for `property` to not exist in the specified array.", function() {
User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']});
var foo = new User({domain: 'www'});
var bar = new User({domain: 'billing'});
var bat = new User({domain: 'admin'});
assert(foo.isValid() === false);
assert(bar.isValid() === false);
assert(bat.isValid() === false);
assert(foo.errors.domain, 'model should have a domain error');
assert(bat.errors.domain, 'model should have a domain error');
assert(bat.errors.domain, 'model should have a domain error');
});
});
describe('Model.validatesNumericalityOf(property, options)', function() {
it("Require a value for `property` to be a specific type of `Number`.", function() {
User.validatesNumericalityOf('age', {int: true});
var joe = new User({age: 10.2});
assert(joe.isValid() === false);
var bob = new User({age: 0});
assert(bob.isValid() === true);
assert(joe.errors.age, 'model should have an age error');
});
});
describe('Model.validatesUniquenessOf(property, options)', function() {
it("Ensure the value for `property` is unique.", function(done) {
User.validatesUniquenessOf('email', {message: 'email is not unique'});
var joe = new User({email: 'joe@joe.com'});
var joe2 = new User({email: 'joe@joe.com'});
joe.save(function () {
joe2.save(function (err) {
assert(err, 'should get a validation error');
assert(joe2.errors.email, 'model should have email error');
done();
});
});
});
});
describe('myModel.isValid()', function() {
it("Validate the model instance.", function() {
User.validatesNumericalityOf('age', {int: true});
var user = new User({first: 'joe', age: 'flarg'})
var valid = user.isValid();
assert(valid === false);
assert(user.errors.age, 'model should have age error');
});
});
describe('Model.attachTo(dataSource)', function() {
it("Attach a model to a [DataSource](#data-source)", function() {
var MyModel = asteroid.createModel('my-model', {name: String});
assert(MyModel.all === undefined, 'should not have data access methods');
MyModel.attachTo(memory);
assert(typeof MyModel.all === 'function', 'should have data access methods after attaching to a data source');
});
});
describe('Model.create([data], [callback])', function() {
it("Create an instance of Model with given data and save to the attached data source.", function(done) {
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
assert(user instanceof User);
done();
});
});
});
describe('model.save([options], [callback])', function() {
it("Save an instance of a Model to the attached data source.", function(done) {
var joe = new User({first: 'Joe', last: 'Bob'});
joe.save(function(err, user) {
assert(user.id);
assert(!err);
assert(!user.errors);
done();
});
});
});
describe('model.updateAttributes(data, [callback])', function() {
it("Save specified attributes to the attached data source.", function(done) {
User.create({first: 'joe', age: 100}, function (err, user) {
assert(!err);
assert.equal(user.first, 'joe');
user.updateAttributes({
first: 'updatedFirst',
last: 'updatedLast'
}, function (err, updatedUser) {
assert(!err);
assert.equal(updatedUser.first, 'updatedFirst');
assert.equal(updatedUser.last, 'updatedLast');
assert.equal(updatedUser.age, 100);
done();
});
});
});
});
describe('Model.upsert(data, callback)', function() {
it("Update when record with id=data.id found, insert otherwise", function(done) {
User.upsert({first: 'joe', id: 7}, function (err, user) {
assert(!err);
assert.equal(user.first, 'joe');
User.upsert({first: 'bob', id: 7}, function (err, updatedUser) {
assert(!err);
assert.equal(updatedUser.first, 'bob');
done();
});
});
});
});
describe('model.destroy([callback])', function() {
it("Remove a model from the attached data source.", function(done) {
User.create({first: 'joe', last: 'bob'}, function (err, user) {
User.find(user.id, function (err, foundUser) {
assert.equal(user.id, foundUser.id);
foundUser.destroy(function () {
User.find(user.id, function (err, notFound) {
assert(!err);
assert.equal(notFound, null);
done();
});
});
});
});
});
});
describe('Model.destroyAll(callback)', function() {
it("Delete all Model instances from data source", function(done) {
(new TaskEmitter())
.task(User, 'create', {first: 'jill'})
.task(User, 'create', {first: 'bob'})
.task(User, 'create', {first: 'jan'})
.task(User, 'create', {first: 'sam'})
.task(User, 'create', {first: 'suzy'})
.on('done', function () {
User.count(function (err, count) {
assert.equal(count, 5);
User.destroyAll(function () {
User.count(function (err, count) {
assert.equal(count, 0);
done();
});
});
});
});
});
});
describe('Model.find(id, callback)', function() {
it("Find instance by id.", function(done) {
User.create({first: 'michael', last: 'jordan', id: 23}, function () {
User.find(23, function (err, user) {
assert.equal(user.id, 23);
assert.equal(user.first, 'michael');
assert.equal(user.last, 'jordan');
done();
});
});
});
});
describe('Model.count([query], callback)', function() {
it("Query count of Model instances in data source", function(done) {
(new TaskEmitter())
.task(User, 'create', {first: 'jill', age: 100})
.task(User, 'create', {first: 'bob', age: 200})
.task(User, 'create', {first: 'jan'})
.task(User, 'create', {first: 'sam'})
.task(User, 'create', {first: 'suzy'})
.on('done', function () {
User.count({age: {gt: 99}}, function (err, count) {
assert.equal(count, 2);
done();
});
});
});
});
describe('Remote Methods', function(){
beforeEach(function () {
User.login = function (username, password, fn) {
if(username === 'foo' && password === 'bar') {
fn(null, 123);
} else {
throw new Error('bad username and password!');
}
}
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', verb: 'get'}
}
);
app.use(asteroid.rest());
app.model(User);
});
describe('example remote method', function () {
it('should allow calling remotely', function(done) {
request(app)
.get('/users/sign-in?username=foo&password=bar')
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res){
if(err) return done(err);
assert(res.body.$data === 123);
done();
});
});
});
describe('Model.beforeRemote(name, fn)', function(){
it('Run a function before a remote method is called by a client.', function(done) {
var hookCalled = false;
User.beforeRemote('*.save', function(ctx, user, next) {
hookCalled = true;
next();
});
// invoke save
request(app)
.post('/users')
.send({data: {first: 'foo', last: 'bar'}})
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if(err) return done(err);
assert(hookCalled, 'hook wasnt called');
done();
});
});
});
describe('Model.afterRemote(name, fn)', function(){
it('Run a function after a remote method is called by a client.', function(done) {
var beforeCalled = false;
var afterCalled = false;
User.beforeRemote('*.save', function(ctx, user, next) {
assert(!afterCalled);
beforeCalled = true;
next();
});
User.afterRemote('*.save', function(ctx, user, next) {
assert(beforeCalled);
afterCalled = true;
next();
});
// invoke save
request(app)
.post('/users')
.send({data: {first: 'foo', last: 'bar'}})
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if(err) return done(err);
assert(beforeCalled, 'before hook was not called');
assert(afterCalled, 'after hook was not called');
done();
});
});
});
describe('Remote Method invoking context', function () {
// describe('ctx.user', function() {
// it("The remote user model calling the method remotely", function(done) {
// done(new Error('test not implemented'));
// });
// });
describe('ctx.req', function() {
it("The express ServerRequest object", function(done) {
var hookCalled = false;
User.beforeRemote('*.save', function(ctx, user, next) {
hookCalled = true;
assert(ctx.req);
assert(ctx.req.url);
assert(ctx.req.method);
assert(ctx.res);
assert(ctx.res.write);
assert(ctx.res.end);
next();
});
// invoke save
request(app)
.post('/users')
.send({data: {first: 'foo', last: 'bar'}})
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if(err) return done(err);
assert(hookCalled);
done();
});
});
});
describe('ctx.res', function() {
it("The express ServerResponse object", function(done) {
var hookCalled = false;
User.beforeRemote('*.save', function(ctx, user, next) {
hookCalled = true;
assert(ctx.req);
assert(ctx.req.url);
assert(ctx.req.method);
assert(ctx.res);
assert(ctx.res.write);
assert(ctx.res.end);
next();
});
// invoke save
request(app)
.post('/users')
.send({data: {first: 'foo', last: 'bar'}})
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if(err) return done(err);
assert(hookCalled);
done();
});
});
});
})
});
describe('Model.hasMany(Model)', function() {
it("Define a one to many relationship.", function(done) {
var Book = memory.createModel('book', {title: String, author: String});
var Chapter = memory.createModel('chapter', {title: String});
// by referencing model
Book.hasMany(Chapter);
Book.create({title: 'Into the Wild', author: 'Jon Krakauer'}, function(err, book) {
// using 'chapters' scope for build:
var c = book.chapters.build({title: 'Chapter 1'});
book.chapters.create({title: 'Chapter 2'}, function () {
c.save(function () {
Chapter.count({bookId: book.id}, function (err, count) {
assert.equal(count, 2);
book.chapters({where: {title: 'Chapter 1'}}, function(err, chapters) {
assert.equal(chapters.length, 1);
assert.equal(chapters[0].title, 'Chapter 1');
done();
});
});
});
});
});
});
});
// describe('Model.hasAndBelongsToMany()', function() {
// it("TODO: implement / document", function(done) {
// /* example -
//
// */
// done(new Error('test not implemented'));
// });
// });
// describe('Model.remoteMethods()', function() {
// it("Return a list of enabled remote methods.", function() {
// app.model(User);
// User.remoteMethods(); // ['save', ...]
// });
// });
// describe('Model.availableMethods()', function() {
// it("Returns the currently available api of a model as well as descriptions of any modified behavior or methods from attached data sources.", function(done) {
// /* example -
// User.attachTo(oracle);
// console.log(User.availableMethods());
//
// {
// 'User.all': {
// accepts: [{arg: 'filter', type: 'object', description: '...'}],
// returns: [{arg: 'users', type: ['User']}]
// },
// 'User.find': {
// accepts: [{arg: 'id', type: 'any'}],
// returns: [{arg: 'items', type: 'User'}]
// },
// ...
// }
// var oracle = asteroid.createDataSource({
// connector: 'oracle',
// host: '111.22.333.44',
// database: 'MYDB',
// username: 'username',
// password: 'password'
// });
//
// */
// done(new Error('test not implemented'));
// });
// });
// describe('Model.before(name, fn)', function(){
// it('Run a function before a method is called.', function() {
// // User.before('save', function(user, next) {
// // console.log('about to save', user);
// //
// // next();
// // });
// //
// // User.before('delete', function(user, next) {
// // // prevent all delete calls
// // next(new Error('deleting is disabled'));
// // });
// // User.beforeRemote('save', function(ctx, user, next) {
// // if(ctx.user.id === user.id) {
// // next();
// // } else {
// // next(new Error('must be logged in to update'))
// // }
// // });
//
// throw new Error('not implemented');
// });
// });
//
// describe('Model.after(name, fn)', function(){
// it('Run a function after a method is called.', function() {
//
// throw new Error('not implemented');
// });
// });
});

View File

@ -3,3 +3,30 @@
*/
assert = require('assert');
asteroid = require('../');
memoryConnector = asteroid.Memory;
app = null;
TaskEmitter = require('sl-task-emitter');
request = require('supertest');
beforeEach(function () {
app = asteroid();
});
assertValidDataSource = function (dataSource) {
// has methods
assert.isFunc(dataSource, 'createModel');
// assert.isFunc(dataSource, 'discover');
// assert.isFunc(dataSource, 'discoverSync');
assert.isFunc(dataSource, 'discoverAndBuildModels');
assert.isFunc(dataSource, 'discoverAndBuildModelsSync');
assert.isFunc(dataSource, 'enable');
assert.isFunc(dataSource, 'disable');
assert.isFunc(dataSource, 'defineOperation');
assert.isFunc(dataSource, 'operations');
}
assert.isFunc = function (obj, name) {
assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist');
assert(typeof obj[name] === 'function', name + ' is not a function');
}