loopback/lib/application.js

617 lines
16 KiB
JavaScript
Raw Normal View History

2013-05-01 19:11:43 +00:00
/**
* Module dependencies.
*/
var DataSource = require('loopback-datasource-juggler').DataSource
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
2013-05-23 16:53:42 +00:00
, assert = require('assert')
2013-10-29 21:12:23 +00:00
, fs = require('fs')
2013-07-25 23:24:00 +00:00
, RemoteObjects = require('strong-remoting')
2013-10-29 21:12:23 +00:00
, swagger = require('strong-remoting/ext/swagger')
, stringUtils = require('underscore.string')
, path = require('path');
2013-05-01 19:11:43 +00:00
/**
* Export the app prototype.
*/
var app = exports = module.exports = {};
/**
2013-05-23 16:53:42 +00:00
* Create a set of remote objects.
*/
2013-05-23 16:53:42 +00:00
app.remotes = function () {
if(this._remotes) {
return this._remotes;
} else {
return (this._remotes = RemoteObjects.create());
}
}
/**
* Remove a route by reference.
2013-05-01 19:11:43 +00:00
*/
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);
}
}
}
}
2013-05-24 14:59:23 +00:00
/**
2013-06-06 00:11:21 +00:00
* Expose a model.
2013-05-01 19:11:43 +00:00
*
2013-06-06 00:11:21 +00:00
* @param Model {Model}
2013-05-01 19:11:43 +00:00
*/
2013-05-24 14:59:23 +00:00
2013-10-29 21:12:23 +00:00
app.model = function (Model, config) {
if(arguments.length === 1) {
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
assert(Model.pluralModelName, 'Model must have a "pluralModelName" property');
2013-10-29 21:12:23 +00:00
this.remotes().exports[Model.pluralModelName] = Model;
2013-11-15 04:19:46 +00:00
this.models().push(Model);
2013-10-29 21:12:23 +00:00
Model.shared = true;
Model.app = this;
Model.emit('attached', this);
return;
}
var modelName = Model;
2013-11-18 20:52:00 +00:00
config = config || {};
assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string');
2013-10-29 21:12:23 +00:00
Model =
this.models[modelName] =
this.models[classify(modelName)] =
this.models[camelize(modelName)] = modelFromConfig(modelName, config, this);
2013-11-18 20:52:00 +00:00
if(config.public !== false) {
this.model(Model);
}
2013-10-29 21:12:23 +00:00
return Model;
2013-05-24 14:59:23 +00:00
}
/**
2013-06-06 00:11:21 +00:00
* Get all exposed models.
2013-05-24 14:59:23 +00:00
*/
app.models = function () {
2013-11-15 04:19:46 +00:00
return this._models || (this._models = []);
2013-05-01 19:11:43 +00:00
}
2013-10-31 17:06:43 +00:00
/**
* Define a DataSource.
*/
app.dataSource = function (name, config) {
this.dataSources[name] =
this.dataSources[classify(name)] =
this.dataSources[camelize(name)] = dataSourcesFromConfig(config);
}
2013-05-24 22:08:23 +00:00
/**
2013-07-17 21:30:38 +00:00
* Get all remote objects.
2013-05-24 22:08:23 +00:00
*/
2013-07-17 21:30:38 +00:00
app.remoteObjects = function () {
var result = {};
var models = this.models();
2013-05-24 22:08:23 +00:00
2013-07-17 21:30:38 +00:00
// add in models
models.forEach(function (ModelCtor) {
// only add shared models
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
result[ModelCtor.pluralModelName] = ModelCtor;
}
});
return result;
2013-05-24 22:08:23 +00:00
}
2013-05-24 14:59:23 +00:00
/**
* Get the apps set of remote objects.
*/
app.remotes = function () {
return this._remotes || (this._remotes = RemoteObjects.create());
2013-07-25 23:24:00 +00:00
}
/**
* Enable documentation
*/
app.docs = function (options) {
var remotes = this.remotes();
swagger(remotes, options);
}
/*!
* 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;
}
2013-10-29 21:12:23 +00:00
/**
* An object to store dataSource instances.
*/
app.dataSources = app.datasources = {};
2013-11-15 04:19:46 +00:00
/**
* Enable app wide authentication.
*/
app.enableAuth = function() {
var remotes = this.remotes();
remotes.before('**', function(ctx, next, method) {
var req = ctx.req;
var Model = method.ctor;
var modelInstance = ctx.instance;
2013-12-11 05:49:18 +00:00
var modelId = modelInstance && modelInstance.id || req.param('id');
2013-11-15 04:19:46 +00:00
if(Model.checkAccess) {
Model.checkAccess(
req.accessToken,
modelId,
method.name,
function(err, allowed) {
if(err) {
console.log(err);
next(err);
} else if(allowed) {
next();
} else {
var e = new Error('Access Denied');
e.statusCode = 401;
next(e);
}
2013-11-15 04:19:46 +00:00
}
);
} else {
next();
}
2013-11-15 04:19:46 +00:00
});
}
2013-10-29 21:12:23 +00:00
/**
* Initialize the app using JSON and JavaScript files.
*
* @throws {Error} If config is not valid
* @throws {Error} If boot fails
*/
app.boot = function(options) {
options = options || {};
if(typeof options === 'string') {
options = {appRootDir: options};
}
var app = this;
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
2013-10-29 21:12:23 +00:00
var ctx = {};
var appConfig = options.app;
var modelConfig = options.models;
var dataSourceConfig = options.dataSources;
if(!appConfig) {
appConfig = tryReadConfig(appRootDir, 'app') || {};
2013-10-29 21:12:23 +00:00
}
if(!modelConfig) {
modelConfig = tryReadConfig(appRootDir, 'models') || {};
2013-10-29 21:12:23 +00:00
}
if(!dataSourceConfig) {
dataSourceConfig = tryReadConfig(appRootDir, 'datasources') || {};
2013-10-29 21:12:23 +00:00
}
assertIsValidConfig('app', appConfig);
assertIsValidConfig('model', modelConfig);
assertIsValidConfig('data source', dataSourceConfig);
appConfig.host =
process.env.npm_config_host ||
process.env.OPENSHIFT_SLS_IP ||
process.env.OPENSHIFT_NODEJS_IP ||
process.env.HOST ||
appConfig.host ||
process.env.npm_package_config_host ||
app.get('host');
appConfig.port =
process.env.npm_config_port ||
process.env.OPENSHIFT_SLS_PORT ||
process.env.OPENSHIFT_NODEJS_PORT ||
process.env.PORT ||
appConfig.port ||
process.env.npm_package_config_port ||
app.get('port') ||
3000;
appConfig.restApiRoot =
appConfig.restApiRoot ||
app.get('restApiRoot') ||
'/api';
2013-10-29 21:12:23 +00:00
if(appConfig.host !== undefined) {
assert(typeof appConfig.host === 'string', 'app.host must be a string');
app.set('host', appConfig.host);
}
if(appConfig.port !== undefined) {
var portType = typeof appConfig.port;
assert(portType === 'string' || portType === 'number', 'app.port must be a string or number');
app.set('port', appConfig.port);
}
assert(appConfig.restApiRoot !== undefined, 'app.restBasePath is required');
assert(typeof appConfig.restApiRoot === 'string', 'app.restBasePath must be a string');
assert(/^\//.test(appConfig.restApiRoot), 'app.restBasePath must start with "/"');
app.set('restApiRoot', appConfig.restBasePath);
for(var configKey in appConfig) {
var cur = app.get(configKey);
if(cur === undefined || cur === null) {
app.set(configKey, appConfig[configKey]);
}
}
2013-10-29 21:12:23 +00:00
// instantiate data sources
forEachKeyedObject(dataSourceConfig, function(key, obj) {
2013-10-31 17:06:43 +00:00
app.dataSource(key, obj);
2013-10-29 21:12:23 +00:00
});
// instantiate models
forEachKeyedObject(modelConfig, function(key, obj) {
app.model(key, obj);
});
2013-11-20 22:18:54 +00:00
// try to attach models to dataSources by type
try {
require('./loopback').autoAttach();
} catch(e) {
if(e.name === 'AssertionError') {
console.warn(e);
} else {
throw e;
}
}
2013-11-19 20:23:02 +00:00
// disable token requirement for swagger, if available
var swagger = app.remotes().exports.swagger;
var requireTokenForSwagger = appConfig.swagger
&& appConfig.swagger.requireToken;
if(swagger) {
swagger.requireToken = requireTokenForSwagger || false;
}
2013-10-29 21:12:23 +00:00
// require directories
var requiredModels = requireDir(path.join(appRootDir, 'models'));
var requiredBootScripts = requireDir(path.join(appRootDir, 'boot'));
2013-10-29 21:12:23 +00:00
}
function assertIsValidConfig(name, config) {
if(config) {
assert(typeof config === 'object', name + ' config must be a valid JSON object');
}
}
function forEachKeyedObject(obj, fn) {
if(typeof obj !== 'object') return;
Object.keys(obj).forEach(function(key) {
fn(key, obj[key]);
});
}
function classify(str) {
return stringUtils.classify(str);
}
function camelize(str) {
return stringUtils.camelize(str);
}
function dataSourcesFromConfig(config) {
var connectorPath;
assert(typeof config === 'object',
'cannont create data source without config object');
if(typeof config.connector === 'string') {
connectorPath = path.join(__dirname, 'connectors', config.connector+'.js');
if(fs.existsSync(connectorPath)) {
config.connector = require(connectorPath);
}
}
2013-10-29 21:12:23 +00:00
return require('./loopback').createDataSource(config);
}
function modelFromConfig(name, config, app) {
var ModelCtor = require('./loopback').createModel(name, config.properties, config.options);
var dataSource = app.dataSources[config.dataSource];
assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"');
ModelCtor.attachTo(dataSource);
return ModelCtor;
}
function requireDir(dir, basenames) {
assert(dir, 'cannot require directory contents without directory name');
var requires = {};
if (arguments.length === 2) {
// if basenames argument is passed, explicitly include those files
basenames.forEach(function (basename) {
var filepath = Path.resolve(Path.join(dir, basename));
requires[basename] = tryRequire(filepath);
});
} else if (arguments.length === 1) {
// if basenames arguments isn't passed, require all javascript
// files (except for those prefixed with _) and all directories
var files = tryReadDir(dir);
// sort files in lowercase alpha for linux
files.sort(function (a,b) {
a = a.toLowerCase();
b = b.toLowerCase();
if (a < b) {
return -1;
} else if (b < a) {
return 1;
} else {
return 0;
}
});
files.forEach(function (filename) {
// ignore index.js and files prefixed with underscore
if ((filename === 'index.js') || (filename[0] === '_')) { return; }
var filepath = path.resolve(path.join(dir, filename));
var ext = path.extname(filename);
var stats = fs.statSync(filepath);
// only require files supported by require.extensions (.txt .md etc.)
if (stats.isFile() && !(ext in require.extensions)) { return; }
var basename = path.basename(filename, ext);
requires[basename] = tryRequire(filepath);
});
}
return requires;
};
function tryRequire(modulePath) {
try {
return require.apply(this, arguments);
} catch(e) {
console.error('failed to require "%s"', modulePath);
throw e;
}
}
function tryReadDir() {
try {
return fs.readdirSync.apply(fs, arguments);
} catch(e) {
return [];
}
}
function isModelCtor(obj) {
return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor';
}
function isDataSource(obj) {
return obj instanceof DataSource;
}
function tryReadConfig(cwd, fileName) {
try {
return require(path.join(cwd, fileName + '.json'));
} catch(e) {
if(e.code !== "MODULE_NOT_FOUND") {
throw e;
}
}
}
/**
* Install all express middleware required by LoopBack.
*
* It is possible to inject your own middleware by listening on one of the
* following events:
*
* - `middleware:preprocessors` is emitted after all other
* request-preprocessing middleware was installed, but before any
* request-handling middleware is configured.
*
* Usage:
* ```js
* app.on('middleware:preprocessors', function() {
* app.use(loopback.limit('5.5mb'))
* });
* ```
* - `middleware:handlers` is emitted when it's time to add your custom
* request-handling middleware. Note that you should not install any
* express routes at this point (express routes are discussed later).
*
* Usage:
* ```js
* app.on('middleware:handlers', function() {
* app.use('/admin', adminExpressApp);
* app.use('/custom', function(req, res, next) {
* res.send(200, { url: req.url });
* });
* });
* ```
* - `middleware:error-loggers` is emitted at the end, before the loopback
* error handling middleware is installed. This is the point where you
* can install your own middleware to log errors.
*
* Notes:
* - The middleware function must take four parameters, otherwise it won't
* be called by express.
*
* - It should also call `next(err)` to let the loopback error handler convert
* the error to an HTTP error response.
*
* Usage:
* ```js
* var bunyan = require('bunyan');
* var log = bunyan.createLogger({name: "myapp"});
* app.on('middleware:error-loggers', function() {
* app.use(function(err, req, res, next) {
* log.error(err);
* next(err);
* });
* });
* ```
*
* Express routes should be added after `installMiddleware` was called.
* This way the express router middleware is injected at the right place in the
* middleware chain. If you add an express route before calling this function,
* bad things will happen: Express will automatically add the router
* middleware and since we haven't added request-preprocessing middleware like
* cookie & body parser yet, your route handlers will receive raw unprocessed
* requests.
*
* This is the correct order in which to call `app` methods:
* ```js
* app.boot(__dirname); // optional
*
* app.installMiddleware();
*
* // [register your express routes here]
*
* app.listen();
* ```
*/
app.installMiddleware = function() {
var loopback = require('../');
/*
* Request pre-processing
*/
this.use(loopback.favicon());
// TODO(bajtos) refactor to app.get('loggerFormat')
var loggerFormat = this.get('env') === 'development' ? 'dev' : 'default';
this.use(loopback.logger(loggerFormat));
this.use(loopback.cookieParser(this.get('cookieSecret')));
this.use(loopback.token({ model: this.models.accessToken }));
this.use(loopback.bodyParser());
this.use(loopback.methodOverride());
// Allow the app to install custom preprocessing middleware
this.emit('middleware:preprocessors');
/*
* Request handling
*/
// LoopBack REST transport
this.use(this.get('restApiRoot') || '/api', loopback.rest());
// Allow the app to install custom request handling middleware
this.emit('middleware:handlers');
// Let express routes handle requests that were not handled
// by any of the middleware registered above.
// This way LoopBack REST and API Explorer take precedence over
// express routes.
this.use(this.router);
// The static file server should come after all other routes
// Every request that goes through the static middleware hits
// the file system to check if a file exists.
this.use(loopback.static(path.join(__dirname, 'public')));
// Requests that get this far won't be handled
// by any middleware. Convert them into a 404 error
// that will be handled later down the chain.
this.use(loopback.urlNotFound());
/*
* Error handling
*/
// Allow the app to install custom error logging middleware
this.emit('middleware:error-handlers');
// The ultimate error handler.
this.use(loopback.errorHandler());
};
/**
* 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 will be 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);
});
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;
}