diff --git a/.gitignore b/.gitignore index 3df69e46..e2f7cef8 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 1870bb33..280811ca 100644 --- a/README.md +++ b/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(); - -### app.dataSource() +_TODO_ -Attach a remote data source to your app. +### App - app.dataSource('color-db', { - adapter: 'oracle', - host: 'localhost', - port: 2345, - user: 'test', - password: 'test' +Create an asteroid application. + + var asteroid = require('asteroid'); + var app = asteroid(); + + app.get('/', function(req, res){ + res.send('hello world'); + }); + + app.listen(3000); + +**Notes:** + + - extends [express](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.model(Model) + +Expose a `Model` to remote clients. + + var memory = asteroid.createDataSource({connector: asteroid.Memory}); + var Color = memory.createModel('color', {name: String}); + + app.model(Color); + app.use(asteroid.rest()); + +**Note:** this will expose all [shared methods](#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 }); -### app.define(name) +### Model -Define a [Model](node_modules/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. - var Color = app.define('color'); - -### app.use(asteroid.rest); - -Expose your models over a REST api. - - // node - app.use(asteroid.rest); + // valid color + var Color = asteroid.createModel('color', {name: String}); + var red = new Color({name: 'red'}); + console.log(red.name); // red - // http - GET /colors + // 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); - 200 OK +**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'} + [ - {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); + diff --git a/gen-tests.js b/gen-tests.js new file mode 100644 index 00000000..51d28b2e --- /dev/null +++ b/gen-tests.js @@ -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'); +}); \ No newline at end of file diff --git a/index.js b/index.js index b465aeec..f7cd0b3b 100644 --- a/index.js +++ b/index.js @@ -2,4 +2,11 @@ * asteroid ~ public api */ -module.exports = require('./lib/asteroid'); \ No newline at end of file +var asteroid = module.exports = require('./lib/asteroid'); + +/** + * Connectors + */ + +asteroid.Connector = require('./lib/connectors/base-connector'); +asteroid.Memory = require('./lib/connectors/memory'); \ No newline at end of file diff --git a/lib/application.js b/lib/application.js index 2cc69e0e..8b2b8131 100644 --- a/lib/application.js +++ b/lib/application.js @@ -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. @@ -18,7 +16,7 @@ var app = exports = module.exports = {}; /** * Create a set of remote objects. */ - + app.remotes = function () { if(this._remotes) { return this._remotes; @@ -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); - - ModelCtor.dataSource = function (name) { - var dataSource = app.dataSources[name]; - - 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]; - } - } - } - } - - 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); +app._models = []; + +/** + * Expose a model. + * + * @param Model {Model} + */ + +app.model = function (Model) { + this._models.push(Model); + Model.app = this; + if(Model._remoteHooks) { + Model._remoteHooks.emit('attached', app); + } } /** - * 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,43 +84,20 @@ app.remoteObjects = function () { var models = this.models(); // add in models - Object.keys(models) - .forEach(function (name) { - var ModelCtor = models[name]; - - // only add shared models - if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') { - result[name] = ModelCtor; - } - }); + models.forEach(function (ModelCtor) { + // only add shared models + if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') { + result[ModelCtor.pluralModelName] = ModelCtor; + } + }); return result; } -/** - * App data sources and models. - */ - -app._models = {}; -app.dataSources = {}; - /** * Get the apps set of remote objects. */ 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)); } \ No newline at end of file diff --git a/lib/asteroid.js b/lib/asteroid.js index c5b90746..e0d19243 100644 --- a/lib/asteroid.js +++ b/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'}; +} diff --git a/lib/connectors/base-connector.js b/lib/connectors/base-connector.js new file mode 100644 index 00000000..cf224629 --- /dev/null +++ b/lib/connectors/base-connector.js @@ -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) { + +} \ No newline at end of file diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js new file mode 100644 index 00000000..ce027f27 --- /dev/null +++ b/lib/connectors/memory.js @@ -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; \ No newline at end of file diff --git a/lib/middleware/configure.js b/lib/middleware/configure.js deleted file mode 100644 index 1c2c7d1c..00000000 --- a/lib/middleware/configure.js +++ /dev/null @@ -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; - }, {}); - diff --git a/node_modules/.bin/_mocha b/node_modules/.bin/_mocha deleted file mode 120000 index f2a54ffd..00000000 --- a/node_modules/.bin/_mocha +++ /dev/null @@ -1 +0,0 @@ -../mocha/bin/_mocha \ No newline at end of file diff --git a/node_modules/.bin/express b/node_modules/.bin/express deleted file mode 120000 index b741d99c..00000000 --- a/node_modules/.bin/express +++ /dev/null @@ -1 +0,0 @@ -../express/bin/express \ No newline at end of file diff --git a/node_modules/.bin/mocha b/node_modules/.bin/mocha deleted file mode 120000 index 43c668d9..00000000 --- a/node_modules/.bin/mocha +++ /dev/null @@ -1 +0,0 @@ -../mocha/bin/mocha \ No newline at end of file diff --git a/node_modules/asteroid-module/.gitignore b/node_modules/asteroid-module/.gitignore deleted file mode 100644 index 6af74074..00000000 --- a/node_modules/asteroid-module/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.swp -*.swo -node_modules/ diff --git a/node_modules/asteroid-module/README.md b/node_modules/asteroid-module/README.md deleted file mode 100644 index b71768ce..00000000 --- a/node_modules/asteroid-module/README.md +++ /dev/null @@ -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 - } \ No newline at end of file diff --git a/node_modules/asteroid-module/index.js b/node_modules/asteroid-module/index.js deleted file mode 100644 index abb2ce9d..00000000 --- a/node_modules/asteroid-module/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * asteroid-module ~ public api - */ - -module.exports = require('./lib/asteroid-module'); \ No newline at end of file diff --git a/node_modules/asteroid-module/lib/asteroid-module.js b/node_modules/asteroid-module/lib/asteroid-module.js deleted file mode 100644 index ab06d017..00000000 --- a/node_modules/asteroid-module/lib/asteroid-module.js +++ /dev/null @@ -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); diff --git a/node_modules/asteroid-module/package.json b/node_modules/asteroid-module/package.json deleted file mode 100644 index 7a779309..00000000 --- a/node_modules/asteroid-module/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "asteroid-module", - "description": "asteroid-module", - "version": "0.0.1", - "scripts": { - "test": "mocha" - }, - "dependencies": { - "debug": "latest" - }, - "devDependencies": { - "mocha": "latest" - } -} \ No newline at end of file diff --git a/node_modules/asteroid-module/test/asteroid-module.test.js b/node_modules/asteroid-module/test/asteroid-module.test.js deleted file mode 100644 index 9e9973a3..00000000 --- a/node_modules/asteroid-module/test/asteroid-module.test.js +++ /dev/null @@ -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 ', function() { - asteroidModule.myMethod(); - }); - - // example async test - it('should ', function(done) { - setTimeout(function () { - asteroidModule.myMethod(); - done(); - }, 0); - }); - }); -}); \ No newline at end of file diff --git a/node_modules/asteroid-module/test/support.js b/node_modules/asteroid-module/test/support.js deleted file mode 100644 index a9572cf5..00000000 --- a/node_modules/asteroid-module/test/support.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * asteroid-module test setup and support. - */ - -assert = require('assert'); \ No newline at end of file diff --git a/node_modules/data-source/.gitignore b/node_modules/data-source/.gitignore deleted file mode 100644 index 6af74074..00000000 --- a/node_modules/data-source/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.swp -*.swo -node_modules/ diff --git a/node_modules/data-source/README.md b/node_modules/data-source/README.md deleted file mode 100644 index e0f7301e..00000000 --- a/node_modules/data-source/README.md +++ /dev/null @@ -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). \ No newline at end of file diff --git a/node_modules/data-source/index.js b/node_modules/data-source/index.js deleted file mode 100644 index 3b2c35c6..00000000 --- a/node_modules/data-source/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * connection ~ public api - */ - -module.exports = require('./lib/data-source'); \ No newline at end of file diff --git a/node_modules/data-source/lib/data-source.js b/node_modules/data-source/lib/data-source.js deleted file mode 100644 index 59047f4a..00000000 --- a/node_modules/data-source/lib/data-source.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/node_modules/data-source/lib/memory.js b/node_modules/data-source/lib/memory.js deleted file mode 100644 index 1061d932..00000000 --- a/node_modules/data-source/lib/memory.js +++ /dev/null @@ -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 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; -} diff --git a/node_modules/data-source/package.json b/node_modules/data-source/package.json deleted file mode 100644 index c329007b..00000000 --- a/node_modules/data-source/package.json +++ /dev/null @@ -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" - } -} diff --git a/node_modules/data-source/test/data-source.test.js b/node_modules/data-source/test/data-source.test.js deleted file mode 100644 index 681c974b..00000000 --- a/node_modules/data-source/test/data-source.test.js +++ /dev/null @@ -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 ', function() { - dataSource.myMethod(); - }); - - // example async test - it('should ', function(done) { - setTimeout(function () { - dataSource.myMethod(); - done(); - }, 0); - }); - }); -}); \ No newline at end of file diff --git a/node_modules/data-source/test/support.js b/node_modules/data-source/test/support.js deleted file mode 100644 index 4d8c7d8c..00000000 --- a/node_modules/data-source/test/support.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * connection test setup and support. - */ - -assert = require('assert'); \ No newline at end of file diff --git a/node_modules/model/.gitignore b/node_modules/model/.gitignore deleted file mode 100644 index 6af74074..00000000 --- a/node_modules/model/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.swp -*.swo -node_modules/ diff --git a/node_modules/model/README.md b/node_modules/model/README.md deleted file mode 100644 index cd312953..00000000 --- a/node_modules/model/README.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/node_modules/model/example/example.js b/node_modules/model/example/example.js deleted file mode 100644 index a3fbffea..00000000 --- a/node_modules/model/example/example.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/node_modules/model/index.js b/node_modules/model/index.js deleted file mode 100644 index 9d72d647..00000000 --- a/node_modules/model/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * model ~ public api - */ - -module.exports = require('./lib/model-configuration'); \ No newline at end of file diff --git a/node_modules/model/lib/model-configuration.js b/node_modules/model/lib/model-configuration.js deleted file mode 100644 index a54efd45..00000000 --- a/node_modules/model/lib/model-configuration.js +++ /dev/null @@ -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'} -} diff --git a/node_modules/model/lib/model.js b/node_modules/model/lib/model.js deleted file mode 100644 index cf687a96..00000000 --- a/node_modules/model/lib/model.js +++ /dev/null @@ -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'); -} \ No newline at end of file diff --git a/node_modules/model/package.json b/node_modules/model/package.json deleted file mode 100644 index d21c693a..00000000 --- a/node_modules/model/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "model", - "description": "model", - "version": "0.0.1", - "scripts": { - "test": "mocha" - }, - "dependencies": { - "debug": "latest" - }, - "devDependencies": { - "mocha": "latest" - } -} \ No newline at end of file diff --git a/node_modules/model/test/model.test.js b/node_modules/model/test/model.test.js deleted file mode 100644 index 928953e2..00000000 --- a/node_modules/model/test/model.test.js +++ /dev/null @@ -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 ', function() { - model.myMethod(); - }); - - // example async test - it('should ', function(done) { - setTimeout(function () { - model.myMethod(); - done(); - }, 0); - }); - }); -}); \ No newline at end of file diff --git a/node_modules/model/test/support.js b/node_modules/model/test/support.js deleted file mode 100644 index e8db5346..00000000 --- a/node_modules/model/test/support.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * model test setup and support. - */ - -assert = require('assert'); \ No newline at end of file diff --git a/node_modules/oracle-data-source/.gitignore b/node_modules/oracle-data-source/.gitignore deleted file mode 100644 index 6af74074..00000000 --- a/node_modules/oracle-data-source/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.swp -*.swo -node_modules/ diff --git a/node_modules/oracle-data-source/README.md b/node_modules/oracle-data-source/README.md deleted file mode 100644 index 43ae30de..00000000 --- a/node_modules/oracle-data-source/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# oracle-data-source - -## About - -Configures the oracle adapter for use in a [data store](../../store). - -### Options - -#### hostname -#### port -#### username -#### password \ No newline at end of file diff --git a/node_modules/oracle-data-source/index.js b/node_modules/oracle-data-source/index.js deleted file mode 100644 index 3312ce08..00000000 --- a/node_modules/oracle-data-source/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * connection ~ public api - */ - -module.exports = require('./lib/oracle-data-source'); \ No newline at end of file diff --git a/node_modules/oracle-data-source/lib/oracle-data-source.js b/node_modules/oracle-data-source/lib/oracle-data-source.js deleted file mode 100644 index 7fa13b79..00000000 --- a/node_modules/oracle-data-source/lib/oracle-data-source.js +++ /dev/null @@ -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'); \ No newline at end of file diff --git a/node_modules/oracle-data-source/package.json b/node_modules/oracle-data-source/package.json deleted file mode 100644 index ce0e8550..00000000 --- a/node_modules/oracle-data-source/package.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/node_modules/route/.gitignore b/node_modules/route/.gitignore deleted file mode 100644 index 6af74074..00000000 --- a/node_modules/route/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.swp -*.swo -node_modules/ diff --git a/node_modules/route/README.md b/node_modules/route/README.md deleted file mode 100644 index 7fcb89a2..00000000 --- a/node_modules/route/README.md +++ /dev/null @@ -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')`. - } - } - diff --git a/node_modules/route/index.js b/node_modules/route/index.js deleted file mode 100644 index 3b31f452..00000000 --- a/node_modules/route/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * resource ~ public api - */ - -module.exports = require('./lib/route'); \ No newline at end of file diff --git a/node_modules/route/lib/http-context.js b/node_modules/route/lib/http-context.js deleted file mode 100644 index b1c72c7f..00000000 --- a/node_modules/route/lib/http-context.js +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/node_modules/route/lib/route.js b/node_modules/route/lib/route.js deleted file mode 100644 index b445b619..00000000 --- a/node_modules/route/lib/route.js +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/node_modules/route/package.json b/node_modules/route/package.json deleted file mode 100644 index db5da708..00000000 --- a/node_modules/route/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "resource", - "description": "resource", - "version": "0.0.1", - "scripts": { - "test": "mocha" - }, - "dependencies": { - "debug": "latest" - }, - "devDependencies": { - "mocha": "latest" - } -} \ No newline at end of file diff --git a/node_modules/route/test/http-context.test.js b/node_modules/route/test/http-context.test.js deleted file mode 100644 index 6cadac3e..00000000 --- a/node_modules/route/test/http-context.test.js +++ /dev/null @@ -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(); - }); - }); - }); -}); \ No newline at end of file diff --git a/node_modules/route/test/support.js b/node_modules/route/test/support.js deleted file mode 100644 index 94e12000..00000000 --- a/node_modules/route/test/support.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * resource test setup and support. - */ - -assert = require('assert'); \ No newline at end of file diff --git a/package.json b/package.json index 0f48605f..23b3999f 100644 --- a/package.json +++ b/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" } diff --git a/test/app.test.js b/test/app.test.js new file mode 100644 index 00000000..72387eb9 --- /dev/null +++ b/test/app.test.js @@ -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'); + }); + }); +}); \ No newline at end of file diff --git a/test/asteroid.test.js b/test/asteroid.test.js index 9979110d..232ad8b4 100644 --- a/test/asteroid.test.js +++ b/test/asteroid.test.js @@ -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); + }); + }); +}); \ No newline at end of file diff --git a/test/data-source.test.js b/test/data-source.test.js new file mode 100644 index 00000000..ae2684c5 --- /dev/null +++ b/test/data-source.test.js @@ -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'); + } + }); + }); +}); \ No newline at end of file diff --git a/test/geo-point.test.js b/test/geo-point.test.js new file mode 100644 index 00000000..fe8be350 --- /dev/null +++ b/test/geo-point.test.js @@ -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')); +// }); +// }); +// }); \ No newline at end of file diff --git a/test/model.test.js b/test/model.test.js new file mode 100644 index 00000000..60470bed --- /dev/null +++ b/test/model.test.js @@ -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'); +// }); +// }); +}); \ No newline at end of file diff --git a/test/support.js b/test/support.js index 2a007e24..1492c6f0 100644 --- a/test/support.js +++ b/test/support.js @@ -2,4 +2,31 @@ * asteroid test setup and support. */ -assert = require('assert'); \ No newline at end of file +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'); +} \ No newline at end of file