Merge branch 'refactor/api'
Conflicts: lib/application.js
This commit is contained in:
commit
654a89147c
|
@ -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
689
README.md
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
9
index.js
9
index.js
|
@ -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');
|
|
@ -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));
|
||||
}
|
175
lib/asteroid.js
175
lib/asteroid.js
|
@ -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'};
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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 +0,0 @@
|
|||
../mocha/bin/_mocha
|
|
@ -1 +0,0 @@
|
|||
../express/bin/express
|
|
@ -1 +0,0 @@
|
|||
../mocha/bin/mocha
|
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
|
@ -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
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* asteroid-module ~ public api
|
||||
*/
|
||||
|
||||
module.exports = require('./lib/asteroid-module');
|
|
@ -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);
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "asteroid-module",
|
||||
"description": "asteroid-module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"test": "mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "latest"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* asteroid-module test setup and support.
|
||||
*/
|
||||
|
||||
assert = require('assert');
|
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
|
@ -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).
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* connection ~ public api
|
||||
*/
|
||||
|
||||
module.exports = require('./lib/data-source');
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* connection test setup and support.
|
||||
*/
|
||||
|
||||
assert = require('assert');
|
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
|
@ -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.
|
|
@ -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();
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* model ~ public api
|
||||
*/
|
||||
|
||||
module.exports = require('./lib/model-configuration');
|
|
@ -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'}
|
||||
}
|
|
@ -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');
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "model",
|
||||
"description": "model",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"test": "mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "latest"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* model test setup and support.
|
||||
*/
|
||||
|
||||
assert = require('assert');
|
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
|
@ -1,12 +0,0 @@
|
|||
# oracle-data-source
|
||||
|
||||
## About
|
||||
|
||||
Configures the oracle adapter for use in a [data store](../../store).
|
||||
|
||||
### Options
|
||||
|
||||
#### hostname
|
||||
#### port
|
||||
#### username
|
||||
#### password
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* connection ~ public api
|
||||
*/
|
||||
|
||||
module.exports = require('./lib/oracle-data-source');
|
|
@ -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');
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
|
@ -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')`.
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* resource ~ public api
|
||||
*/
|
||||
|
||||
module.exports = require('./lib/route');
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "resource",
|
||||
"description": "resource",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"test": "mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "latest"
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* resource test setup and support.
|
||||
*/
|
||||
|
||||
assert = require('assert');
|
17
package.json
17
package.json
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'));
|
||||
// });
|
||||
// });
|
||||
// });
|
|
@ -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');
|
||||
// });
|
||||
// });
|
||||
});
|
|
@ -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');
|
||||
}
|
Loading…
Reference in New Issue