From 1bf94a1b495cff777ddb00b783f1e41469fd88bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 Jun 2014 10:40:13 +0200 Subject: [PATCH 1/2] Start strong-docs API docs. Add docs.json with strong-docs configuration. Extract the example of `models.json` into docs/configuration.md. --- docs.json | 10 +++++++++ docs/configuration.md | 50 +++++++++++++++++++++++++++++++++++++++++ index.js | 52 +------------------------------------------ 3 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 docs.json create mode 100644 docs/configuration.md diff --git a/docs.json b/docs.json new file mode 100644 index 0000000..83908f8 --- /dev/null +++ b/docs.json @@ -0,0 +1,10 @@ +{ + "content": [ + { + "title": "Bootstrap API", + "depth": 2 + }, + "index.js", + "docs/configuration.md" + ] +} diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..082e959 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,50 @@ +## Configuration and conventions + +### Model Definitions + +The following is example JSON for two `Model` definitions: +"dealership" and "location". + +```js +{ + "dealership": { + // a reference, by name, to a dataSource definition + "dataSource": "my-db", + // the options passed to Model.extend(name, properties, options) + "options": { + "relations": { + "cars": { + "type": "hasMany", + "model": "Car", + "foreignKey": "dealerId" + } + } + }, + // the properties passed to Model.extend(name, properties, options) + "properties": { + "id": {"id": true}, + "name": "String", + "zip": "Number", + "address": "String" + } + }, + "car": { + "dataSource": "my-db" + "properties": { + "id": { + "type": "String", + "required": true, + "id": true + }, + "make": { + "type": "String", + "required": true + }, + "model": { + "type": "String", + "required": true + } + } + } +} +``` diff --git a/index.js b/index.js index 2b309e7..64158b3 100644 --- a/index.js +++ b/index.js @@ -42,56 +42,6 @@ var debug = require('debug')('loopback:boot'); * * Throws an error if the config object is not valid or if boot fails. * - * - * **Model Definitions** - * - * The following is example JSON for two `Model` definitions: - * "dealership" and "location". - * - * ```js - * { - * "dealership": { - * // a reference, by name, to a dataSource definition - * "dataSource": "my-db", - * // the options passed to Model.extend(name, properties, options) - * "options": { - * "relations": { - * "cars": { - * "type": "hasMany", - * "model": "Car", - * "foreignKey": "dealerId" - * } - * } - * }, - * // the properties passed to Model.extend(name, properties, options) - * "properties": { - * "id": {"id": true}, - * "name": "String", - * "zip": "Number", - * "address": "String" - * } - * }, - * "car": { - * "dataSource": "my-db" - * "properties": { - * "id": { - * "type": "String", - * "required": true, - * "id": true - * }, - * "make": { - * "type": "String", - * "required": true - * }, - * "model": { - * "type": "String", - * "required": true - * } - * } - * } - * } - * ``` - * * @param app LoopBack application created by `loopback()`. * @options {String|Object} options Boot options; If String, this is * the application root directory; if object, has below properties. @@ -107,7 +57,7 @@ var debug = require('debug')('loopback:boot'); * `datasources.json`. Defaults to `appRootDir`. * @end * - * @header boot(app, [options]) + * @header bootLoopBackApp(app, [options]) */ exports = module.exports = function bootLoopBackApp(app, options) { From b14800416a86a69aa4ec830b898b2a404c20d51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 2 Jun 2014 18:47:46 +0200 Subject: [PATCH 2/2] Split the boot process into two steps Split bootLoopBackApp into two steps: - compile - execute Most of the changes are just shuffling the existing code around. What has changed: - `loopback.autoAttach()` is called after `models/*` are required. The calls were made in the opposite order before this commit. --- docs.json | 5 +- docs/instructions.md | 33 +++ index.js | 213 +------------------- lib/compiler.js | 126 ++++++++++++ lib/executor.js | 167 ++++++++++++++++ test/boot.test.js | 442 ----------------------------------------- test/compiler.test.js | 220 ++++++++++++++++++++ test/executor.test.js | 239 ++++++++++++++++++++++ test/helpers/appdir.js | 49 +++++ 9 files changed, 849 insertions(+), 645 deletions(-) create mode 100644 docs/instructions.md create mode 100644 lib/compiler.js create mode 100644 lib/executor.js delete mode 100644 test/boot.test.js create mode 100644 test/compiler.test.js create mode 100644 test/executor.test.js create mode 100644 test/helpers/appdir.js diff --git a/docs.json b/docs.json index 83908f8..c87c562 100644 --- a/docs.json +++ b/docs.json @@ -5,6 +5,9 @@ "depth": 2 }, "index.js", - "docs/configuration.md" + "lib/compiler.js", + "lib/executor.js", + "docs/configuration.md", + "docs/instructions.md" ] } diff --git a/docs/instructions.md b/docs/instructions.md new file mode 100644 index 0000000..8acfc8c --- /dev/null +++ b/docs/instructions.md @@ -0,0 +1,33 @@ +## Two-step boot + +The methods `compile` and `execute` can be used to split the bootstrap +process into two steps, the first one run by a build script before calling +`browserify`, the second one run in the browser by the browserified app. + +The first method - `compile` - loads all configuration files, applies any +values specified in environmental variable and produces one JSON object +containing all instructions needed by `execute` to bootstrap the application. + +```js +{ + app: { + /* application config from app.json & friends */ + }, + models: { + /* model configuration from models.json */ + }, + dataSources: { + /* datasources configuration from datasources.json & friends*/ + }, + files: { + models: [ + '/project/models/customer.js', + /* ... */ + ], + boot: [ + '/project/boot/authentication.js', + /* ... */ + ] + } +} +``` diff --git a/index.js b/index.js index 64158b3..9c25987 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,6 @@ -var assert = require('assert'); -var fs = require('fs'); -var path = require('path'); -var _ = require('underscore'); -var loopback = require('loopback'); var ConfigLoader = require('./lib/config-loader'); -var debug = require('debug')('loopback:boot'); +var compile = require('./lib/compiler'); +var execute = require('./lib/executor'); /** * Initialize an application from an options object or @@ -55,209 +51,22 @@ var debug = require('debug')('loopback:boot'); * and `models/*.js`. Defaults to `appRootDir`. * @property {String} datasourcesRootDir Directory to use when loading * `datasources.json`. Defaults to `appRootDir`. + * @property {String} env Environment type, defaults to `process.env.NODE_ENV` + * or `development`. Common values are `development`, `staging` and + * `production`; however the applications are free to use any names. * @end * * @header bootLoopBackApp(app, [options]) */ exports = module.exports = function bootLoopBackApp(app, options) { - /*jshint camelcase:false */ - options = options || {}; + // backwards compatibility with loopback's app.boot + options.env = options.env || app.get('env'); - if(typeof options === 'string') { - options = { appRootDir: options }; - } - var appRootDir = options.appRootDir = options.appRootDir || process.cwd(); - var env = app.get('env'); - - var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env); - - var modelsRootDir = options.modelsRootDir || appRootDir; - var modelConfig = options.models || - ConfigLoader.loadModels(modelsRootDir, env); - - var dsRootDir = options.dsRootDir || appRootDir; - var dataSourceConfig = options.dataSources || - ConfigLoader.loadDataSources(dsRootDir, env); - - 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 = _.find([ - 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 - ], _.isFinite); - - appConfig.restApiRoot = - appConfig.restApiRoot || - app.get('restApiRoot') || - '/api'; - - 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]); - } - } - - // instantiate data sources - forEachKeyedObject(dataSourceConfig, function(key, obj) { - app.dataSource(key, obj); - }); - - // instantiate models - forEachKeyedObject(modelConfig, function(key, obj) { - app.model(key, obj); - }); - - // try to attach models to dataSources by type - try { - loopback.autoAttach(); - } catch(e) { - if(e.name === 'AssertionError') { - console.warn(e); - } else { - throw e; - } - } - - // 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; - } - - // require directories - requireDir(path.join(modelsRootDir, 'models'), app); - requireDir(path.join(appRootDir, 'boot'), app); + var instructions = compile(options); + execute(app, instructions); }; -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 requireDir(dir, app) { - assert(dir, 'cannot require directory contents without directory name'); - - var requires = {}; - - // 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 exports = tryRequire(filepath); - if (isFunctionNotModelCtor(exports)) - exports(app); - - var basename = path.basename(filename, ext); - requires[basename] = exports; - }); - - return requires; -} - -function tryRequire(modulePath) { - try { - return require.apply(this, arguments); - } catch(e) { - if(e.code === 'MODULE_NOT_FOUND') { - debug('Warning: cannot require %s - module not found.', modulePath); - return undefined; - } - console.error('failed to require "%s"', modulePath); - throw e; - } -} - -function tryReadDir() { - try { - return fs.readdirSync.apply(fs, arguments); - } catch(e) { - return []; - } -} - -function isFunctionNotModelCtor(fn) { - return typeof fn === 'function' && - !(fn.prototype instanceof loopback.Model); -} - exports.ConfigLoader = ConfigLoader; +exports.compile = compile; +exports.execute = execute; diff --git a/lib/compiler.js b/lib/compiler.js new file mode 100644 index 0000000..ac35dab --- /dev/null +++ b/lib/compiler.js @@ -0,0 +1,126 @@ +var assert = require('assert'); +var fs = require('fs'); +var path = require('path'); +var ConfigLoader = require('./config-loader'); +var debug = require('debug')('loopback:boot:compiler'); + +/** + * Gather all bootstrap-related configuration data and compile it into + * a single object containing instruction for `boot.execute`. + * + * @options {String|Object} options Boot options; If String, this is + * the application root directory; if object, has the properties + * described in `bootLoopBackApp` options above. + * @return {Object} + * + * @header boot.compile(options) + */ + +module.exports = function compile(options) { + options = options || {}; + + if(typeof options === 'string') { + options = { appRootDir: options }; + } + + var appRootDir = options.appRootDir = options.appRootDir || process.cwd(); + var env = options.env || process.env.NODE_ENV || 'development'; + + var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env); + assertIsValidConfig('app', appConfig); + + var modelsRootDir = options.modelsRootDir || appRootDir; + var modelsConfig = options.models || + ConfigLoader.loadModels(modelsRootDir, env); + assertIsValidConfig('model', modelsConfig); + + var dsRootDir = options.dsRootDir || appRootDir; + var dataSourcesConfig = options.dataSources || + ConfigLoader.loadDataSources(dsRootDir, env); + assertIsValidConfig('data source', dataSourcesConfig); + + // require directories + var modelsScripts = findScripts(path.join(modelsRootDir, 'models')); + var bootScripts = findScripts(path.join(appRootDir, 'boot')); + + return { + app: appConfig, + dataSources: dataSourcesConfig, + models: modelsConfig, + files: { + models: modelsScripts, + boot: bootScripts + } + }; +}; + +function assertIsValidConfig(name, config) { + if(config) { + assert(typeof config === 'object', + name + ' config must be a valid JSON object'); + } +} + +/** + * Find all javascript files (except for those prefixed with _) + * and all directories. + * @param {String} dir Full path of the directory to enumerate. + * @return {Array.} A list of absolute paths to pass to `require()`. + * @private + */ + +function findScripts(dir) { + assert(dir, 'cannot require directory contents without directory name'); + + 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; + } + }); + + var results = []; + 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()) { + if (ext in require.extensions) + results.push(filepath); + else + debug('Skipping file %s - unknown extension', filepath); + } else { + try { + path.join(require.resolve(filepath)); + } catch(err) { + debug('Skipping directory %s - %s', filepath, err.code || err); + } + } + }); + + return results; +} + +function tryReadDir() { + try { + return fs.readdirSync.apply(fs, arguments); + } catch(e) { + return []; + } +} diff --git a/lib/executor.js b/lib/executor.js new file mode 100644 index 0000000..538970e --- /dev/null +++ b/lib/executor.js @@ -0,0 +1,167 @@ +var assert = require('assert'); +var _ = require('underscore'); +var loopback = require('loopback'); +var debug = require('debug')('loopback:boot:executor'); + +/** + * Execute bootstrap instructions gathered by `boot.compile`. + * + * @options {Object} app The loopback app to boot. + * @options {Object} instructions Boot instructions. + * + * @header boot.execute(instructions) + */ + +module.exports = function execute(app, instructions) { + setHost(app, instructions); + setPort(app, instructions); + setApiRoot(app, instructions); + applyAppConfig(app, instructions); + + setupDataSources(app, instructions); + setupModels(app, instructions); + autoAttach(); + + runBootScripts(app, instructions); + + enableAnonymousSwagger(app, instructions); +}; + +function setHost(app, instructions) { + //jshint camelcase:false + var host = + process.env.npm_config_host || + process.env.OPENSHIFT_SLS_IP || + process.env.OPENSHIFT_NODEJS_IP || + process.env.HOST || + instructions.app.host || + process.env.npm_package_config_host || + app.get('host'); + + if(host !== undefined) { + assert(typeof host === 'string', 'app.host must be a string'); + app.set('host', host); + } +} + +function setPort(app, instructions) { + //jshint camelcase:false + var port = _.find([ + process.env.npm_config_port, + process.env.OPENSHIFT_SLS_PORT, + process.env.OPENSHIFT_NODEJS_PORT, + process.env.PORT, + instructions.app.port, + process.env.npm_package_config_port, + app.get('port'), + 3000 + ], _.isFinite); + + if(port !== undefined) { + var portType = typeof port; + assert(portType === 'string' || portType === 'number', + 'app.port must be a string or number'); + app.set('port', port); + } +} + +function setApiRoot(app, instructions) { + var restApiRoot = + instructions.app.restApiRoot || + app.get('restApiRoot') || + '/api'; + + assert(restApiRoot !== undefined, 'app.restBasePath is required'); + assert(typeof restApiRoot === 'string', + 'app.restApiRoot must be a string'); + assert(/^\//.test(restApiRoot), + 'app.restApiRoot must start with "/"'); + app.set('restApiRoot', restApiRoot); +} + +function applyAppConfig(app, instructions) { + var appConfig = instructions.app; + for(var configKey in appConfig) { + var cur = app.get(configKey); + if(cur === undefined || cur === null) { + app.set(configKey, appConfig[configKey]); + } + } +} + +function setupDataSources(app, instructions) { + forEachKeyedObject(instructions.dataSources, function(key, obj) { + app.dataSource(key, obj); + }); +} + +function setupModels(app, instructions) { + forEachKeyedObject(instructions.models, function(key, obj) { + app.model(key, obj); + }); + + runScripts(app, instructions.files.models); +} + +function forEachKeyedObject(obj, fn) { + if(typeof obj !== 'object') return; + + Object.keys(obj).forEach(function(key) { + fn(key, obj[key]); + }); +} + +function runScripts(app, list) { + if (!list || !list.length) return; + list.forEach(function(filepath) { + var exports = tryRequire(filepath); + if (isFunctionNotModelCtor(exports)) + exports(app); + }); +} + +function isFunctionNotModelCtor(fn) { + return typeof fn === 'function' && + !(fn.prototype instanceof loopback.Model); +} + +function tryRequire(modulePath) { + try { + return require.apply(this, arguments); + } catch(e) { + if(e.code === 'MODULE_NOT_FOUND') { + debug('Warning: cannot require %s - module not found.', modulePath); + return undefined; + } + console.error('failed to require "%s"', modulePath); + throw e; + } +} + +// Deprecated, will be removed soon +function autoAttach() { + try { + loopback.autoAttach(); + } catch(e) { + if(e.name === 'AssertionError') { + console.warn(e); + } else { + throw e; + } + } +} + +function runBootScripts(app, instructions) { + runScripts(app, instructions.files.boot); +} + +function enableAnonymousSwagger(app, instructions) { + // disable token requirement for swagger, if available + var swagger = app.remotes().exports.swagger; + if (!swagger) return; + + var appConfig = instructions.app; + var requireTokenForSwagger = appConfig.swagger && + appConfig.swagger.requireToken; + swagger.requireToken = requireTokenForSwagger || false; +} diff --git a/test/boot.test.js b/test/boot.test.js deleted file mode 100644 index 6ebdcc8..0000000 --- a/test/boot.test.js +++ /dev/null @@ -1,442 +0,0 @@ -var boot = require('../'); -var fs = require('fs-extra'); -var extend = require('util')._extend; -var path = require('path'); -var loopback = require('loopback'); -var assert = require('assert'); -var expect = require('must'); -var sandbox = require('./helpers/sandbox'); - -var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); - -var appDir; - -describe('bootLoopBackApp', function() { - beforeEach(sandbox.reset); - - beforeEach(function makeUniqueAppDir(done) { - // Node's module loader has a very aggressive caching, therefore - // we can't reuse the same path for multiple tests - // The code here is used to generate a random string - require('crypto').randomBytes(5, function(ex, buf) { - var randomStr = buf.toString('hex'); - appDir = sandbox.resolve(randomStr); - done(); - }); - }); - - describe('from options', function () { - var app; - beforeEach(function () { - app = loopback(); - boot(app, { - app: { - port: 3000, - host: '127.0.0.1', - restApiRoot: '/rest-api', - foo: {bar: 'bat'}, - baz: true - }, - models: { - 'foo-bar-bat-baz': { - options: { - plural: 'foo-bar-bat-bazzies' - }, - dataSource: 'the-db' - } - }, - dataSources: { - 'the-db': { - connector: 'memory', - defaultForType: 'db' - } - } - }); - }); - - it('should have port setting', function () { - assert.equal(app.get('port'), 3000); - }); - - it('should have host setting', function() { - assert.equal(app.get('host'), '127.0.0.1'); - }); - - it('should have restApiRoot setting', function() { - assert.equal(app.get('restApiRoot'), '/rest-api'); - }); - - it('should have other settings', function () { - expect(app.get('foo')).to.eql({ - bar: 'bat' - }); - expect(app.get('baz')).to.eql(true); - }); - - it('Instantiate models', function () { - assert(app.models); - assert(app.models.FooBarBatBaz); - assert(app.models.fooBarBatBaz); - assertValidDataSource(app.models.FooBarBatBaz.dataSource); - assert.isFunc(app.models.FooBarBatBaz, 'find'); - assert.isFunc(app.models.FooBarBatBaz, 'create'); - }); - - it('Attach models to data sources', function () { - assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb); - }); - - it('Instantiate data sources', function () { - assert(app.dataSources); - assert(app.dataSources.theDb); - assertValidDataSource(app.dataSources.theDb); - assert(app.dataSources.TheDb); - }); - - describe('boot and models directories', function() { - beforeEach(function() { - boot(app, SIMPLE_APP); - }); - - it('should run all modules in the boot directory', function () { - assert(process.loadedFooJS); - delete process.loadedFooJS; - }); - - it('should run all modules in the models directory', function () { - assert(process.loadedBarJS); - delete process.loadedBarJS; - }); - }); - - describe('PaaS and npm env variables', function() { - function bootWithDefaults() { - app = loopback(); - boot(app, { - app: { - port: undefined, - host: undefined - } - }); - } - - it('should be honored', function() { - function assertHonored(portKey, hostKey) { - process.env[hostKey] = randomPort(); - process.env[portKey] = randomHost(); - bootWithDefaults(); - assert.equal(app.get('port'), process.env[portKey]); - assert.equal(app.get('host'), process.env[hostKey]); - delete process.env[portKey]; - delete process.env[hostKey]; - } - - assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP'); - assertHonored('npm_config_port', 'npm_config_host'); - assertHonored('npm_package_config_port', 'npm_package_config_host'); - assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP'); - assertHonored('PORT', 'HOST'); - }); - - it('should be honored in order', function() { - /*jshint camelcase:false */ - process.env.npm_config_host = randomHost(); - process.env.OPENSHIFT_SLS_IP = randomHost(); - process.env.OPENSHIFT_NODEJS_IP = randomHost(); - process.env.HOST = randomHost(); - process.env.npm_package_config_host = randomHost(); - - bootWithDefaults(); - assert.equal(app.get('host'), process.env.npm_config_host); - - delete process.env.npm_config_host; - delete process.env.OPENSHIFT_SLS_IP; - delete process.env.OPENSHIFT_NODEJS_IP; - delete process.env.HOST; - delete process.env.npm_package_config_host; - - process.env.npm_config_port = randomPort(); - process.env.OPENSHIFT_SLS_PORT = randomPort(); - process.env.OPENSHIFT_NODEJS_PORT = randomPort(); - process.env.PORT = randomPort(); - process.env.npm_package_config_port = randomPort(); - - bootWithDefaults(); - assert.equal(app.get('host'), process.env.npm_config_host); - assert.equal(app.get('port'), process.env.npm_config_port); - - delete process.env.npm_config_port; - delete process.env.OPENSHIFT_SLS_PORT; - delete process.env.OPENSHIFT_NODEJS_PORT; - delete process.env.PORT; - delete process.env.npm_package_config_port; - }); - - function randomHost() { - return Math.random().toString().split('.')[1]; - } - - function randomPort() { - return Math.floor(Math.random() * 10000); - } - - it('should honor 0 for free port', function () { - boot(app, {app: {port: 0}}); - assert.equal(app.get('port'), 0); - }); - - it('should default to port 3000', function () { - boot(app, {app: {port: undefined}}); - assert.equal(app.get('port'), 3000); - }); - }); - }); - - describe('from directory', function () { - it('Load config files', function () { - var app = loopback(); - - boot(app, SIMPLE_APP); - - assert(app.models.foo); - assert(app.models.Foo); - assert(app.models.Foo.dataSource); - assert.isFunc(app.models.Foo, 'find'); - assert.isFunc(app.models.Foo, 'create'); - }); - - it('merges datasource configs from multiple files', function() { - givenAppInSandbox(); - - writeAppConfigFile('datasources.local.json', { - db: { local: 'applied' } - }); - - var env = process.env.NODE_ENV || 'development'; - writeAppConfigFile('datasources.' + env + '.json', { - db: { env: 'applied' } - }); - - var app = loopback(); - boot(app, appDir); - - var db = app.datasources.db.settings; - expect(db).to.have.property('local', 'applied'); - expect(db).to.have.property('env', 'applied'); - - var expectedLoadOrder = ['local', 'env']; - var actualLoadOrder = Object.keys(db).filter(function(k) { - return expectedLoadOrder.indexOf(k) !== -1; - }); - - expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); - }); - - it('supports .js for custom datasource config files', function() { - givenAppInSandbox(); - fs.writeFileSync( - path.resolve(appDir, 'datasources.local.js'), - 'module.exports = { db: { fromJs: true } };'); - - var app = loopback(); - boot(app, appDir); - - var db = app.datasources.db.settings; - expect(db).to.have.property('fromJs', true); - }); - - it('refuses to merge Object properties', function() { - givenAppInSandbox(); - writeAppConfigFile('datasources.local.json', { - db: { nested: { key: 'value' } } - }); - - var app = loopback(); - expect(function() { boot(app, appDir); }) - .to.throw(/`nested` is not a value type/); - }); - - it('refuses to merge Array properties', function() { - givenAppInSandbox(); - writeAppConfigFile('datasources.local.json', { - db: { nested: ['value'] } - }); - - var app = loopback(); - expect(function() { boot(app, appDir); }) - .to.throw(/`nested` is not a value type/); - }); - - it('merges app configs from multiple files', function() { - givenAppInSandbox(); - - writeAppConfigFile('app.local.json', { cfgLocal: 'applied' }); - - var env = process.env.NODE_ENV || 'development'; - writeAppConfigFile('app.' + env + '.json', { cfgEnv: 'applied' }); - - var app = loopback(); - boot(app, appDir); - - expect(app.settings).to.have.property('cfgLocal', 'applied'); - expect(app.settings).to.have.property('cfgEnv', 'applied'); - - var expectedLoadOrder = ['cfgLocal', 'cfgEnv']; - var actualLoadOrder = Object.keys(app.settings).filter(function(k) { - return expectedLoadOrder.indexOf(k) !== -1; - }); - - expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); - }); - - it('supports .js for custom app config files', function() { - givenAppInSandbox(); - fs.writeFileSync( - path.resolve(appDir, 'app.local.js'), - 'module.exports = { fromJs: true };'); - - var app = loopback(); - boot(app, appDir); - - expect(app.settings).to.have.property('fromJs', true); - }); - - it('supports `dsRootDir` option', function() { - givenAppInSandbox(); - - var customDir = path.resolve(appDir, 'custom'); - fs.mkdirsSync(customDir); - fs.renameSync( - path.resolve(appDir, 'datasources.json'), - path.resolve(customDir, 'datasources.json')); - - var app = loopback(); - - // workaround for https://github.com/strongloop/loopback/pull/283 - app.datasources = app.dataSources = {}; - - boot(app, { - appRootDir: appDir, - dsRootDir: path.resolve(appDir, 'custom') - }); - - expect(app.datasources).to.have.property('db'); - }); - - it('supports `modelsRootDir` option', function() { - givenAppInSandbox(); - - writeAppConfigFile('custom/models.json', { - foo: { dataSource: 'db' } - }); - - global.testData = {}; - writeAppFile('custom/models/foo.js', 'global.testData.foo = "loaded";'); - - var app = loopback(); - boot(app, { - appRootDir: appDir, - modelsRootDir: path.resolve(appDir, 'custom') - }); - - expect(app.models).to.have.property('foo'); - expect(global.testData).to.have.property('foo', 'loaded'); - }); - - it('calls function exported by models/model.js', function() { - givenAppInSandbox(); - writeAppFile('models/model.js', - 'module.exports = function(app) { app.fnCalled = true; };'); - - var app = loopback(); - delete app.fnCalled; - boot(app, appDir); - expect(app.fnCalled, 'exported fn was called').to.be.true(); - }); - - it('calls function exported by boot/init.js', function() { - givenAppInSandbox(); - writeAppFile('boot/init.js', - 'module.exports = function(app) { app.fnCalled = true; };'); - - var app = loopback(); - delete app.fnCalled; - boot(app, appDir); - expect(app.fnCalled, 'exported fn was called').to.be.true(); - }); - - it('does not call Model ctor exported by models/model.json', function() { - givenAppInSandbox(); - writeAppFile('models/model.js', - 'var loopback = require("loopback");\n' + - 'module.exports = loopback.Model.extend("foo");\n' + - 'module.exports.prototype._initProperties = function() {\n' + - ' global.fnCalled = true;\n' + - '};'); - - var app = loopback(); - delete global.fnCalled; - boot(app, appDir); - expect(global.fnCalled, 'exported fn was called').to.be.undefined(); - }); - - it('supports models/ subdirectires that are not require()able', function() { - givenAppInSandbox(); - writeAppFile('models/test/model.test.js', - 'throw new Error("should not been called");'); - - var app = loopback(); - boot(app, appDir); - - // no assert, the test passed when we got here - }); - }); -}); - - -function assertValidDataSource(dataSource) { - // has methods - assert.isFunc(dataSource, 'createModel'); - assert.isFunc(dataSource, 'discoverModelDefinitions'); - assert.isFunc(dataSource, 'discoverSchema'); - assert.isFunc(dataSource, 'enableRemote'); - assert.isFunc(dataSource, 'disableRemote'); - assert.isFunc(dataSource, 'defineOperation'); - assert.isFunc(dataSource, 'operations'); -} - -assert.isFunc = function (obj, name) { - assert(obj, 'cannot assert function ' + name + - ' on object that does not exist'); - assert(typeof obj[name] === 'function', name + ' is not a function'); -}; - -function givenAppInSandbox(appConfig, dataSources, models) { - fs.mkdirsSync(appDir); - - appConfig = extend({ - }, appConfig); - writeAppConfigFile('app.json', appConfig); - - dataSources = extend({ - db: { - connector: 'memory', - defaultForType: 'db' - } - }, dataSources); - writeAppConfigFile('datasources.json', dataSources); - - models = extend({ - }, models); - writeAppConfigFile('models.json', models); -} - -function writeAppConfigFile(name, json) { - writeAppFile(name, JSON.stringify(json, null, 2)); -} - -function writeAppFile(name, content) { - var filePath = path.resolve(appDir, name); - fs.mkdirsSync(path.dirname(filePath)); - fs.writeFileSync(filePath, content, 'utf-8'); -} diff --git a/test/compiler.test.js b/test/compiler.test.js new file mode 100644 index 0000000..b1076b0 --- /dev/null +++ b/test/compiler.test.js @@ -0,0 +1,220 @@ +var boot = require('../'); +var fs = require('fs-extra'); +var path = require('path'); +var assert = require('assert'); +var expect = require('must'); +var sandbox = require('./helpers/sandbox'); +var appdir = require('./helpers/appdir'); + +var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); + +describe('compiler', function() { + beforeEach(sandbox.reset); + beforeEach(appdir.init); + + describe('from options', function() { + var options, instructions, appConfig; + beforeEach(function() { + options = { + app: { + port: 3000, + host: '127.0.0.1', + restApiRoot: '/rest-api', + foo: {bar: 'bat'}, + baz: true + }, + models: { + 'foo-bar-bat-baz': { + options: { + plural: 'foo-bar-bat-bazzies' + }, + dataSource: 'the-db' + } + }, + dataSources: { + 'the-db': { + connector: 'memory', + defaultForType: 'db' + } + } + }; + instructions = boot.compile(options); + appConfig = instructions.app; + }); + + it('has port setting', function() { + expect(appConfig).to.have.property('port', 3000); + }); + + it('has host setting', function() { + expect(appConfig).to.have.property('host', '127.0.0.1'); + }); + + it('has restApiRoot setting', function() { + expect(appConfig).to.have.property('restApiRoot', '/rest-api'); + }); + + it('has other settings', function() { + expect(appConfig).to.have.property('baz', true); + expect(appConfig.foo, 'appConfig.foo').to.eql({ + bar: 'bat' + }); + }); + + it('has models definition', function() { + expect(instructions.models).to.eql(options.models); + }); + + it('has datasources definition', function() { + expect(instructions.dataSources).to.eql(options.dataSources); + }); + }); + + describe('from directory', function() { + it('loads config files', function() { + var instructions = boot.compile(SIMPLE_APP); + assert(instructions.models.foo); + assert(instructions.models.foo.dataSource); + }); + + it('merges datasource configs from multiple files', function() { + appdir.createConfigFilesSync(); + appdir.writeConfigFileSync('datasources.local.json', { + db: { local: 'applied' } + }); + + var env = process.env.NODE_ENV || 'development'; + appdir.writeConfigFileSync('datasources.' + env + '.json', { + db: { env: 'applied' } + }); + + var instructions = boot.compile(appdir.PATH); + + var db = instructions.dataSources.db; + expect(db).to.have.property('local', 'applied'); + expect(db).to.have.property('env', 'applied'); + + var expectedLoadOrder = ['local', 'env']; + var actualLoadOrder = Object.keys(db).filter(function(k) { + return expectedLoadOrder.indexOf(k) !== -1; + }); + + expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); + }); + + it('supports .js for custom datasource config files', function() { + appdir.createConfigFilesSync(); + appdir.writeFileSync('datasources.local.js', + 'module.exports = { db: { fromJs: true } };'); + + var instructions = boot.compile(appdir.PATH); + + var db = instructions.dataSources.db; + expect(db).to.have.property('fromJs', true); + }); + + it('refuses to merge Object properties', function() { + appdir.createConfigFilesSync(); + appdir.writeConfigFileSync('datasources.local.json', { + db: { nested: { key: 'value' } } + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/`nested` is not a value type/); + }); + + it('refuses to merge Array properties', function() { + appdir.createConfigFilesSync(); + appdir.writeConfigFileSync('datasources.local.json', { + db: { nested: ['value'] } + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/`nested` is not a value type/); + }); + + it('merges app configs from multiple files', function() { + appdir.createConfigFilesSync(); + + appdir.writeConfigFileSync('app.local.json', { cfgLocal: 'applied' }); + + var env = process.env.NODE_ENV || 'development'; + appdir.writeConfigFileSync('app.' + env + '.json', { cfgEnv: 'applied' }); + + var instructions = boot.compile(appdir.PATH); + var appConfig = instructions.app; + + expect(appConfig).to.have.property('cfgLocal', 'applied'); + expect(appConfig).to.have.property('cfgEnv', 'applied'); + + var expectedLoadOrder = ['cfgLocal', 'cfgEnv']; + var actualLoadOrder = Object.keys(appConfig).filter(function(k) { + return expectedLoadOrder.indexOf(k) !== -1; + }); + + expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); + }); + + it('supports .js for custom app config files', function() { + appdir.createConfigFilesSync(); + appdir.writeFileSync('app.local.js', + 'module.exports = { fromJs: true };'); + + var instructions = boot.compile(appdir.PATH); + var appConfig = instructions.app; + + expect(appConfig).to.have.property('fromJs', true); + }); + + it('supports `dsRootDir` option', function() { + appdir.createConfigFilesSync(); + + var customDir = path.resolve(appdir.PATH, 'custom'); + fs.mkdirsSync(customDir); + fs.renameSync( + path.resolve(appdir.PATH, 'datasources.json'), + path.resolve(customDir, 'datasources.json')); + + var instructions = boot.compile({ + appRootDir: appdir.PATH, + dsRootDir: path.resolve(appdir.PATH, 'custom') + }); + + expect(instructions.dataSources).to.have.property('db'); + }); + + it('supports `modelsRootDir` option', function() { + appdir.createConfigFilesSync(); + appdir.writeConfigFileSync('custom/models.json', { + foo: { dataSource: 'db' } + }); + + var fooJs = appdir.writeFileSync('custom/models/foo.js', ''); + + var instructions = boot.compile({ + appRootDir: appdir.PATH, + modelsRootDir: path.resolve(appdir.PATH, 'custom') + }); + + expect(instructions.models).to.have.property('foo'); + expect(instructions.files.models).to.eql([fooJs]); + }); + + it('includes boot/*.js scripts', function() { + appdir.createConfigFilesSync(); + var initJs = appdir.writeFileSync('boot/init.js', + 'module.exports = function(app) { app.fnCalled = true; };'); + var instructions = boot.compile(appdir.PATH); + expect(instructions.files.boot).to.eql([initJs]); + }); + + it('supports models/ subdirectires that are not require()able', function() { + appdir.createConfigFilesSync(); + appdir.writeFileSync('models/test/model.test.js', + 'throw new Error("should not been called");'); + var instructions = boot.compile(appdir.PATH); + + expect(instructions.files.models).to.eql([]); + }); + }); +}); diff --git a/test/executor.test.js b/test/executor.test.js new file mode 100644 index 0000000..1c64e95 --- /dev/null +++ b/test/executor.test.js @@ -0,0 +1,239 @@ +var boot = require('../'); +var path = require('path'); +var loopback = require('loopback'); +var assert = require('assert'); +var expect = require('must'); +var sandbox = require('./helpers/sandbox'); +var appdir = require('./helpers/appdir'); + +var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); + +var app; + + +describe('executor', function() { + beforeEach(sandbox.reset); + + beforeEach(appdir.init); + + beforeEach(function() { + app = loopback(); + }); + + var dummyInstructions = someInstructions({ + app: { + port: 3000, + host: '127.0.0.1', + restApiRoot: '/rest-api', + foo: { bar: 'bat' }, + baz: true + }, + models: { + 'foo-bar-bat-baz': { + options: { + plural: 'foo-bar-bat-bazzies' + }, + dataSource: 'the-db' + } + }, + dataSources: { + 'the-db': { + connector: 'memory', + defaultForType: 'db' + } + } + }); + + it('instantiates models', function() { + boot.execute(app, dummyInstructions); + assert(app.models); + assert(app.models.FooBarBatBaz); + assert(app.models.fooBarBatBaz); + assertValidDataSource(app.models.FooBarBatBaz.dataSource); + assert.isFunc(app.models.FooBarBatBaz, 'find'); + assert.isFunc(app.models.FooBarBatBaz, 'create'); + }); + + it('attaches models to data sources', function() { + boot.execute(app, dummyInstructions); + assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb); + }); + + it('instantiates data sources', function() { + boot.execute(app, dummyInstructions); + assert(app.dataSources); + assert(app.dataSources.theDb); + assertValidDataSource(app.dataSources.theDb); + assert(app.dataSources.TheDb); + }); + + describe('with boot and models files', function() { + beforeEach(function() { + boot.execute(app, simpleAppInstructions()); + }); + + it('should run `boot/*` files', function() { + assert(process.loadedFooJS); + delete process.loadedFooJS; + }); + + it('should run `models/*` files', function() { + assert(process.loadedBarJS); + delete process.loadedBarJS; + }); + }); + + describe('with PaaS and npm env variables', function() { + function bootWithDefaults() { + app = loopback(); + boot.execute(app, someInstructions({ + app: { + port: undefined, + host: undefined + } + })); + } + + it('should honor host and port', function() { + function assertHonored(portKey, hostKey) { + process.env[hostKey] = randomPort(); + process.env[portKey] = randomHost(); + bootWithDefaults(); + assert.equal(app.get('port'), process.env[portKey], portKey); + assert.equal(app.get('host'), process.env[hostKey], hostKey); + delete process.env[portKey]; + delete process.env[hostKey]; + } + + assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP'); + assertHonored('npm_config_port', 'npm_config_host'); + assertHonored('npm_package_config_port', 'npm_package_config_host'); + assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP'); + assertHonored('PORT', 'HOST'); + }); + + it('should prioritize sources', function() { + /*jshint camelcase:false */ + process.env.npm_config_host = randomHost(); + process.env.OPENSHIFT_SLS_IP = randomHost(); + process.env.OPENSHIFT_NODEJS_IP = randomHost(); + process.env.HOST = randomHost(); + process.env.npm_package_config_host = randomHost(); + + bootWithDefaults(); + assert.equal(app.get('host'), process.env.npm_config_host); + + delete process.env.npm_config_host; + delete process.env.OPENSHIFT_SLS_IP; + delete process.env.OPENSHIFT_NODEJS_IP; + delete process.env.HOST; + delete process.env.npm_package_config_host; + + process.env.npm_config_port = randomPort(); + process.env.OPENSHIFT_SLS_PORT = randomPort(); + process.env.OPENSHIFT_NODEJS_PORT = randomPort(); + process.env.PORT = randomPort(); + process.env.npm_package_config_port = randomPort(); + + bootWithDefaults(); + assert.equal(app.get('host'), process.env.npm_config_host); + assert.equal(app.get('port'), process.env.npm_config_port); + + delete process.env.npm_config_port; + delete process.env.OPENSHIFT_SLS_PORT; + delete process.env.OPENSHIFT_NODEJS_PORT; + delete process.env.PORT; + delete process.env.npm_package_config_port; + }); + + function randomHost() { + return Math.random().toString().split('.')[1]; + } + + function randomPort() { + return Math.floor(Math.random() * 10000); + } + + it('should honor 0 for free port', function() { + boot.execute(app, someInstructions({ app: { port: 0 } })); + assert.equal(app.get('port'), 0); + }); + + it('should default to port 3000', function() { + boot.execute(app, someInstructions({ app: { port: undefined } })); + assert.equal(app.get('port'), 3000); + }); + }); + + it('calls function exported by models/model.js', function() { + var file = appdir.writeFileSync('models/model.js', + 'module.exports = function(app) { app.fnCalled = true; };'); + + delete app.fnCalled; + boot.execute(app, someInstructions({ files: { models: [ file ] } })); + expect(app.fnCalled, 'exported fn was called').to.be.true(); + }); + + it('calls function exported by boot/init.js', function() { + var file = appdir.writeFileSync('boot/init.js', + 'module.exports = function(app) { app.fnCalled = true; };'); + + delete app.fnCalled; + boot.execute(app, someInstructions({ files: { boot: [ file ] } })); + expect(app.fnCalled, 'exported fn was called').to.be.true(); + }); + + it('does not call Model ctor exported by models/model.json', function() { + var file = appdir.writeFileSync('models/model.js', + 'var loopback = require("loopback");\n' + + 'module.exports = loopback.Model.extend("foo");\n' + + 'module.exports.prototype._initProperties = function() {\n' + + ' global.fnCalled = true;\n' + + '};'); + + delete global.fnCalled; + boot.execute(app, someInstructions({ files: { models: [ file ] } })); + expect(global.fnCalled, 'exported fn was called').to.be.undefined(); + }); +}); + + +function assertValidDataSource(dataSource) { + // has methods + assert.isFunc(dataSource, 'createModel'); + assert.isFunc(dataSource, 'discoverModelDefinitions'); + assert.isFunc(dataSource, 'discoverSchema'); + assert.isFunc(dataSource, 'enableRemote'); + assert.isFunc(dataSource, 'disableRemote'); + assert.isFunc(dataSource, 'defineOperation'); + assert.isFunc(dataSource, 'operations'); +} + +assert.isFunc = function (obj, name) { + assert(obj, 'cannot assert function ' + name + + ' on object that does not exist'); + assert(typeof obj[name] === 'function', name + ' is not a function'); +}; + +function someInstructions(values) { + var result = { + app: values.app || {}, + models: values.models || {}, + dataSources: values.dataSources || {}, + files: { + models: [], + boot: [] + } + }; + + if (values.files) { + for (var k in values.files) + result.files[k] = values.files[k]; + } + + return result; +} + +function simpleAppInstructions() { + return boot.compile(SIMPLE_APP); +} diff --git a/test/helpers/appdir.js b/test/helpers/appdir.js new file mode 100644 index 0000000..4db2e10 --- /dev/null +++ b/test/helpers/appdir.js @@ -0,0 +1,49 @@ +var path = require('path'); +var fs = require('fs-extra'); +var extend = require('util')._extend; +var sandbox = require('./sandbox'); + +var appdir = exports; + +var PATH = appdir.PATH = null; + +appdir.init = function(cb) { + // Node's module loader has a very aggressive caching, therefore + // we can't reuse the same path for multiple tests + // The code here is used to generate a random string + require('crypto').randomBytes(5, function(err, buf) { + if (err) return cb(err); + var randomStr = buf.toString('hex'); + PATH = appdir.PATH = sandbox.resolve(randomStr); + cb(null, appdir.PATH); + }); +}; + +appdir.createConfigFilesSync = function(appConfig, dataSources, models) { + appConfig = extend({ + }, appConfig); + appdir.writeConfigFileSync ('app.json', appConfig); + + dataSources = extend({ + db: { + connector: 'memory', + defaultForType: 'db' + } + }, dataSources); + appdir.writeConfigFileSync ('datasources.json', dataSources); + + models = extend({ + }, models); + appdir.writeConfigFileSync ('models.json', models); +}; + +appdir.writeConfigFileSync = function(name, json) { + return appdir.writeFileSync(name, JSON.stringify(json, null, 2)); +}; + +appdir.writeFileSync = function(name, content) { + var filePath = path.resolve(PATH, name); + fs.mkdirsSync(path.dirname(filePath)); + fs.writeFileSync(filePath, content, 'utf-8'); + return filePath; +};