464 lines
12 KiB
JavaScript
464 lines
12 KiB
JavaScript
/*!
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var DataSource = require('loopback-datasource-juggler').DataSource
|
|
, registry = require('./registry')
|
|
, assert = require('assert')
|
|
, fs = require('fs')
|
|
, extend = require('util')._extend
|
|
, _ = require('underscore')
|
|
, RemoteObjects = require('strong-remoting')
|
|
, stringUtils = require('underscore.string')
|
|
, path = require('path');
|
|
|
|
/**
|
|
* The `App` object represents a Loopback application.
|
|
*
|
|
* The App object extends [Express](http://expressjs.com/api.html#express) and
|
|
* supports
|
|
* [Express / Connect middleware](http://expressjs.com/api.html#middleware). See
|
|
* [Express documentation](http://expressjs.com/api.html) for details.
|
|
*
|
|
* ```js
|
|
* var loopback = require('loopback');
|
|
* var app = loopback();
|
|
*
|
|
* app.get('/', function(req, res){
|
|
* res.send('hello world');
|
|
* });
|
|
*
|
|
* app.listen(3000);
|
|
* ```
|
|
*
|
|
* @class LoopBackApplication
|
|
* @header var app = loopback()
|
|
*/
|
|
function App() {
|
|
// this is a dummy placeholder for jsdox
|
|
}
|
|
|
|
/*!
|
|
* Export the app prototype.
|
|
*/
|
|
|
|
var app = exports = module.exports = {};
|
|
|
|
/**
|
|
* Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
|
|
*
|
|
* **NOTE:** Calling `app.remotes()` more than once returns only a single set of remote objects.
|
|
* @returns {RemoteObjects}
|
|
*/
|
|
|
|
app.remotes = function () {
|
|
if(this._remotes) {
|
|
return this._remotes;
|
|
} else {
|
|
var options = {};
|
|
|
|
if(this.get) {
|
|
options = this.get('remoting');
|
|
}
|
|
|
|
return (this._remotes = RemoteObjects.create(options));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Remove a route by reference.
|
|
*/
|
|
|
|
app.disuse = function (route) {
|
|
if(this.stack) {
|
|
for (var i = 0; i < this.stack.length; i++) {
|
|
if(this.stack[i].route === route) {
|
|
this.stack.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach a model to the app. The `Model` will be available on the
|
|
* `app.models` object.
|
|
*
|
|
* ```js
|
|
* // Attach an existing model
|
|
* var User = loopback.User;
|
|
* app.model(User);
|
|
*
|
|
* // Attach an existing model, alter some aspects of the model
|
|
* var User = loopback.User;
|
|
* app.model(User, { dataSource: 'db' });
|
|
*
|
|
* // LoopBack 1.x way: create and attach a new model (deprecated)
|
|
* var Widget = app.model('Widget', {
|
|
* dataSource: 'db',
|
|
* properties: {
|
|
* name: 'string'
|
|
* }
|
|
* });
|
|
* ```
|
|
*
|
|
* @param {Object|String} Model The model to attach.
|
|
* @options {Object} config The model's configuration.
|
|
* @property {String|DataSource} dataSource The `DataSource` to which to
|
|
* attach the model.
|
|
* @property {Boolean} [public] whether the model should be exposed via REST API
|
|
* @property {Object} [relations] relations to add/update
|
|
* @end
|
|
* @returns {ModelConstructor} the model class
|
|
*/
|
|
|
|
app.model = function (Model, config) {
|
|
var isPublic = true;
|
|
if (arguments.length > 1) {
|
|
config = config || {};
|
|
if (typeof Model === 'string') {
|
|
// create & attach the model - backwards compatibility
|
|
|
|
// create config for loopback.modelFromConfig
|
|
var modelConfig = extend({}, config);
|
|
modelConfig.options = extend({}, config.options);
|
|
modelConfig.name = Model;
|
|
|
|
// modeller does not understand `dataSource` option
|
|
delete modelConfig.dataSource;
|
|
|
|
Model = registry.createModel(modelConfig);
|
|
|
|
// delete config options already applied
|
|
['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
|
|
delete config[prop];
|
|
if (config.options) delete config.options[prop];
|
|
});
|
|
delete config.properties;
|
|
}
|
|
|
|
configureModel(Model, config, this);
|
|
isPublic = config.public !== false;
|
|
} else {
|
|
assert(Model.prototype instanceof registry.Model,
|
|
Model.modelName + ' must be a descendant of loopback.Model');
|
|
}
|
|
|
|
var modelName = Model.modelName;
|
|
this.models[modelName] =
|
|
this.models[classify(modelName)] =
|
|
this.models[camelize(modelName)] = Model;
|
|
|
|
this.models().push(Model);
|
|
|
|
if (isPublic && Model.sharedClass) {
|
|
this.remotes().addClass(Model.sharedClass);
|
|
if (Model.settings.trackChanges && Model.Change) {
|
|
this.remotes().addClass(Model.Change.sharedClass);
|
|
}
|
|
clearHandlerCache(this);
|
|
this.emit('modelRemoted', Model.sharedClass);
|
|
}
|
|
|
|
Model.shared = isPublic;
|
|
Model.app = this;
|
|
Model.emit('attached', this);
|
|
return Model;
|
|
};
|
|
|
|
/**
|
|
* Get the models exported by the app. Returns only models defined using `app.model()`
|
|
*
|
|
* There are two ways to access models:
|
|
*
|
|
* 1. Call `app.models()` to get a list of all models.
|
|
*
|
|
* ```js
|
|
* var models = app.models();
|
|
*
|
|
* models.forEach(function (Model) {
|
|
* console.log(Model.modelName); // color
|
|
* });
|
|
* ```
|
|
*
|
|
* **2. Use `app.model` to access a model by name.
|
|
* `app.model` has properties for all defined models.
|
|
*
|
|
* The following example illustrates accessing the `Product` and `CustomerReceipt` models
|
|
* using the `models` object.
|
|
*
|
|
* ```js
|
|
* var loopback = require('loopback');
|
|
* var app = loopback();
|
|
* app.boot({
|
|
* dataSources: {
|
|
* db: {connector: 'memory'}
|
|
* }
|
|
* });
|
|
*
|
|
* app.model('product', {dataSource: 'db'});
|
|
* app.model('customer-receipt', {dataSource: 'db'});
|
|
*
|
|
* // available based on the given name
|
|
* var Product = app.models.Product;
|
|
*
|
|
* // also available as camelCase
|
|
* var product = app.models.product;
|
|
*
|
|
* // multi-word models are avaiable as pascal cased
|
|
* var CustomerReceipt = app.models.CustomerReceipt;
|
|
*
|
|
* // also available as camelCase
|
|
* var customerReceipt = app.models.customerReceipt;
|
|
* ```
|
|
*
|
|
* @returns {Array} Array of model classes.
|
|
*/
|
|
|
|
app.models = function () {
|
|
return this._models || (this._models = []);
|
|
}
|
|
|
|
/**
|
|
* Define a DataSource.
|
|
*
|
|
* @param {String} name The data source name
|
|
* @param {Object} config The data source config
|
|
*/
|
|
|
|
app.dataSource = function (name, config) {
|
|
this.dataSources[name] =
|
|
this.dataSources[classify(name)] =
|
|
this.dataSources[camelize(name)] =
|
|
dataSourcesFromConfig(config, this.connectors);
|
|
}
|
|
|
|
/**
|
|
* Register a connector.
|
|
*
|
|
* When a new data-source is being added via `app.dataSource`, the connector
|
|
* name is looked up in the registered connectors first.
|
|
*
|
|
* Connectors are required to be explicitly registered only for applications
|
|
* using browserify, because browserify does not support dynamic require,
|
|
* which is used by LoopBack to automatically load the connector module.
|
|
*
|
|
* @param {String} name Name of the connector, e.g. 'mysql'.
|
|
* @param {Object} connector Connector object as returned
|
|
* by `require('loopback-connector-{name}')`.
|
|
*/
|
|
app.connector = function(name, connector) {
|
|
this.connectors[name] =
|
|
this.connectors[classify(name)] =
|
|
this.connectors[camelize(name)] = connector;
|
|
};
|
|
|
|
/**
|
|
* Get all remote objects.
|
|
* @returns {Object} [Remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
|
|
*/
|
|
|
|
app.remoteObjects = function () {
|
|
var result = {};
|
|
|
|
this.remotes().classes().forEach(function(sharedClass) {
|
|
result[sharedClass.name] = sharedClass.ctor;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
* Get a handler of the specified type from the handler cache.
|
|
*/
|
|
|
|
app.handler = function (type) {
|
|
var handlers = this._handlers || (this._handlers = {});
|
|
if(handlers[type]) {
|
|
return handlers[type];
|
|
}
|
|
|
|
var remotes = this.remotes();
|
|
var handler = this._handlers[type] = remotes.handler(type);
|
|
return handler;
|
|
}
|
|
|
|
/**
|
|
* An object to store dataSource instances.
|
|
*/
|
|
|
|
app.dataSources = app.datasources = {};
|
|
|
|
/**
|
|
* Enable app wide authentication.
|
|
*/
|
|
|
|
app.enableAuth = function() {
|
|
var remotes = this.remotes();
|
|
var app = this;
|
|
|
|
remotes.before('**', function(ctx, next, method) {
|
|
var req = ctx.req;
|
|
var Model = method.ctor;
|
|
var modelInstance = ctx.instance;
|
|
var modelId = modelInstance && modelInstance.id || req.param('id');
|
|
|
|
var modelSettings = Model.settings || {};
|
|
var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401;
|
|
if(!req.accessToken){
|
|
errStatusCode = 401;
|
|
}
|
|
|
|
if(Model.checkAccess) {
|
|
// Pause the request before checking access
|
|
// See https://github.com/strongloop/loopback-storage-service/issues/7
|
|
req.pause();
|
|
Model.checkAccess(
|
|
req.accessToken,
|
|
modelId,
|
|
method,
|
|
function(err, allowed) {
|
|
// Emit any cached data events that fired while checking access.
|
|
req.resume();
|
|
if(err) {
|
|
console.log(err);
|
|
next(err);
|
|
} else if(allowed) {
|
|
next();
|
|
} else {
|
|
|
|
var messages = {
|
|
403:'Access Denied',
|
|
404: ('could not find a model with id ' + modelId),
|
|
401:'Authorization Required'
|
|
};
|
|
|
|
var e = new Error(messages[errStatusCode] || messages[403]);
|
|
e.statusCode = errStatusCode;
|
|
next(e);
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
|
|
this.isAuthEnabled = true;
|
|
};
|
|
|
|
app.boot = function(options) {
|
|
throw new Error(
|
|
'`app.boot` was removed, use the new module loopback-boot instead');
|
|
}
|
|
|
|
function classify(str) {
|
|
return stringUtils.classify(str);
|
|
}
|
|
|
|
function camelize(str) {
|
|
return stringUtils.camelize(str);
|
|
}
|
|
|
|
function dataSourcesFromConfig(config, connectorRegistry) {
|
|
var connectorPath;
|
|
|
|
assert(typeof config === 'object',
|
|
'cannont create data source without config object');
|
|
|
|
if(typeof config.connector === 'string') {
|
|
var name = config.connector;
|
|
if (connectorRegistry[name]) {
|
|
config.connector = connectorRegistry[name];
|
|
} else {
|
|
connectorPath = path.join(__dirname, 'connectors', name + '.js');
|
|
|
|
if (fs.existsSync(connectorPath)) {
|
|
config.connector = require(connectorPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return registry.createDataSource(config);
|
|
}
|
|
|
|
function configureModel(ModelCtor, config, app) {
|
|
assert(ModelCtor.prototype instanceof registry.Model,
|
|
ModelCtor.modelName + ' must be a descendant of loopback.Model');
|
|
|
|
var dataSource = config.dataSource;
|
|
|
|
if(typeof dataSource === 'string') {
|
|
dataSource = app.dataSources[dataSource];
|
|
}
|
|
|
|
assert(dataSource instanceof DataSource,
|
|
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
|
|
config.dataSource +'"');
|
|
|
|
config = extend({}, config);
|
|
config.dataSource = dataSource;
|
|
|
|
registry.configureModel(ModelCtor, config);
|
|
}
|
|
|
|
function clearHandlerCache(app) {
|
|
app._handlers = undefined;
|
|
}
|
|
|
|
/**
|
|
* Listen for connections and update the configured port.
|
|
*
|
|
* When there are no parameters or there is only one callback parameter,
|
|
* the server will listen on `app.get('host')` and `app.get('port')`.
|
|
*
|
|
* ```js
|
|
* // listen on host/port configured in app config
|
|
* app.listen();
|
|
* ```
|
|
*
|
|
* Otherwise all arguments are forwarded to `http.Server.listen`.
|
|
*
|
|
* ```js
|
|
* // listen on the specified port and all hosts, ignore app config
|
|
* app.listen(80);
|
|
* ```
|
|
*
|
|
* The function also installs a `listening` callback that calls
|
|
* `app.set('port')` with the value returned by `server.address().port`.
|
|
* This way the port param contains always the real port number, even when
|
|
* listen was called with port number 0.
|
|
*
|
|
* @param {Function} cb If specified, the callback is added as a listener
|
|
* for the server's "listening" event.
|
|
* @returns {http.Server} A node `http.Server` with this application configured
|
|
* as the request handler.
|
|
*/
|
|
app.listen = function(cb) {
|
|
var self = this;
|
|
|
|
var server = require('http').createServer(this);
|
|
|
|
server.on('listening', function() {
|
|
self.set('port', this.address().port);
|
|
if (!self.get('url')) {
|
|
// A better default host would be `0.0.0.0`,
|
|
// but such URL is not supported by Windows
|
|
var host = self.get('host') || '127.0.0.1';
|
|
var url = 'http://' + host + ':' + self.get('port') + '/';
|
|
self.set('url', url);
|
|
}
|
|
});
|
|
|
|
var useAppConfig =
|
|
arguments.length == 0 ||
|
|
(arguments.length == 1 && typeof arguments[0] == 'function');
|
|
|
|
if (useAppConfig) {
|
|
server.listen(this.get('port'), this.get('host'), cb);
|
|
} else {
|
|
server.listen.apply(server, arguments);
|
|
}
|
|
|
|
return server;
|
|
}
|