diff --git a/README.md b/README.md index 89971c0d..5d9e226d 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ Create an asteroid application. Expose a `Model` to remote clients. - var memory = asteroid.createDataSource({connector: 'memory'}); - var Color = memory.defineModel({name: String}); + var memory = asteroid.createDataSource({connector: asteroid.Memory}); + var Color = memory.createModel('color', {name: String}); app.model(Color); app.use(asteroid.rest()); @@ -57,23 +57,29 @@ Get the app's exposed models. var models = app.models(); models.forEach(function (Model) { - console.log(Model.name); // color + console.log(Model.modelName); // color }); ### Model -An Asteroid `Model` is a vanilla JavaScript class constructor with an attached set of properties and settings. A `Model` instance is created by passing a data object containing properties to the `Model` constructor. +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 = asteroid.createModel({name: 'string'}); + // valid color + var Color = asteroid.createModel('color', {name: String}); var red = new Color({name: 'red'}); + console.log(red.name); // red + + // invalid color + var foo = new Color({bar: 'bat baz'}); + console.log(foo.bar); // undefined **Properties** A model defines a list of property names, types and other validation metadata. A [DataSource](#data-source) uses this definition to validate a `Model` during operations such as `save()`. -**Settings** +**Options** -Some [DataSources](#data-source) may support additional `Model` settings. +Some [DataSources](#data-source) may support additional `Model` options. Define an asteroid model. @@ -254,10 +260,9 @@ Define a static model method. }); } -Expose the static model method to clients as a [remote method](#remote-method). +Setup the static model method to be exposed to clients as a [remote method](#remote-method). asteroid.remoteMethod( - User, User.login, { accepts: [ @@ -279,13 +284,13 @@ Define an instance method. Define a remote model instance method. - asteroid.remoteMethod(User, User.prototype.logout); + 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(Model, fn, [options]); +##### asteroid.remoteMethod(fn, [options]); Expose a remote method. @@ -294,7 +299,6 @@ Expose a remote method. } asteroid.remoteMethod( - Product, Product.stats, { returns: {arg: 'stats', type: 'array'}, @@ -341,12 +345,23 @@ Run a function before or after a model method is called. next(); }); + User.after('save', function(user, next) { + console.log('after save complete', user); + + next(); + }); + Prevent the method from being called by passing an error to `next()`. User.before('delete', function(user, next) { // prevent all delete calls next(new Error('deleting is disabled')); }); + + User.after('delete', function(user, next) { + console.log('deleted', user); + next(); + }); #### Remote Hooks @@ -360,6 +375,11 @@ Run a function before or after a remote method is called by a client. } }); + User.afterRemote('save', function(ctx, user, next) { + console.log('user has been saved', user); + next(); + }); + #### Context Remote hooks are provided with a Context `ctx` that contains raw access to the transport specific objects. The `ctx` object also has a set of consistent apis that are consistent across transports. @@ -467,56 +487,70 @@ Define a data source for persisting models. password: 'password' }); -#### dataSource.createModel(name, properties, settings) +#### dataSource.createModel(name, properties, options) Define a model and attach it to a `DataSource`. var Color = oracle.createModel('color', {name: String}); -#### dataSource.discover(options, fn) - -Discover an object containing properties and settings for an existing data source. - - oracle.discover({owner: 'MYORG'}, function(err, tables) { - var productSchema = tables.PRODUCTS; - var ProductModel = oracle.createModel('product', productSchema.properties, productSchema.settings); - }); - -#### dataSource.discoverSync(options) - -Synchronously discover an object containing properties and settings for an existing data source tables or collections. - - var tables = oracle.discover({owner: 'MYORG'}); - var productSchema = tables.PRODUCTS; - var ProductModel = oracle.createModel('product', productSchema.properties, productSchema.settings); - -#### dataSource.discoverModels(options, fn) +#### dataSource.discoverAndBuildModels(owner, tableOrView, options, fn) Discover a set of models based on tables or collections in a data source. - oracle.discoverModels({owner: 'MYORG'}, function(err, models) { + oracle.discoverAndBuildModels('MYORG', function(err, models) { var ProductModel = models.Product; }); -**Note:** The `models` will contain all properties and settings discovered from the data source. It will also automatically discover and create relationships. +**Note:** The `models` will contain all properties and options discovered from the data source. It will also automatically discover and create relationships. -#### dataSource.discoverModelsSync(options) +#### dataSource.discoverAndBuildModelsSync(owner, tableOrView, options) Synchronously Discover a set of models based on tables or collections in a data source. - var models = oracle.discoverModels({owner: 'MYORG'}); + var models = oracle.discoverAndBuildModelsSync('MYORG'); var ProductModel = models.Product; +#### dataSource.defineOperation(name, options, fn) + +Define and enable 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.enable(operation) Enable 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 rest = asteroid.createDataSource({ + var twitter = asteroid.createDataSource({ connector: require('asteroid-rest'), - url: 'http://maps.googleapis.com/maps/api' - enableAll: true + url: 'http://api.twitter.com' }); // enable an operation @@ -564,11 +598,18 @@ Output: { find: { - allowRemote: true, + remoteEnabled: true, accepts: [...], returns: [...] enabled: true }, + save: { + remoteEnabled: true, + prototype: true, + accepts: [...], + returns: [...], + enabled: true + }, ... } @@ -577,7 +618,7 @@ Output: Create a data source with a specific connector. See **available connectors** for specific connector documentation. var memory = asteroid.createDataSource({ - connector: require('asteroid-memory') + connector: asteroid.Memory }); **Available Connectors** diff --git a/TODO.md b/TODO.md deleted file mode 100644 index aed8c811..00000000 --- a/TODO.md +++ /dev/null @@ -1,56 +0,0 @@ - - app.model(Model) - - app.models() - - - - Model.validatesPresenceOf(properties...) - - Model.validatesLengthOf(property, options) - - Model.validatesInclusionOf(property, options) - - Model.validatesExclusionOf(property, options) - - Model.validatesNumericalityOf(property, options) - - Model.validatesUniquenessOf(property, options) - - myModel.isValid() - - Model.attachTo(dataSource) - -##### Model.create([data], [callback]) -##### model.save([options], [callback]) -##### model.updateAttributes(data, [callback]) -##### model.upsert(data, callback) -##### model.destroy([callback]) -##### Model.destroyAll(callback) -##### Model.find(id, callback) -##### Model.count([query], callback) -#### Static Methods -#### Instance Methods -#### Remote Methods -##### asteroid.remoteMethod(Model, fn, [options]); -#### Hooks -#### Remote Hooks -#### Context -##### ctx.me -##### Rest -###### ctx.req -###### ctx.res -#### Relationships -##### Model.hasMany(Model) -##### Model.hasAndBelongsToMany() -#### Model.availableHooks() -#### Shared Methods -#### Model.availableMethods() -### Data Source -#### dataSource.createModel(name, options, settings) -#### dataSource.discover(options, fn) -#### dataSource.discoverSync(options) -#### dataSource.discoverModels(options, fn) -#### dataSource.discoverModelsSync(options) -#### dataSource.enable(operation) -#### dataSource.disable(operation) -#### dataSource.operations() -#### Connectors -### GeoPoint -#### geoPoint.distanceTo(geoPoint, options) -#### GeoPoint.distanceBetween(a, b, options) -#### Distance Types -#### geoPoint.lat -#### geoPoint.long -### Asteroid Types -### REST Router -### SocketIO Middleware **Not Available** \ No newline at end of file diff --git a/index.js b/index.js index 016e0d07..f7cd0b3b 100644 --- a/index.js +++ b/index.js @@ -5,13 +5,8 @@ var asteroid = module.exports = require('./lib/asteroid'); /** - * Connector + * Connectors */ -asteroid.Connector = require('./lib/connector'); - -/** - * JugglingDB Connector - */ - -asteroid.JdbConnector = require('./lib/jdb-connector'); \ No newline at end of file +asteroid.Connector = require('./lib/connectors/base-connector'); +asteroid.Memory = require('./lib/connectors/memory'); \ No newline at end of file diff --git a/lib/asteroid.js b/lib/asteroid.js index edba9bb5..6ab45baf 100644 --- a/lib/asteroid.js +++ b/lib/asteroid.js @@ -72,18 +72,41 @@ asteroid.errorHandler.title = 'Asteroid'; /** * 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 (options) { - var connector = options.connector; - var jdbAdapter = connector.jdbAdapter; - - if(jdbAdapter) { - // TODO remove jdb dependency - delete options.connector; - return new DataSource(jdbAdapter, options); - } else { - // TODO implement asteroid data source - throw Error('unsupported adapter') - } -} \ No newline at end of file +asteroid.createDataSource = function (name, options) { + return new DataSource(name, options); +} + +/** + * 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) { + var mb = new ModelBuilder(); + return mb.define(name, properties, arguments); +} + +/** + * Add a remote method to a model. + * @param {Function} fn + * @param {Object} options (optional) + */ + +asteroid.remoteMethod = function (fn, options) { + fn.shared = true; + Object.keys(options).forEach(function (key) { + fn[key] = options[key]; + }); +} + diff --git a/lib/connector.js b/lib/connectors/base-connector.js similarity index 100% rename from lib/connector.js rename to lib/connectors/base-connector.js 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/test/app.test.js b/test/app.test.js index f5bea4b7..72387eb9 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,37 +1,21 @@ describe('app', function() { describe('app.model(Model)', function() { - it("Expose a `Model` to remote clients.", function(done) { - /* example - - var memory = asteroid.createDataSource({connector: 'memory'}); - var Color = memory.defineModel({name: String}); + 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); - app.use(asteroid.rest()); - - - */ - done(new Error('test not implemented')); + assert.equal(app.models().length, 1); }); }); describe('app.models()', function() { - it("Get the app's exposed models.", function(done) { - /* example - + it("Get the app's exposed models.", function() { + var Color = asteroid.createModel('color', {name: String}); var models = app.models(); - models.forEach(function (Model) { - console.log(Model.name); // color - }); - - var Color = asteroid.createModel({name: 'string'}); - var red = new Color({name: 'red'}); - var User = asteroid.createModel('user', { - first: String, - last: String, - age: Number - }); - */ - done(new Error('test not implemented')); + 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 404be6b0..232ad8b4 100644 --- a/test/asteroid.test.js +++ b/test/asteroid.test.js @@ -1,51 +1,34 @@ describe('asteroid', function() { describe('asteroid.createDataSource(options)', function(){ - it('Create a data sources with a connector.', function(done) { - done(new Error('not implemented')); + 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("Expose a remote method.", function(done) { - /* example - + it("Setup a remote method.", function() { + var Product = asteroid.createModel('product', {price: Number}); + Product.stats = function(fn) { - myApi.getStats('products', fn); + // ... } asteroid.remoteMethod( - Product, Product.stats, { returns: {arg: 'stats', type: 'array'}, http: {path: '/info', verb: 'get'} } ); - // examples - {arg: 'myArg', type: 'number'} - [ - {arg: 'arg1', type: 'number', required: true}, - {arg: 'arg2', type: 'array'} - ] - 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')) - } - }); - - */ - done(new Error('test not implemented')); + 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 index 1f4ec10c..43f368ff 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -1,11 +1,30 @@ describe('DataSource', function() { describe('dataSource.createModel(name, properties, settings)', function() { - it("Define a model and attach it to a `DataSource`.", function(done) { - /* example - - var Color = oracle.createModel('color', {name: String}); - */ - done(new Error('test not implemented')); + it("Define a model and attach it to a `DataSource`.", function() { + var memory = asteroid.createDataSource({connector: asteroid.Memory}); + 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'); }); }); @@ -114,7 +133,7 @@ describe('DataSource', function() { ... } var memory = asteroid.createDataSource({ - connector: require('asteroid-memory') + connector: asteroid.Memory }); { diff --git a/test/model.test.js b/test/model.test.js index 5a0434fe..12bc0d11 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -315,4 +315,49 @@ describe('Model', function() { 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'); + }); + }); + + describe('Model.beforeRemote(name, fn)', function(){ + it('Run a function before a remote method is called by a client.', function() { + + throw new Error('not implemented'); + }); + }); + + describe('Model.afterRemote(name, fn)', function(){ + it('Run a function after a remote method is called by a client.', function() { + + throw new Error('not implemented'); + }); + }); }); \ No newline at end of file diff --git a/test/support.js b/test/support.js index 5559fe10..bc302075 100644 --- a/test/support.js +++ b/test/support.js @@ -4,10 +4,27 @@ assert = require('assert'); asteroid = require('../'); -memoryConnector = require('asteroid-memory'); +memoryConnector = asteroid.Memory; beforeEach(function () { app = asteroid(); - EmptyModel = asteroid.createModel(); memoryDataSource = asteroid.createDataSource({connector: memoryConnector}); -}); \ No newline at end of file +}); + +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