Add support for app level Model isolation

- `loopback.registry` is now a true global registry
 - `app.registry` is unique per app object
 - `Model.registry` is set when a Model is created using any registry method
 - `loopback.localRegistry` and `loopback({localRegistry: true})` when set to `true` this will create a `Registry` per `Application`. It defaults to `false`.
This commit is contained in:
Ritchie Martori 2015-04-01 14:50:36 -07:00
parent 5a51c7f0fa
commit b9170751bc
18 changed files with 415 additions and 120 deletions

View File

@ -375,6 +375,8 @@ module.exports = function(ACL) {
*/ */
ACL.checkAccessForContext = function(context, callback) { ACL.checkAccessForContext = function(context, callback) {
var registry = this.registry;
if (!(context instanceof AccessContext)) { if (!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
} }
@ -394,7 +396,7 @@ module.exports = function(ACL) {
var staticACLs = this.getStaticACLs(model.modelName, property); var staticACLs = this.getStaticACLs(model.modelName, property);
var self = this; var self = this;
var roleModel = loopback.getModelByType(Role); var roleModel = registry.getModelByType(Role);
this.find({where: {model: model.modelName, property: propertyQuery, this.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function(err, acls) { accessType: accessTypeQuery}}, function(err, acls) {
if (err) { if (err) {

View File

@ -24,9 +24,11 @@ module.exports = function(RoleMapping) {
* @param {Application} application * @param {Application} application
*/ */
RoleMapping.prototype.application = function(callback) { RoleMapping.prototype.application = function(callback) {
var registry = this.constructor.registry;
if (this.principalType === RoleMapping.APPLICATION) { if (this.principalType === RoleMapping.APPLICATION) {
var applicationModel = this.constructor.Application || var applicationModel = this.constructor.Application ||
loopback.getModelByType(loopback.Application); registry.getModelByType('Application');
applicationModel.findById(this.principalId, callback); applicationModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function() { process.nextTick(function() {
@ -42,9 +44,10 @@ module.exports = function(RoleMapping) {
* @param {User} user * @param {User} user
*/ */
RoleMapping.prototype.user = function(callback) { RoleMapping.prototype.user = function(callback) {
var RoleMapping = this.constructor;
if (this.principalType === RoleMapping.USER) { if (this.principalType === RoleMapping.USER) {
var userModel = this.constructor.User || var userModel = RoleMapping.User ||
loopback.getModelByType(loopback.User); RoleMapping.registry.getModelByType('User');
userModel.findById(this.principalId, callback); userModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function() { process.nextTick(function() {
@ -60,9 +63,11 @@ module.exports = function(RoleMapping) {
* @param {User} childUser * @param {User} childUser
*/ */
RoleMapping.prototype.childRole = function(callback) { RoleMapping.prototype.childRole = function(callback) {
var registry = this.constructor.registry;
if (this.principalType === RoleMapping.ROLE) { if (this.principalType === RoleMapping.ROLE) {
var roleModel = this.constructor.Role || var roleModel = this.constructor.Role ||
loopback.getModelByType(loopback.Role); registry.getModelByType(loopback.Role);
roleModel.findById(this.principalId, callback); roleModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function() { process.nextTick(function() {

View File

@ -29,7 +29,8 @@ module.exports = function(Role) {
// Set up the connection to users/applications/roles once the model // Set up the connection to users/applications/roles once the model
Role.once('dataSourceAttached', function() { Role.once('dataSourceAttached', function() {
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); var registry = Role.registry;
var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping);
Role.prototype.users = function(callback) { Role.prototype.users = function(callback) {
roleMappingModel.find({where: {roleId: this.id, roleMappingModel.find({where: {roleId: this.id,
principalType: RoleMapping.USER}}, function(err, mappings) { principalType: RoleMapping.USER}}, function(err, mappings) {
@ -242,6 +243,8 @@ module.exports = function(Role) {
context = new AccessContext(context); context = new AccessContext(context);
} }
var registry = this.registry;
debug('isInRole(): %s', role); debug('isInRole(): %s', role);
context.debug(); context.debug();
@ -277,7 +280,7 @@ module.exports = function(Role) {
return; return;
} }
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping);
this.findOne({where: {name: role}}, function(err, result) { this.findOne({where: {name: role}}, function(err, result) {
if (err) { if (err) {
if (callback) callback(err); if (callback) callback(err);
@ -332,6 +335,7 @@ module.exports = function(Role) {
context = new AccessContext(context); context = new AccessContext(context);
} }
var roles = []; var roles = [];
var registry = this.registry;
var addRole = function(role) { var addRole = function(role) {
if (role && roles.indexOf(role) === -1) { if (role && roles.indexOf(role) === -1) {
@ -358,7 +362,7 @@ module.exports = function(Role) {
}); });
}); });
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping);
context.principals.forEach(function(p) { context.principals.forEach(function(p) {
// Check against the role mappings // Check against the role mappings
var principalType = p.type || undefined; var principalType = p.type || undefined;

View File

@ -25,6 +25,7 @@ module.exports = function(Scope) {
*/ */
Scope.checkPermission = function(scope, model, property, accessType, callback) { Scope.checkPermission = function(scope, model, property, accessType, callback) {
var ACL = loopback.ACL; var ACL = loopback.ACL;
var registry = this.registry;
assert(ACL, assert(ACL,
'ACL model must be defined before Scope.checkPermission is called'); 'ACL model must be defined before Scope.checkPermission is called');
@ -32,7 +33,7 @@ module.exports = function(Scope) {
if (err) { if (err) {
if (callback) callback(err); if (callback) callback(err);
} else { } else {
var aclModel = loopback.getModelByType(ACL); var aclModel = registry.getModelByType(ACL);
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
} }
}); });

View File

@ -334,6 +334,7 @@ module.exports = function(User) {
User.prototype.verify = function(options, fn) { User.prototype.verify = function(options, fn) {
var user = this; var user = this;
var userModel = this.constructor; var userModel = this.constructor;
var registry = userModel.registry;
assert(typeof options === 'object', 'options required when calling user.verify()'); assert(typeof options === 'object', 'options required when calling user.verify()');
assert(options.type, 'You must supply a verification type (options.type)'); assert(options.type, 'You must supply a verification type (options.type)');
assert(options.type === 'email', 'Unsupported verification type'); assert(options.type === 'email', 'Unsupported verification type');
@ -364,7 +365,7 @@ module.exports = function(User) {
options.redirect; options.redirect;
// Email model // Email model
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); var Email = options.mailer || this.constructor.email || registry.getModelByType(loopback.Email);
// Set a default token generation function if one is not provided // Set a default token generation function if one is not provided
var tokenGenerator = options.generateVerificationToken || User.generateVerificationToken; var tokenGenerator = options.generateVerificationToken || User.generateVerificationToken;

View File

@ -3,7 +3,7 @@
*/ */
var DataSource = require('loopback-datasource-juggler').DataSource; var DataSource = require('loopback-datasource-juggler').DataSource;
var registry = require('./registry'); var Registry = require('./registry');
var assert = require('assert'); var assert = require('assert');
var fs = require('fs'); var fs = require('fs');
var extend = require('util')._extend; var extend = require('util')._extend;
@ -104,6 +104,8 @@ app.disuse = function(route) {
app.model = function(Model, config) { app.model = function(Model, config) {
var isPublic = true; var isPublic = true;
var registry = this.registry;
if (arguments.length > 1) { if (arguments.length > 1) {
config = config || {}; config = config || {};
if (typeof Model === 'string') { if (typeof Model === 'string') {
@ -130,7 +132,7 @@ app.model = function(Model, config) {
configureModel(Model, config, this); configureModel(Model, config, this);
isPublic = config.public !== false; isPublic = config.public !== false;
} else { } else {
assert(Model.prototype instanceof registry.Model, assert(Model.prototype instanceof Model.registry.getModel('Model'),
Model.modelName + ' must be a descendant of loopback.Model'); Model.modelName + ' must be a descendant of loopback.Model');
} }
@ -216,7 +218,7 @@ app.models = function() {
* @param {Object} config The data source config * @param {Object} config The data source config
*/ */
app.dataSource = function(name, config) { app.dataSource = function(name, config) {
var ds = dataSourcesFromConfig(config, this.connectors); var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
this.dataSources[name] = this.dataSources[name] =
this.dataSources[classify(name)] = this.dataSources[classify(name)] =
this.dataSources[camelize(name)] = ds; this.dataSources[camelize(name)] = ds;
@ -362,14 +364,14 @@ app.boot = function(options) {
'`app.boot` was removed, use the new module loopback-boot instead'); '`app.boot` was removed, use the new module loopback-boot instead');
}; };
function dataSourcesFromConfig(config, connectorRegistry) { function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
var connectorPath; var connectorPath;
assert(typeof config === 'object', assert(typeof config === 'object',
'cannont create data source without config object'); 'cannont create data source without config object');
if (typeof config.connector === 'string') { if (typeof config.connector === 'string') {
var name = config.connector; name = config.connector;
if (connectorRegistry[name]) { if (connectorRegistry[name]) {
config.connector = connectorRegistry[name]; config.connector = connectorRegistry[name];
} else { } else {
@ -385,7 +387,7 @@ function dataSourcesFromConfig(config, connectorRegistry) {
} }
function configureModel(ModelCtor, config, app) { function configureModel(ModelCtor, config, app) {
assert(ModelCtor.prototype instanceof registry.Model, assert(ModelCtor.prototype instanceof ModelCtor.registry.getModel('Model'),
ModelCtor.modelName + ' must be a descendant of loopback.Model'); ModelCtor.modelName + ' must be a descendant of loopback.Model');
var dataSource = config.dataSource; var dataSource = config.dataSource;
@ -405,7 +407,7 @@ function configureModel(ModelCtor, config, app) {
config = extend({}, config); config = extend({}, config);
config.dataSource = dataSource; config.dataSource = dataSource;
registry.configureModel(ModelCtor, config); app.registry.configureModel(ModelCtor, config);
} }
function clearHandlerCache(app) { function clearHandlerCache(app) {

View File

@ -1,43 +1,43 @@
module.exports = function(loopback) { module.exports = function(registry) {
// NOTE(bajtos) we must use static require() due to browserify limitations // NOTE(bajtos) we must use static require() due to browserify limitations
loopback.Email = createModel( registry.Email = createModel(
require('../common/models/email.json'), require('../common/models/email.json'),
require('../common/models/email.js')); require('../common/models/email.js'));
loopback.Application = createModel( registry.Application = createModel(
require('../common/models/application.json'), require('../common/models/application.json'),
require('../common/models/application.js')); require('../common/models/application.js'));
loopback.AccessToken = createModel( registry.AccessToken = createModel(
require('../common/models/access-token.json'), require('../common/models/access-token.json'),
require('../common/models/access-token.js')); require('../common/models/access-token.js'));
loopback.RoleMapping = createModel( registry.RoleMapping = createModel(
require('../common/models/role-mapping.json'), require('../common/models/role-mapping.json'),
require('../common/models/role-mapping.js')); require('../common/models/role-mapping.js'));
loopback.Role = createModel( registry.Role = createModel(
require('../common/models/role.json'), require('../common/models/role.json'),
require('../common/models/role.js')); require('../common/models/role.js'));
loopback.ACL = createModel( registry.ACL = createModel(
require('../common/models/acl.json'), require('../common/models/acl.json'),
require('../common/models/acl.js')); require('../common/models/acl.js'));
loopback.Scope = createModel( registry.Scope = createModel(
require('../common/models/scope.json'), require('../common/models/scope.json'),
require('../common/models/scope.js')); require('../common/models/scope.js'));
loopback.User = createModel( registry.User = createModel(
require('../common/models/user.json'), require('../common/models/user.json'),
require('../common/models/user.js')); require('../common/models/user.js'));
loopback.Change = createModel( registry.Change = createModel(
require('../common/models/change.json'), require('../common/models/change.json'),
require('../common/models/change.js')); require('../common/models/change.js'));
loopback.Checkpoint = createModel( registry.Checkpoint = createModel(
require('../common/models/checkpoint.json'), require('../common/models/checkpoint.json'),
require('../common/models/checkpoint.js')); require('../common/models/checkpoint.js'));
@ -50,18 +50,18 @@ module.exports = function(loopback) {
MAIL: 'mail' MAIL: 'mail'
}; };
loopback.Email.autoAttach = dataSourceTypes.MAIL; registry.Email.autoAttach = dataSourceTypes.MAIL;
loopback.PersistedModel.autoAttach = dataSourceTypes.DB; registry.getModel('PersistedModel').autoAttach = dataSourceTypes.DB;
loopback.User.autoAttach = dataSourceTypes.DB; registry.User.autoAttach = dataSourceTypes.DB;
loopback.AccessToken.autoAttach = dataSourceTypes.DB; registry.AccessToken.autoAttach = dataSourceTypes.DB;
loopback.Role.autoAttach = dataSourceTypes.DB; registry.Role.autoAttach = dataSourceTypes.DB;
loopback.RoleMapping.autoAttach = dataSourceTypes.DB; registry.RoleMapping.autoAttach = dataSourceTypes.DB;
loopback.ACL.autoAttach = dataSourceTypes.DB; registry.ACL.autoAttach = dataSourceTypes.DB;
loopback.Scope.autoAttach = dataSourceTypes.DB; registry.Scope.autoAttach = dataSourceTypes.DB;
loopback.Application.autoAttach = dataSourceTypes.DB; registry.Application.autoAttach = dataSourceTypes.DB;
function createModel(definitionJson, customizeFn) { function createModel(definitionJson, customizeFn) {
var Model = loopback.createModel(definitionJson); var Model = registry.createModel(definitionJson);
customizeFn(Model); customizeFn(Model);
return Model; return Model;
} }

8
lib/global-registry.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = function() {
var Registry = require('./registry');
var registry = global.__LOOPBACK_GLOBAL_REGISTRY__;
if (!registry) {
registry = global.__LOOPBACK_GLOBAL_REGISTRY__ = new Registry();
}
return registry;
};

View File

@ -10,6 +10,9 @@ var ejs = require('ejs');
var path = require('path'); var path = require('path');
var merge = require('util')._extend; var merge = require('util')._extend;
var assert = require('assert'); var assert = require('assert');
var Registry = require('./registry');
var getGlobalRegistry = require('./global-registry');
var juggler = require('loopback-datasource-juggler');
/** /**
* LoopBack core module. It provides static properties and * LoopBack core module. It provides static properties and
@ -25,6 +28,7 @@ var assert = require('assert');
* @property {String} mime * @property {String} mime
* @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property. * @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property.
* @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property. * @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property.
* @property {Registry} registry The global `Registry` object.
* @property {String} faviconFile Path to a default favicon shipped with LoopBack. * @property {String} faviconFile Path to a default favicon shipped with LoopBack.
* Use as follows: `app.use(require('serve-favicon')(loopback.faviconFile));` * Use as follows: `app.use(require('serve-favicon')(loopback.faviconFile));`
* @class loopback * @class loopback
@ -45,6 +49,22 @@ loopback.version = require('../package.json').version;
loopback.mime = express.mime; loopback.mime = express.mime;
Object.defineProperty(loopback, 'registry', {
get: getGlobalRegistry
});
Object.defineProperty(loopback, 'Model', {
get: function() {
return this.registry.getModel('Model');
}
});
Object.defineProperty(loopback, 'PersistedModel', {
get: function() {
return this.registry.getModel('PersistedModel');
}
});
/*! /*!
* Create an loopback application. * Create an loopback application.
* *
@ -52,7 +72,7 @@ loopback.mime = express.mime;
* @api public * @api public
*/ */
function createApplication() { function createApplication(options) {
var app = loopbackExpress(); var app = loopbackExpress();
merge(app, proto); merge(app, proto);
@ -76,6 +96,13 @@ function createApplication() {
app.connector('memory', loopback.Memory); app.connector('memory', loopback.Memory);
app.connector('remote', loopback.Remote); app.connector('remote', loopback.Remote);
if (loopback.localRegistry || options && options.localRegistry === true) {
// setup the app registry
var registry = app.registry = new Registry();
} else {
app.registry = loopback.registry;
}
return app; return app;
} }
@ -91,7 +118,6 @@ function mixin(source) {
} }
mixin(require('./runtime')); mixin(require('./runtime'));
mixin(require('./registry'));
/*! /*!
* Expose static express methods like `express.errorHandler`. * Expose static express methods like `express.errorHandler`.
@ -191,8 +217,198 @@ loopback.template = function(file) {
require('../server/current-context')(loopback); require('../server/current-context')(loopback);
/**
* Create a named vanilla JavaScript class constructor with an attached
* set of properties and options.
*
* This function comes with two variants:
* * `loopback.createModel(name, properties, options)`
* * `loopback.createModel(config)`
*
* In the second variant, the parameters `name`, `properties` and `options`
* are provided in the config object. Any additional config entries are
* interpreted as `options`, i.e. the following two configs are identical:
*
* ```js
* { name: 'Customer', base: 'User' }
* { name: 'Customer', options: { base: 'User' } }
* ```
*
* **Example**
*
* Create an `Author` model using the three-parameter variant:
*
* ```js
* loopback.createModel(
* 'Author',
* {
* firstName: 'string',
* lastName: 'string'
* },
* {
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* }
* );
* ```
*
* Create the same model using a config object:
*
* ```js
* loopback.createModel({
* name: 'Author',
* properties: {
* firstName: 'string',
* lastName: 'string'
* },
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* });
* ```
*
* @param {String} name Unique name.
* @param {Object} properties
* @param {Object} options (optional)
*
* @header loopback.createModel
*/
loopback.createModel = function(name, properties, options) {
return this.registry.createModel.apply(this.registry, arguments);
};
/**
* Alter an existing Model class.
* @param {Model} ModelCtor The model constructor to alter.
* @options {Object} config Additional configuration to apply
* @property {DataSource} dataSource Attach the model to a dataSource.
* @property {Object} [relations] Model relations to add/update.
*
* @header loopback.configureModel(ModelCtor, config)
*/
loopback.configureModel = function(ModelCtor, config) {
return this.registry.configureModel.apply(this.registry, arguments);
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`
* @param {String} modelName The model name
* @returns {Model} The model class
*
* @header loopback.findModel(modelName)
*/
loopback.findModel = function(modelName) {
return this.registry.findModel.apply(this.registry, arguments);
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`. Throw an error when no such model exists.
*
* @param {String} modelName The model name
* @returns {Model} The model class
*
* @header loopback.getModel(modelName)
*/
loopback.getModel = function(modelName) {
return this.registry.getModel.apply(this.registry, arguments);
};
/**
* Look up a model class by the base model class.
* The method can be used by LoopBack
* to find configured models in models.json over the base model.
* @param {Model} modelType The base model class
* @returns {Model} The subclass if found or the base class
*
* @header loopback.getModelByType(modelType)
*/
loopback.getModelByType = function(modelType) {
return this.registry.getModelByType.apply(this.registry, arguments);
};
/**
* Create a data source with passing the provided options to the connector.
*
* @param {String} name Optional name.
* @options {Object} options Data Source options
* @property {Object} connector LoopBack connector.
* @property {*} [*] Other connector properties.
* See the relevant connector documentation.
*/
loopback.createDataSource = function(name, options) {
return this.registry.createDataSource.apply(this.registry, arguments);
};
/**
* Get an in-memory data source. Use one if it already exists.
*
* @param {String} [name] The name of the data source.
* If not provided, the `'default'` is used.
*/
loopback.memory = function(name) {
return this.registry.memory.apply(this.registry, arguments);
};
/**
* Set the default `dataSource` for a given `type`.
* @param {String} type The datasource type.
* @param {Object|DataSource} dataSource The data source settings or instance
* @returns {DataSource} The data source instance.
*
* @header loopback.setDefaultDataSourceForType(type, dataSource)
*/
loopback.setDefaultDataSourceForType = function(type, dataSource) {
return this.registry.setDefaultDataSourceForType.apply(this.registry, arguments);
};
/**
* Get the default `dataSource` for a given `type`.
* @param {String} type The datasource type.
* @returns {DataSource} The data source instance
*/
loopback.getDefaultDataSourceForType = function(type) {
return this.registry.getDefaultDataSourceForType.apply(this.registry, arguments);
};
/**
* Attach any model that does not have a dataSource to
* the default dataSource for the type the Model requests
*/
loopback.autoAttach = function() {
return this.registry.autoAttach.apply(this.registry, arguments);
};
loopback.autoAttachModel = function(ModelCtor) {
return this.registry.autoAttachModel.apply(this.registry, arguments);
};
// temporary alias to simplify migration of code based on <=2.0.0-beta3
Object.defineProperty(loopback, 'DataModel', {
get: function() {
return this.registry.DataModel;
}
});
/*! /*!
* Built in models / services * Built in models / services
*/ */
require('./builtin-models')(loopback); require('./builtin-models')(loopback);
loopback.DataSource = juggler.DataSource;

View File

@ -95,15 +95,22 @@ module.exports = function(registry) {
* @property [{string}] settings.acls Array of ACLs for the model. * @property [{string}] settings.acls Array of ACLs for the model.
* @class * @class
*/ */
var Model = registry.modelBuilder.define('Model'); var Model = registry.modelBuilder.define('Model');
Model.registry = registry;
/*! /*!
* Called when a model is extended. * Called when a model is extended.
*/ */
Model.setup = function() { Model.setup = function() {
var ModelCtor = this; var ModelCtor = this;
var Parent = this.super_;
if (!ModelCtor.registry && Parent && Parent.registry) {
ModelCtor.registry = Parent.registry;
}
var options = this.settings; var options = this.settings;
var typeName = this.modelName; var typeName = this.modelName;
@ -250,6 +257,7 @@ module.exports = function(registry) {
*/ */
var _aclModel = null; var _aclModel = null;
Model._ACL = function getACL(ACL) { Model._ACL = function getACL(ACL) {
var registry = this.registry;
if (ACL !== undefined) { if (ACL !== undefined) {
// The function is used as a setter // The function is used as a setter
_aclModel = ACL; _aclModel = ACL;

View File

@ -9,8 +9,7 @@ var deprecated = require('depd')('loopback');
var debug = require('debug')('loopback:persisted-model'); var debug = require('debug')('loopback:persisted-model');
module.exports = function(registry) { module.exports = function(registry) {
var Model = registry.getModel('Model');
var Model = registry.Model;
/** /**
* Extends Model with basic query and CRUD support. * Extends Model with basic query and CRUD support.
@ -1401,7 +1400,7 @@ module.exports = function(registry) {
} }
PersistedModel._defineChangeModel = function() { PersistedModel._defineChangeModel = function() {
var BaseChangeModel = registry.getModel('Change'); var BaseChangeModel = this.registry.getModel('Change');
assert(BaseChangeModel, assert(BaseChangeModel,
'Change model must be defined before enabling change replication'); 'Change model must be defined before enabling change replication');

View File

@ -1,12 +1,3 @@
/*
* This file exports methods and objects for manipulating
* Models and DataSources.
*
* It is an internal file that should not be used outside of loopback.
* All exported entities can be accessed via the `loopback` object.
* @private
*/
var assert = require('assert'); var assert = require('assert');
var extend = require('util')._extend; var extend = require('util')._extend;
var juggler = require('loopback-datasource-juggler'); var juggler = require('loopback-datasource-juggler');
@ -14,11 +5,23 @@ var debug = require('debug')('loopback:registry');
var DataSource = juggler.DataSource; var DataSource = juggler.DataSource;
var ModelBuilder = juggler.ModelBuilder; var ModelBuilder = juggler.ModelBuilder;
var registry = module.exports; module.exports = Registry;
registry.defaultDataSources = {}; /**
* Define and reference `Models` and `DataSources`.
*
* @class
*/
registry.modelBuilder = new ModelBuilder(); function Registry() {
this.defaultDataSources = {};
this.modelBuilder = new ModelBuilder();
require('./model')(this);
require('./persisted-model')(this);
// Set the default model base class.
this.modelBuilder.defaultModelBaseClass = this.getModel('Model');
}
/** /**
* Create a named vanilla JavaScript class constructor with an attached * Create a named vanilla JavaScript class constructor with an attached
@ -84,7 +87,8 @@ registry.modelBuilder = new ModelBuilder();
* @header loopback.createModel * @header loopback.createModel
*/ */
registry.createModel = function(name, properties, options) { Registry.prototype.createModel = function(name, properties, options) {
if (arguments.length === 1 && typeof name === 'object') { if (arguments.length === 1 && typeof name === 'object') {
var config = name; var config = name;
name = config.name; name = config.name;
@ -106,7 +110,7 @@ registry.createModel = function(name, properties, options) {
if (baseName === 'DataModel') { if (baseName === 'DataModel') {
console.warn('Model `%s` is extending deprecated `DataModel. ' + console.warn('Model `%s` is extending deprecated `DataModel. ' +
'Use `PeristedModel` instead.', name); 'Use `PeristedModel` instead.', name);
BaseModel = this.PersistedModel; BaseModel = this.getModel('PeristedModel');
} else { } else {
console.warn('Model `%s` is extending an unknown model `%s`. ' + console.warn('Model `%s` is extending an unknown model `%s`. ' +
'Using `PersistedModel` as the base.', name, baseName); 'Using `PersistedModel` as the base.', name, baseName);
@ -114,9 +118,9 @@ registry.createModel = function(name, properties, options) {
} }
} }
BaseModel = BaseModel || this.PersistedModel; BaseModel = BaseModel || this.getModel('PersistedModel');
var model = BaseModel.extend(name, properties, options); var model = BaseModel.extend(name, properties, options);
model.registry = this;
// try to attach // try to attach
try { try {
@ -174,7 +178,7 @@ function addACL(acls, acl) {
* @header loopback.configureModel(ModelCtor, config) * @header loopback.configureModel(ModelCtor, config)
*/ */
registry.configureModel = function(ModelCtor, config) { Registry.prototype.configureModel = function(ModelCtor, config) {
var settings = ModelCtor.settings; var settings = ModelCtor.settings;
var modelName = ModelCtor.modelName; var modelName = ModelCtor.modelName;
@ -248,25 +252,26 @@ registry.configureModel = function(ModelCtor, config) {
/** /**
* Look up a model class by name from all models created by * Look up a model class by name from all models created by
* `loopback.createModel()` * `loopback.createModel()`
* @param {String} modelName The model name * @param {String|Function} modelOrName The model name or a `Model` constructor.
* @returns {Model} The model class * @returns {Model} The model class
* *
* @header loopback.findModel(modelName) * @header loopback.findModel(modelName)
*/ */
registry.findModel = function(modelName) { Registry.prototype.findModel = function(modelName) {
if (typeof modelType === 'function') return modelName;
return this.modelBuilder.models[modelName]; return this.modelBuilder.models[modelName];
}; };
/** /**
* Look up a model class by name from all models created by * Look up a model class by name from all models created by
* `loopback.createModel()`. Throw an error when no such model exists. * `loopback.createModel()`. **Throw an error when no such model exists.**
* *
* @param {String} modelName The model name * @param {String} modelOrName The model name or a `Model` constructor.
* @returns {Model} The model class * @returns {Model} The model class
* *
* @header loopback.getModel(modelName) * @header loopback.getModel(modelName)
*/ */
registry.getModel = function(modelName) { Registry.prototype.getModel = function(modelName) {
var model = this.findModel(modelName); var model = this.findModel(modelName);
if (model) return model; if (model) return model;
@ -282,9 +287,17 @@ registry.getModel = function(modelName) {
* *
* @header loopback.getModelByType(modelType) * @header loopback.getModelByType(modelType)
*/ */
registry.getModelByType = function(modelType) { Registry.prototype.getModelByType = function(modelType) {
assert(typeof modelType === 'function', var type = typeof modelType;
'The model type must be a constructor'); var accepted = ['function', 'string'];
assert(accepted.indexOf(type) > -1,
'The model type must be a constructor or model name');
if (type === 'string') {
modelType = this.getModel(modelType);
}
var models = this.modelBuilder.models; var models = this.modelBuilder.models;
for (var m in models) { for (var m in models) {
if (models[m].prototype instanceof modelType) { if (models[m].prototype instanceof modelType) {
@ -302,12 +315,11 @@ registry.getModelByType = function(modelType) {
* @property {Object} connector LoopBack connector. * @property {Object} connector LoopBack connector.
* @property {*} [*] Other&nbsp;connector properties. * @property {*} [*] Other&nbsp;connector properties.
* See the relevant connector documentation. * See the relevant connector documentation.
*
* @header loopback.createDataSource(name, options)
*/ */
registry.createDataSource = function(name, options) { Registry.prototype.createDataSource = function(name, options) {
var self = this; var self = this;
var ds = new DataSource(name, options, self.modelBuilder); var ds = new DataSource(name, options, self.modelBuilder);
ds.createModel = function(name, properties, settings) { ds.createModel = function(name, properties, settings) {
settings = settings || {}; settings = settings || {};
@ -340,11 +352,9 @@ registry.createDataSource = function(name, options) {
* *
* @param {String} [name] The name of the data source. * @param {String} [name] The name of the data source.
* If not provided, the `'default'` is used. * If not provided, the `'default'` is used.
*
* @header loopback.memory([name])
*/ */
registry.memory = function(name) { Registry.prototype.memory = function(name) {
name = name || 'default'; name = name || 'default';
var memory = ( var memory = (
this._memoryDataSources || (this._memoryDataSources = {}) this._memoryDataSources || (this._memoryDataSources = {})
@ -368,7 +378,7 @@ registry.memory = function(name) {
* @header loopback.setDefaultDataSourceForType(type, dataSource) * @header loopback.setDefaultDataSourceForType(type, dataSource)
*/ */
registry.setDefaultDataSourceForType = function(type, dataSource) { Registry.prototype.setDefaultDataSourceForType = function(type, dataSource) {
var defaultDataSources = this.defaultDataSources; var defaultDataSources = this.defaultDataSources;
if (!(dataSource instanceof DataSource)) { if (!(dataSource instanceof DataSource)) {
@ -382,21 +392,19 @@ registry.setDefaultDataSourceForType = function(type, dataSource) {
/** /**
* Get the default `dataSource` for a given `type`. * Get the default `dataSource` for a given `type`.
* @param {String} type The datasource type. * @param {String} type The datasource type.
* @returns {DataSource} The data source instance. * @returns {DataSource} The data source instance
* @header loopback.getDefaultDataSourceForType(type)
*/ */
registry.getDefaultDataSourceForType = function(type) { Registry.prototype.getDefaultDataSourceForType = function(type) {
return this.defaultDataSources && this.defaultDataSources[type]; return this.defaultDataSources && this.defaultDataSources[type];
}; };
/** /**
* Attach any model that does not have a dataSource to * Attach any model that does not have a dataSource to
* the default dataSource for the type the Model requests * the default dataSource for the type the Model requests
* @header loopback.autoAttach()
*/ */
registry.autoAttach = function() { Registry.prototype.autoAttach = function() {
var models = this.modelBuilder.models; var models = this.modelBuilder.models;
assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object'); assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object');
@ -410,7 +418,7 @@ registry.autoAttach = function() {
}, this); }, this);
}; };
registry.autoAttachModel = function(ModelCtor) { Registry.prototype.autoAttachModel = function(ModelCtor) {
if (ModelCtor.autoAttach) { if (ModelCtor.autoAttach) {
var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach); var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach);
@ -424,18 +432,8 @@ registry.autoAttachModel = function(ModelCtor) {
} }
}; };
registry.DataSource = DataSource;
/*
* Core models
* @private
*/
registry.Model = require('./model')(registry);
registry.PersistedModel = require('./persisted-model')(registry);
// temporary alias to simplify migration of code based on <=2.0.0-beta3 // temporary alias to simplify migration of code based on <=2.0.0-beta3
Object.defineProperty(registry, 'DataModel', { Object.defineProperty(Registry.prototype, 'DataModel', {
get: function() { get: function() {
var stackLines = new Error().stack.split('\n'); var stackLines = new Error().stack.split('\n');
console.warn('loopback.DataModel is deprecated, ' + console.warn('loopback.DataModel is deprecated, ' +
@ -445,6 +443,3 @@ Object.defineProperty(registry, 'DataModel', {
return this.PersistedModel; return this.PersistedModel;
} }
}); });
// Set the default model base class. This is done after the Model class is defined.
registry.modelBuilder.defaultModelBaseClass = registry.Model;

View File

@ -28,6 +28,7 @@ function rest() {
return function restApiHandler(req, res, next) { return function restApiHandler(req, res, next) {
var app = req.app; var app = req.app;
var registry = app.registry;
// added for https://github.com/strongloop/loopback/issues/1134 // added for https://github.com/strongloop/loopback/issues/1134
if (app.get('legacyExplorer') !== false) { if (app.get('legacyExplorer') !== false) {
@ -55,14 +56,8 @@ function rest() {
} }
if (app.isAuthEnabled) { if (app.isAuthEnabled) {
// NOTE(bajtos) It would be better to search app.models for a model var AccessToken = registry.getModelByType('AccessToken');
// of type AccessToken instead of searching all loopback models. handlers.push(loopback.token({ model: AccessToken, app: app }));
// Unfortunately that's not supported now.
// Related discussions:
// https://github.com/strongloop/loopback/pull/167
// https://github.com/strongloop/loopback/commit/f07446a
var AccessToken = loopback.getModelByType(loopback.AccessToken);
handlers.push(loopback.token({ model: AccessToken }));
} }
handlers.push(function(req, res, next) { handlers.push(function(req, res, next) {

View File

@ -68,11 +68,8 @@ function escapeRegExp(str) {
function token(options) { function token(options) {
options = options || {}; options = options || {};
var TokenModel = options.model || loopback.AccessToken; var TokenModel;
if (typeof TokenModel === 'string') {
// Make it possible to configure the model in middleware.json
TokenModel = loopback.getModel(TokenModel);
}
var currentUserLiteral = options.currentUserLiteral; var currentUserLiteral = options.currentUserLiteral;
if (currentUserLiteral && (typeof currentUserLiteral !== 'string')) { if (currentUserLiteral && (typeof currentUserLiteral !== 'string')) {
debug('Set currentUserLiteral to \'me\' as the value is not a string.'); debug('Set currentUserLiteral to \'me\' as the value is not a string.');
@ -81,10 +78,23 @@ function token(options) {
if (typeof currentUserLiteral === 'string') { if (typeof currentUserLiteral === 'string') {
currentUserLiteral = escapeRegExp(currentUserLiteral); currentUserLiteral = escapeRegExp(currentUserLiteral);
} }
assert(typeof TokenModel === 'function',
'loopback.token() middleware requires a AccessToken model');
return function(req, res, next) { return function(req, res, next) {
var app = req.app;
var registry = app.registry;
if (!TokenModel) {
if (registry === loopback.registry) {
TokenModel = options.model || loopback.AccessToken;
} else if (options.model) {
TokenModel = registry.getModel(options.model);
} else {
TokenModel = registry.getModel('AccessToken');
}
}
assert(typeof TokenModel === 'function',
'loopback.token() middleware requires a AccessToken model');
if (req.accessToken !== undefined) { if (req.accessToken !== undefined) {
rewriteUserLiteral(req, currentUserLiteral); rewriteUserLiteral(req, currentUserLiteral);
return next(); return next();

View File

@ -352,7 +352,7 @@ function createTestApp(testToken, settings, done) {
var app = loopback(); var app = loopback();
app.use(loopback.cookieParser('secret')); app.use(loopback.cookieParser('secret'));
app.use(loopback.token({model: 'MyToken', currentUserLiteral: 'me'})); app.use(loopback.token({model: Token, currentUserLiteral: 'me'}));
app.get('/token', function(req, res) { app.get('/token', function(req, res) {
res.cookie('authorization', testToken.id, {signed: true}); res.cookie('authorization', testToken.id, {signed: true});
res.end(); res.end();

View File

@ -628,7 +628,7 @@ describe('app', function() {
var Foo = app.models.foo; var Foo = app.models.foo;
var f = new Foo(); var f = new Foo();
assert(f instanceof loopback.Model); assert(f instanceof app.registry.getModel('Model'));
}); });
it('interprets extra first-level keys as options', function() { it('interprets extra first-level keys as options', function() {
@ -673,14 +673,15 @@ describe('app', function() {
describe('app.model(ModelCtor, config)', function() { describe('app.model(ModelCtor, config)', function() {
it('attaches the model to a datasource', function() { it('attaches the model to a datasource', function() {
var previousModel = loopback.registry.findModel('TestModel');
app.dataSource('db', { connector: 'memory' }); app.dataSource('db', { connector: 'memory' });
var TestModel = loopback.Model.extend('TestModel');
// TestModel was most likely already defined in a different test,
// thus TestModel.dataSource may be already set
delete TestModel.dataSource;
app.model(TestModel, { dataSource: 'db' }); if (previousModel) {
delete previousModel.dataSource;
}
assert(!previousModel || !previousModel.dataSource);
app.model('TestModel', { dataSource: 'db' });
expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db); expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db);
}); });
}); });

View File

@ -60,8 +60,8 @@ describe('DataSource', function() {
}); });
var Color = ds.createModel('color', {name: String}); var Color = ds.createModel('color', {name: String});
assert(Color.prototype instanceof loopback.Model); assert(Color.prototype instanceof Color.registry.getModel('Model'));
assert.equal(Color.base, loopback.Model); assert.equal(Color.base.modelName, 'PersistedModel');
}); });
}); });

48
test/registries.test.js Normal file
View File

@ -0,0 +1,48 @@
describe('Registry', function() {
describe('one per app', function() {
it('should allow two apps to reuse the same model name', function(done) {
var appFoo = loopback();
var appBar = loopback();
var modelName = 'MyModel';
var subModelName = 'Sub' + modelName;
var settings = {base: 'PersistedModel'};
appFoo.set('perAppRegistries', true);
appBar.set('perAppRegistries', true);
var dsFoo = appFoo.dataSource('dsFoo', {connector: 'memory'});
var dsBar = appFoo.dataSource('dsBar', {connector: 'memory'});
var FooModel = appFoo.model(modelName, settings);
var FooSubModel = appFoo.model(subModelName, settings);
var BarModel = appBar.model(modelName, settings);
var BarSubModel = appBar.model(subModelName, settings);
FooModel.attachTo(dsFoo);
FooSubModel.attachTo(dsFoo);
BarModel.attachTo(dsBar);
BarSubModel.attachTo(dsBar);
FooModel.hasMany(FooSubModel);
BarModel.hasMany(BarSubModel);
expect(appFoo.models[modelName]).to.not.equal(appBar.models[modelName]);
BarModel.create({name: 'bar'}, function(err, bar) {
assert(!err);
bar.subMyModels.create({parent: 'bar'}, function(err) {
assert(!err);
FooSubModel.find(function(err, foos) {
assert(!err);
expect(foos).to.eql([]);
BarSubModel.find(function(err, bars) {
assert(!err);
expect(bars.map(function(f) {
return f.parent;
})).to.eql(['bar']);
done();
});
});
});
});
});
});
});