diff --git a/README.md b/README.md index ac9e7582..33ecd580 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ v0.9.0 - [DataSource](#data-source) - [Connectors](#connectors) - [Loopback Types](#loopback-types) - - [GeoPoint](#geo-point) + - [GeoPoint](#geo-point) - [REST Router](#rest-router) - [Bundled Models](#bundled-models) - [User](#user-model) diff --git a/example/mobile-models/app.js b/example/mobile-models/app.js new file mode 100644 index 00000000..f671f360 --- /dev/null +++ b/example/mobile-models/app.js @@ -0,0 +1,45 @@ +var models = require('../../lib/models'); + +var loopback = require('../../'); +var app = loopback(); + +app.use(loopback.rest()); + +var dataSource = loopback.createDataSource('db', {connector: loopback.Memory}); + +var Application = models.Application(dataSource); + +app.model(Application); + + +var data = {pushSettings: [ + { "platform": "apns", + "apns": { + "pushOptions": { + "gateway": "gateway.sandbox.push.apple.com", + "cert": "credentials/apns_cert_dev.pem", + "key": "credentials/apns_key_dev.pem" + }, + + "feedbackOptions": { + "gateway": "feedback.sandbox.push.apple.com", + "cert": "credentials/apns_cert_dev.pem", + "key": "credentials/apns_key_dev.pem", + "batchFeedback": true, + "interval": 300 + } + }} +]} + +Application.create(data, function(err, data) { + console.log('Created: ', data.toObject()); +}); + + +Application.register('MyApp', 'My first mobile application', 'rfeng', function (err, result) { + console.log(result.toObject()); + + result.resetKeys(function (err, result) { + console.log(result.toObject()); + }); +}); diff --git a/lib/application.js b/lib/application.js index 5e573ad2..4ed7b9f9 100644 --- a/lib/application.js +++ b/lib/application.js @@ -25,6 +25,21 @@ app.remotes = function () { } } +/** + * Expose an object or Class remotely. + * + * @param {String} name The remote namespace (eg. url base) + * @param {Object|Function} obj The object to remote + */ + +app.remote = function (name, obj) { + // add the object to the remote exports + this.remotes().exports[name] = obj; + + // clear the handlers cache + this._handlers = {}; +} + /** * Remove a route by reference. */ @@ -44,6 +59,7 @@ app.disuse = function (route) { */ app._models = []; +app._services = []; /** * Expose a model. @@ -52,6 +68,8 @@ app._models = []; */ app.model = function (Model) { + var remotes = this.remotes(); + this._models.push(Model); Model.shared = true; Model.app = this; @@ -67,22 +85,28 @@ app.models = function () { } /** - * Get all remote objects. + * Expose a service. + * + * @param {String} name + * @param {Service} service */ -app.remoteObjects = function () { - var result = {}; - var models = this.models(); +app.service = function (name, service) { + this._services.push(service); + service.shared = true; - // add in models - models.forEach(function (ModelCtor) { - // only add shared models - if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') { - result[ModelCtor.pluralModelName] = ModelCtor; - } - }); - - return result; + service.app = this; + + // add to the remote exports + this.remote(name, service); +} + +/** + * Get all exposed services. + */ + +app.services = function () { + return this._services; } /** @@ -91,4 +115,29 @@ app.remoteObjects = function () { app.remotes = function () { return this._remotes || (this._remotes = RemoteObjects.create()); -} \ No newline at end of file +} + +/** + * Get a remotes handler. + */ + +app.handler = function (type) { + var handler = this._handlers[type]; + + if(!handler) { + // get the sl remoting object + var remotes = this.remotes(); + + // create and save the handler + handler = this._handlers[type] = remotes.handler(type); + } + + return handler; +} + +/*! + * Handlers + */ + +app._handlers = {}; + diff --git a/lib/middleware/rest.js b/lib/middleware/rest.js index 468d0a44..5f1d9144 100644 --- a/lib/middleware/rest.js +++ b/lib/middleware/rest.js @@ -17,21 +17,12 @@ module.exports = rest; function rest() { return function (req, res, next) { - var app = req.app; - var remotes = app.remotes(); - - // get all remote objects - var objs = app.remoteObjects(); - - // export remote objects - remotes.exports = objs; - - var handler = remotes.handler('rest'); + var handler = req.app.handler('rest'); if(req.url === '/routes') { res.send(handler.adapter.allRoutes()); } else if(req.url === '/models') { - return res.send(remotes.toJSON()); + return res.send(req.app.remotes().toJSON()); } else { handler(req, res, next); } diff --git a/lib/models/acl.js b/lib/models/acl.js index 14a6cf8c..42c80a04 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -1,17 +1,25 @@ -// Schema ACL options +/** +Schema ACL options +Object level permissions, for example, an album owned by a user -// Object level permissions +Factors to be authorized against: -// open: no protection -// none: always rejected -// owner: only the owner -// loggedIn: any logged in user -// roles: logged in users with the roles -// related: owner of the related objects - -// Class level permissions +* model name: Album +* model instance properties: userId of the album, friends, shared +* methods +* app and/or user ids/roles + ** loggedIn + ** roles + ** userId + ** appId + ** none + ** everyone + ** relations: owner/friend/granted +Class level permissions, for example, Album + * model name: Album + * methods // blog posts allow: ['owner', 'admin'] to: '*' // allow owner's of posts and admins to do anything allow: '*' to: ['find', 'read'] // allow everyone to read and find @@ -27,5 +35,4 @@ allow: 'owner' to: ['*.destroy', '*.save'] // scopes // URL level permissions - - +*/ diff --git a/lib/models/application.js b/lib/models/application.js index 173d2b97..e66e29da 100644 --- a/lib/models/application.js +++ b/lib/models/application.js @@ -1,17 +1,54 @@ -// Application model +// Authentication schemes +var AuthenticationSchemeSchema = { + scheme: String, // local, facebook, google, twitter, linkedin, github + credential: Object // Scheme-specific credentials +} + +var APNSSettingSchema = { + pushOptions: {type: { + gateway: String, + cert: String, + key: String + }}, + + feedbackOptions: {type: { + gateway: String, + cert: String, + key: String, + batchFeedback: Boolean, + interval: Number + }} +}; + +// Push notification settings +var PushNotificationSettingSchema = { + platform: {type: String, required: true}, // apns, gcm, mpns + // configuration: {type: Object} // platform-specific configurations + apns: APNSSettingSchema +} + +/** + * Data model for Application + */ var ApplicationSchema = { // Basic information - id: {type: String, required: true}, - name: {type: String, required: true}, - description: String, // description - icon: String, // The icon url - public: Boolean, - permissions: [String], + id: {type: String, required: true}, // The id + name: {type: String, required: true}, // The name + description: String, // The description + icon: String, // The icon image url - userId: String, + owner: String, // The user id of the developer who registers the application + collaborators: [String], // A list of users ids who have permissions to work on this app - status: String, + // EMail + email: String, // e-mail address + emailVerified: Boolean, // Is the e-mail verified + + // oAuth 2.0 settings + url: String, // The application url + callbackUrls: [String], // oAuth 2.0 code/token callback url + permissions: [String], // A list of permissions required by the application // Keys clientKey: String, @@ -21,21 +58,80 @@ var ApplicationSchema = { masterKey: String, // Push notification - pushPlatforms: [String], - pushCredentials: [], + pushSettings: [PushNotificationSettingSchema], - // Authentication - authenticationEnabled: Boolean, - anonymousAllowed: Boolean, - schemes: [String], // Basic, facebook, github, google - attachedCredentials: [], + // User Authentication + authenticationEnabled: {type: Boolean, default: true}, + anonymousAllowed: {type: Boolean, default: true}, + authenticationSchemes: [AuthenticationSchemeSchema], - // email - email: String, // e-mail address - emailVerified: Boolean, // Is the e-mail verified + status: {type: String, default: 'sandbox'}, // Status of the application, production/sandbox/disabled - collaborators: [String], // A list of users ids who have permissions to work on this app - - created: Date, - lastUpdated: Date + // Timestamps + created: {type: Date, default: Date}, + modified: {type: Date, default: Date} }; + + +/** + * Application management functions + */ + +var crypto = require('crypto'); + +function generateKey(hmacKey, algorithm) { + hmacKey = hmacKey || 'loopback'; + algorithm = algorithm || 'sha256'; + var hmac = crypto.createHmac(algorithm, hmacKey); + var buf = crypto.randomBytes(64); + hmac.update(buf); + return hmac.digest('base64'); +} + +module.exports = function (dataSource) { + dataSource = dataSource || new require('loopback-data').ModelBuilder(); + + // var AuthenticationScheme = dataSource.define('AuthenticationScheme', AuthenticationSchemeSchema); + // ApplicationSchema.authenticationSchemes = [AuthenticationScheme]; + + // var PushNotificationSetting = dataSource.define('PushNotificationSetting', PushNotificationSettingSchema); + // ApplicationSchema.pushSettings = [PushNotificationSetting]; + + var Application = dataSource.define('Application', ApplicationSchema); + + // Application.hasMany(AuthenticationScheme, {as: 'authenticationSchemes', foreignKey: 'appId'}); + // Application.hasMany(PushNotificationSetting, {as: 'pushNotificationSettings', foreignKey: 'appId'}); + + Application.afterInitialize = function () { + var app = this; + // use data argument to update object + app.created = app.modified = new Date(); + app.id = generateKey('id', 'sha1'); + app.clientKey = generateKey('client'); + app.javaScriptKey = generateKey('javaScript'); + app.restApiKey = generateKey('restApi'); + app.windowsKey = generateKey('windows'); + app.masterKey = generateKey('master'); + }; + + // Register a new application + Application.register = function (name, description, owner, cb) { + Application.create({name: name, description: description, owner: owner}, cb); + } + + Application.prototype.resetKeys = function(cb) { + this.clientKey = generateKey('client'); + this.javaScriptKey = generateKey('javaScript'); + this.restApiKey = generateKey('restApi'); + this.windowsKey = generateKey('windows'); + this.masterKey = generateKey('master'); + this.save(cb); + } + + return Application; +} + + + + + diff --git a/lib/models/index.js b/lib/models/index.js new file mode 100644 index 00000000..35f6a412 --- /dev/null +++ b/lib/models/index.js @@ -0,0 +1,6 @@ +exports.Application = require('./application'); +exports.ACL = require('./acl'); +exports.Role = require('./role'); +exports.Installation = require('./installation'); + + diff --git a/lib/models/installation.js b/lib/models/installation.js index 37e8bd0d..d54b3246 100644 --- a/lib/models/installation.js +++ b/lib/models/installation.js @@ -1,4 +1,26 @@ -// Device registration +// See Device registration var InstallationSchema = { + id: { + type: String, + required: true, + id: 1 + }, + appId: String, // Application id + appVersion: String, // Application version + userId: String, // User id + deviceToken: String, + deviceType: String, + subscriptions: [String], + status: {type: String, default: 'active'}, // Status of the application, production/sandbox/disabled + + // Timestamps + created: {type: Date, default: Date}, + modified: {type: Date, default: Date} }; + +module.exports = function(dataSource) { + dataSource = dataSource || new require('loopback-data').ModelBuilder(); + var Installation = dataSource.define('Installation', InstallationSchema); + return Installation; +} diff --git a/lib/models/role.js b/lib/models/role.js index a9546777..810897e2 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -1,11 +1,18 @@ // Role model var RoleSchema = { - id: {type: String, required: true}, - name: {type: String, required: true}, - roles: [String], - users: [String], - acl: [], + id: {type: String, required: true}, // Id + name: {type: String, required: true}, // The name of a role + description: String, // Description + roles: [String], // A role can be an aggregate of other roles + users: [String], // A role contains a list of user ids - created: Date, - lastUpdated: Date + // Timestamps + created: {type: Date, default: Date}, + modified: {type: Date, default: Date} +} + +module.exports = function(dataSource) { + dataSource = dataSource || new require('loopback-data').ModelBuilder(); + var Role = dataSource.define('Role', RoleSchema); + return Role; } \ No newline at end of file diff --git a/lib/models/user.js b/lib/models/user.js index 2865668d..93fdb07b 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -22,6 +22,7 @@ var properties = { email: String, emailVerified: Boolean, verificationToken: String, + credentials: [ 'UserCredential' // User credentials, private or public, such as private/public keys, Kerberos tickets, oAuth tokens, facebook, google, github ids ], @@ -328,4 +329,4 @@ User.prototype.logout = function (fn) { * Setup the base user. */ -User.setup(); \ No newline at end of file +User.setup(); diff --git a/test/README.md b/test/README.md index 85df4d75..15ca0530 100644 --- a/test/README.md +++ b/test/README.md @@ -2,6 +2,12 @@ - [app](#app) - [app.model(Model)](#app-appmodelmodel) - [app.models()](#app-appmodels) +<<<<<<< HEAD +======= + - [loopback](#loopback) + - [loopback.createDataSource(options)](#loopback-loopbackcreatedatasourceoptions) + - [loopback.remoteMethod(Model, fn, [options]);](#loopback-loopbackremotemethodmodel-fn-options) +>>>>>>> master - [DataSource](#datasource) - [dataSource.createModel(name, properties, settings)](#datasource-datasourcecreatemodelname-properties-settings) - [dataSource.operations()](#datasource-datasourceoperations) @@ -76,6 +82,48 @@ assert.equal(models.length, 1); assert.equal(models[0].modelName, 'color'); ``` +<<<<<<< HEAD +======= + +# loopback + +## loopback.createDataSource(options) +Create a data source with a connector.. + +```js +var dataSource = loopback.createDataSource({ + connector: loopback.Memory +}); +assert(dataSource.connector()); +``` + + +## loopback.remoteMethod(Model, fn, [options]); +Setup a remote method.. + +```js +var Product = loopback.createModel('product', {price: Number}); + +Product.stats = function(fn) { + // ... +} + +loopback.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); +``` + +>>>>>>> master # DataSource diff --git a/test/loopback.test.js b/test/loopback.test.js index 2bedb14b..bedb0e1a 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -1,6 +1,6 @@ describe('loopback', function() { describe('loopback.createDataSource(options)', function(){ - it('Create a data source with a connector', function() { + it('Create a data source with a connector.', function() { var dataSource = loopback.createDataSource({ connector: loopback.Memory }); @@ -9,7 +9,7 @@ describe('loopback', function() { }); describe('loopback.remoteMethod(Model, fn, [options]);', function() { - it("Setup a remote method", function() { + it("Setup a remote method.", function() { var Product = loopback.createModel('product', {price: Number}); Product.stats = function(fn) { @@ -31,18 +31,4 @@ describe('loopback', function() { assert.equal(Product.stats.shared, true); }); }); - - describe('loopback.memory([name])', function(){ - it('Get an in-memory data source. Use one if it already exists', function() { - var memory = loopback.memory(); - assertValidDataSource(memory); - var m1 = loopback.memory(); - var m2 = loopback.memory('m2'); - var alsoM2 = loopback.memory('m2'); - - assert(m1 === memory); - assert(m1 !== m2); - assert(alsoM2 === m2); - }); - }); }); \ No newline at end of file