From 166451443ae20b50a430f2688269e2ed69f79fe4 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 12 Jun 2013 15:44:38 -0700 Subject: [PATCH] Updated docs, updated tests --- README.md | 117 ++------ lib/application.js | 16 +- lib/asteroid.js | 104 ++++++- package.json | 4 +- test/data-source.test.js | 50 +--- test/geo-point.test.js | 82 ++--- test/model.test.js | 628 ++++++++++++++++++++++++--------------- test/support.js | 2 + 8 files changed, 572 insertions(+), 431 deletions(-) diff --git a/README.md b/README.md index 5d9e226d..191a1eec 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Validate the model instance. 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: 'oracle', + connector: require('asteroid-oracle'), host: '111.22.333.44', database: 'MYDB', username: 'username', @@ -187,7 +187,7 @@ Save specified attributes to the attached data source. name: 'updatedLast' }, fn); -##### model.upsert(data, callback) +##### Model.upsert(data, callback) Update when record with id=data.id found, insert otherwise. **Note:** no setters, validations or hooks applied when using upsert. @@ -335,58 +335,47 @@ Each argument may define any of the [asteroid types](#asteroid-types). - 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. -#### Hooks - -Run a function before or after a model method is called. - - User.before('save', function(user, next) { - console.log('about to save', user); - - 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 Run a function before or after a remote method is called by a client. - User.beforeRemote('save', function(ctx, user, next) { - if(ctx.user.id === user.id) { + // *.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) { + 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.*', ...); + #### 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. +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.me +##### ctx.user -The id of the user calling the method remotely. **Note:** this is undefined if a user is not logged in. +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. ##### Rest @@ -442,36 +431,10 @@ Query and create the related models. TODO: implement / document -#### Model.availableHooks() - -Return a list of available hooks. - - console.log(User.availableHooks()); // ['save', ...] - #### Shared Methods Any static or instance method can be decorated as `shared`. These methods are exposed over the provided transport (eg. [asteroid.rest](#rest)). -#### Model.availableMethods() - -Returns the currently available api of a model as well as descriptions of any modified behavior or methods from attached data sources. - - User.attachTo(oracle); - console.log(User.availableMethods()); - -Output: - - { - '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'}] - }, - ... - } ### Data Source @@ -512,7 +475,7 @@ Synchronously Discover a set of models based on tables or collections in a data #### dataSource.defineOperation(name, options, fn) -Define and enable a new operation available to all model's attached to the data source. +Define a new operation available to all model's attached to the data source. var maps = asteroid.createDataSource({ connector: require('asteroid-rest'), @@ -542,31 +505,14 @@ Define and enable a new operation available to all model's attached to the data console.log(point.lat, point.long); // 24.224424 44.444445 }); -#### dataSource.enable(operation) +#### dataSource.enableRemote(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 twitter = asteroid.createDataSource({ - connector: require('asteroid-rest'), - url: 'http://api.twitter.com' - }); +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()`. - // enable an operation - twitter.enable('find'); - - // enable remote access - twitter.enableRemote('find') - -**Notes:** - - only enabled operations will be added to attached models - - data sources must enable / disable operations before attaching or creating models +#### dataSource.disableRemote(operation) -#### dataSource.disable(operation) - -Disable 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()`. +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 @@ -575,9 +521,6 @@ Disable a data source operation. Each [connector](#connector) has its own set of host: '...', ... }); - - // disable an operation completely - oracle.disable('destroyAll'); // or only disable it as a remote method oracle.disableRemote('destroyAll'); diff --git a/lib/application.js b/lib/application.js index b44e30f4..b36ca716 100644 --- a/lib/application.js +++ b/lib/application.js @@ -54,6 +54,7 @@ app._models = []; app.model = function (Model) { this._models.push(Model); + Model.app = this; } /** @@ -74,15 +75,12 @@ 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; } diff --git a/lib/asteroid.js b/lib/asteroid.js index 6ab45baf..acf011cf 100644 --- a/lib/asteroid.js +++ b/lib/asteroid.js @@ -81,7 +81,13 @@ asteroid.errorHandler.title = 'Asteroid'; */ asteroid.createDataSource = function (name, options) { - return new DataSource(name, options); + var ds = new DataSource(name, options); + ds.createModel = function (name, properties, settings) { + var Model = asteroid.createModel(name, properties, settings); + Model.attachTo(ds); + return Model; + } + return ds; } /** @@ -93,8 +99,102 @@ asteroid.createDataSource = function (name, options) { */ asteroid.createModel = function (name, properties, options) { + assert(typeof name === 'string', 'Cannot create a model without a name'); + var mb = new ModelBuilder(); - return mb.define(name, properties, arguments); + var ModelCtor = mb.define(name, properties, arguments); + 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 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 (data, id, fn) { + if(typeof data === 'function') { + fn = data; + data = null; + id = null; + } else if (typeof id === 'function') { + fn = id; + 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 remotes = this.app.remotes(); + remotes.before(ModelCtor.pluralModelName + '.' + name, function (ctx, next) { + fn(ctx, ctx.instance, next); + }); + } + + // after remote hook + ModelCtor.afterRemote = function (name, fn) { + var remotes = this.app.remotes(); + remotes.before(ModelCtor.pluralModelName + '.' + name, function (ctx, next) { + fn(ctx, ctx.instance, next); + }); + } + + return ModelCtor; } /** diff --git a/package.json b/package.json index 3948faf5..23b3999f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "inflection": "~1.2.5" }, "devDependencies": { - "mocha": "latest" + "mocha": "latest", + "sl-task-emitter": "0.0.x", + "supertest": "latest" }, "optionalDependencies": { "jugglingdb-oracle": "git+ssh://git@github.com:strongloop/jugglingdb-oracle.git" diff --git a/test/data-source.test.js b/test/data-source.test.js index dc46b8e4..ae2684c5 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -81,53 +81,12 @@ describe('DataSource', function() { // }); // }); - describe('dataSource.enable(operation)', function() { - it("Enable a data source operation", function() { - // enable an operation - memory.disable('find'); - - var find = memory.getOperation('find'); - - assert.equal(find.name, 'find'); - assert.equal(find.enabled, false); - assert.equal(find.remoteEnabled, false); - - memory.enable('find'); - - assert.equal(find.name, 'find'); - assert.equal(find.enabled, true); - assert.equal(find.remoteEnabled, false); - - memory.enableRemote('find'); - - assert.equal(find.remoteEnabled, true); - }); - }); - - describe('dataSource.disable(operation)', function() { - it("Disable a data source operation", function() { - var find = memory.getOperation('all'); - - assert.equal(find.name, 'all'); - assert.equal(find.enabled, true); - assert.equal(find.remoteEnabled, true); - - memory.disableRemote('all'); - - assert.equal(find.name, 'all'); - assert.equal(find.enabled, true); - assert.equal(find.remoteEnabled, false); - - memory.disable('all'); - - assert.equal(find.name, 'all'); - assert.equal(find.enabled, false); - assert.equal(find.remoteEnabled, false); - }); - }); - 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); @@ -154,7 +113,6 @@ describe('DataSource', function() { function existsAndShared(name, isRemoteEnabled) { var op = memory.getOperation(name); - console.log(op.name, op.remoteEnabled, isRemoteEnabled); assert(op.remoteEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled'); } }); diff --git a/test/geo-point.test.js b/test/geo-point.test.js index 0fd64ed6..fe8be350 100644 --- a/test/geo-point.test.js +++ b/test/geo-point.test.js @@ -1,41 +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 +// 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 index 12bc0d11..60470bed 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1,195 +1,250 @@ 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(done) { - /* example - + it("Require a model to include a property to be considered valid.", function() { User.validatesPresenceOf('first', 'last', 'age'); - */ - done(new Error('test not implemented')); + 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(done) { - /* example - + it("Require a property length to be within a specified range.", function() { User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}}); - */ - done(new Error('test not implemented')); + 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(done) { - /* example - + it("Require a value for `property` to be in the specified array.", function() { User.validatesInclusionOf('gender', {in: ['male', 'female']}); - */ - done(new Error('test not implemented')); + 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(done) { - /* example - + it("Require a value for `property` to not exist in the specified array.", function() { User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']}); - */ - done(new Error('test not implemented')); + 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(done) { - /* example - + it("Require a value for `property` to be a specific type of `Number`.", function() { User.validatesNumericalityOf('age', {int: true}); - */ - done(new Error('test not implemented')); + 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) { - /* example - User.validatesUniquenessOf('email', {message: 'email is not unique'}); - */ - done(new Error('test not implemented')); + + 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(done) { - /* example - - user.isValid(function (valid) { - if (!valid) { - user.errors // hash of errors {attr: [errmessage, errmessage, ...], attr: ...} - } - }); - */ - done(new Error('test not implemented')); + 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(done) { - /* example - - var oracle = asteroid.createDataSource({ - connector: 'oracle', - host: '111.22.333.44', - database: 'MYDB', - username: 'username', - password: 'password' - }); - User.attachTo(oracle); + it("Attach a model to a [DataSource](#data-source)", function() { + var MyModel = asteroid.createModel('my-model', {name: String}); - */ - done(new Error('test not implemented')); + 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) { - /* example - User.create({first: 'Joe', last: 'Bob'}, function(err, user) { - console.log(user instanceof User); // true + assert(user instanceof User); + done(); }); - */ - done(new Error('test not implemented')); }); }); describe('model.save([options], [callback])', function() { it("Save an instance of a Model to the attached data source.", function(done) { - /* example - 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); - } + assert(user.id); + assert(!err); + assert(!user.errors); + done(); }); - */ - done(new Error('test not implemented')); }); }); describe('model.updateAttributes(data, [callback])', function() { it("Save specified attributes to the attached data source.", function(done) { - /* example - - - user.updateAttributes({ - first: 'updatedFirst', - name: 'updatedLast' - }, fn); - */ - done(new Error('test not implemented')); + 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() { + describe('Model.upsert(data, callback)', function() { it("Update when record with id=data.id found, insert otherwise", function(done) { - /* example - - - */ - done(new Error('test not implemented')); + 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) { - /* example - - model.destroy(function(err) { - // model instance destroyed + 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(); + }); + }); + }); }); - - */ - done(new Error('test not implemented')); }); }); describe('Model.destroyAll(callback)', function() { it("Delete all Model instances from data source", function(done) { - done(new Error('test not implemented')); + (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) { - /* example - - User.find(23, function(err, user) { - console.info(user.id); // 23 + 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(); + }); }); - User.all({where: {age: {gt: 21}}, order: 'age DESC', limit: 10, skip: 10}) - */ - done(new Error('test not implemented')); }); }); describe('Model.count([query], callback)', function() { it("Query count of Model instances in data source", function(done) { - /* example - - User.count({approved: true}, function(err, count) { - console.log(count); // 2081 - }); - 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); - } + (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, User.login, { accepts: [ @@ -197,167 +252,250 @@ describe('Model', function() { {arg: 'password', type: 'string', required: true} ], returns: {arg: 'sessionId', type: 'any'}, - http: {path: '/sign-in'} + http: {path: '/sign-in', verb: 'get'} } ); - User.prototype.logout = function (fn) { - MySessionModel.destroyAll({userId: this.id}, fn); - } - - asteroid.remoteMethod(User, User.prototype.logout); - */ - done(new Error('test not implemented')); + 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.me', function() { - it("The id of the user 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.req', function() { - it("The express ServerRequest object", function(done) { - done(new Error('test not implemented')); - }); - }); - - describe('ctx.res', function() { - it("The express ServerResponse object", function(done) { - /* example - - - */ - done(new Error('test not implemented')); - }); + 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) { - /* example - + var Book = memory.createModel('book', {title: String, author: String}); + var Chapter = memory.createModel('chapter', {title: String}); + // by referencing model Book.hasMany(Chapter); - // specify the name - Book.hasMany('chapters', {model: Chapter}); - Book.create(function(err, book) { + Book.create({title: 'Into the Wild', author: 'Jon Krakauer'}, 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' + 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(); + }); + }); + }); }); }); - - */ - done(new Error('test not implemented')); }); }); - describe('Model.hasAndBelongsToMany()', function() { - it("TODO: implement / document", function(done) { - /* example - - - */ - done(new Error('test not implemented')); - }); - }); + // describe('Model.hasAndBelongsToMany()', function() { + // it("TODO: implement / document", function(done) { + // /* example - + // + // */ + // done(new Error('test not implemented')); + // }); + // }); - describe('Model.availableHooks()', function() { - it("Return a list of available hooks.", function(done) { - /* example - - console.log(User.availableHooks()); // ['save', ...] - - */ - 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.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'); - }); - }); - - 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'); - }); - }); +// 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 8f1563be..1492c6f0 100644 --- a/test/support.js +++ b/test/support.js @@ -6,6 +6,8 @@ assert = require('assert'); asteroid = require('../'); memoryConnector = asteroid.Memory; app = null; +TaskEmitter = require('sl-task-emitter'); +request = require('supertest'); beforeEach(function () { app = asteroid();