From 7d6bbc5378f6ad6d87eb2933269f75707a0a5b59 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Wed, 9 Nov 2016 15:44:27 -0500 Subject: [PATCH 01/52] [ci] test node 7 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ba5724f..ca9e39a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ node_js: - 4 - 5 - 6 + - 7 compiler: - gcc env: From 4b0bdec9041dbeaa22f80c11b6f35f6479a31554 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 14 Nov 2016 16:05:18 -0500 Subject: [PATCH 02/52] [refactor] merged trailpack-core into trails - tests pass - config object is now copied during merge - TODO migrate motd and validation schemas --- index.js | 77 +++++++++++++++++------ lib/core.js | 98 ++++++++++++++++++++++++++++++ lib/index.js | 1 + lib/trailpack.js | 8 --- lib/trails.js | 52 +++++++--------- package.json | 14 ++++- test/index.js | 6 +- test/integration/app.js | 52 ++++++++++++++++ test/integration/i18n.test.js | 34 +++++++++++ test/integration/index.js | 0 test/integration/trailpack.test.js | 28 +++++++++ test/lib/context.test.js | 45 ++++++++++++++ test/lib/trailpack.test.js | 19 ------ test/lib/trails.test.js | 18 +++--- 14 files changed, 362 insertions(+), 90 deletions(-) create mode 100644 lib/core.js create mode 100644 test/integration/app.js create mode 100644 test/integration/i18n.test.js create mode 100644 test/integration/index.js create mode 100644 test/integration/trailpack.test.js create mode 100644 test/lib/context.test.js diff --git a/index.js b/index.js index 4284d7c..2dc4274 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const events = require('events') const lib = require('./lib') +const i18next = require('i18next') /** * The Trails Application. Merges the configuration and API resources @@ -12,6 +13,8 @@ module.exports = class TrailsApp extends events.EventEmitter { /** * @param pkg The application package.json + * @param app.api The application api (api/ folder) + * @param app.config The application configuration (config/ folder) * * Initialize the Trails Application and its EventEmitter parentclass. Set * some necessary default configuration. @@ -19,19 +22,24 @@ module.exports = class TrailsApp extends events.EventEmitter { constructor (app) { super() - if (!process.env.NODE_ENV) { - process.env.NODE_ENV = 'development' - } if (!app.pkg) { throw new lib.Errors.PackageNotDefinedError() } + if (!app.api && !(app && app.api)) { + throw new lib.Errors.ApiNotDefinedError() + } + + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development' + } + const processEnv = Object.freeze(JSON.parse(JSON.stringify(process.env))) lib.Trails.validateConfig(app.config) Object.defineProperties(this, { env: { enumerable: false, - value: Object.freeze(JSON.parse(JSON.stringify(process.env))) + value: processEnv }, pkg: { enumerable: false, @@ -44,7 +52,7 @@ module.exports = class TrailsApp extends events.EventEmitter { value: process.versions }, config: { - value: lib.Trails.buildConfig(app.config), + value: lib.Trails.buildConfig(app.config, processEnv), configurable: true }, api: { @@ -87,34 +95,63 @@ module.exports = class TrailsApp extends events.EventEmitter { enumerable: false, writable: true, value: { } + }, + models: { + enumerable: true, + writable: false, + value: { } + }, + services: { + enumerable: true, + writable: false, + value: { } + }, + controllers: { + enumerable: true, + writable: false, + value: { } + }, + policies: { + enumerable: true, + writable: false, + value: { } + }, + translate: { + enumerable: false, + writable: true } }) + lib.Core.createDefaultPaths(this) this.setMaxListeners(this.config.main.maxListeners) + + Object.assign(this.models, lib.Core.bindMethods(this, 'models')) + Object.assign(this.services, lib.Core.bindMethods(this, 'services')) + Object.assign(this.controllers, lib.Core.bindMethods(this, 'controllers')) + Object.assign(this.policies, lib.Core.bindMethods(this, 'policies')) + this.config.main.packs.forEach(Pack => new Pack(this)) - delete this.config.env // Delete env config, now it has been merge + this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) + + delete this.config.env // Delete env config, now it has been merged } /** - * Start the App. Load all Trailpacks. The "api" property is required, here, - * if not provided to the constructor. + * Start the App. Load all Trailpacks. * - * @param app.api The application api (api/ folder) - * @param app.config The application configuration (config/ folder) * @return Promise */ - start (app) { - if (!this.api && !(app && app.api)) { - throw new lib.Errors.ApiNotDefinedError() - } - this.api || (this.api = app && app.api) - - this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) + start () { lib.Trails.bindEvents(this) lib.Trailpack.bindTrailpackPhaseListeners(this, this.loadedPacks) lib.Trailpack.bindTrailpackMethodListeners(this, this.loadedPacks) - this.emit('trails:start') + i18next.init(this.config.i18n, (err, t) => { + if (err) throw err + + this.translate = t + this.emit('trails:start') + }) return this.after('trails:ready') .then(() => { @@ -208,4 +245,8 @@ module.exports = class TrailsApp extends events.EventEmitter { get log () { return this.config.log.logger } + + get __ () { + return this.translate + } } diff --git a/lib/core.js b/lib/core.js new file mode 100644 index 0000000..12efb6c --- /dev/null +++ b/lib/core.js @@ -0,0 +1,98 @@ +'use strict' + +const fs = require('fs') +const isPlainObject = require('lodash.isplainobject') +const mapValues = require('lodash.mapvalues') +const forEach = require('lodash.foreach') +const includes = require('lodash.includes') +const isFunction = require('lodash.isFunction') + +const Core = module.exports = { + + reservedMethods: [ + 'app', + 'api', + 'log', + '__', + 'constructor', + 'undefined', + 'methods', + 'config', + 'schema' + ], + + /** + * Bind the context of API resource methods. + */ + bindMethods (app, resource) { + return mapValues(app.api[resource], (Resource, resourceName) => { + if (isPlainObject(Resource)) { + throw new Error(`${resourceName} should be a class. It is a regular object`) + } + + const obj = new Resource(app) + + obj.methods = Core.getClassMethods(obj) + forEach(obj.methods, method => { + obj[method] = obj[method].bind(obj) + }) + return obj + }) + }, + + /** + * Traverse protoype chain and aggregate all class method names + */ + getClassMethods (obj) { + const props = [ ] + const objectRoot = new Object() + + while (!obj.isPrototypeOf(objectRoot)) { + Object.getOwnPropertyNames(obj).forEach(prop => { + if (props.indexOf(prop) === -1 && + !includes(Core.reservedMethods, prop) && + isFunction(obj[prop])) { + + props.push(prop) + } + }) + obj = Object.getPrototypeOf(obj) + } + + return props + }, + + /** + * create paths if they don't exist + */ + createDefaultPaths (app) { + const paths = app.config.main.paths + + return Promise.all(Object.keys(paths).map(pathName => { + const dir = paths[pathName] + if (Array.isArray(dir)) { + dir.map(item => { + pathCreate(item.path, pathName) + }) + } + else { + pathCreate(dir, pathName) + } + })) + function pathCreate(dir, pathName) { + try { + const stats = fs.statSync(dir) + + if (!stats.isDirectory()) { + app.log.error('The path "', pathName, '" is not a directory.') + app.log.error('config.main.paths should only contain paths to directories') + return Promise.reject() + } + } + catch (e) { + fs.mkdirSync(dir) + } + } + } + +} diff --git a/lib/index.js b/lib/index.js index e3e97e2..1a72bbe 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,3 +5,4 @@ exports.Trailpack = require('./trailpack') exports.Trails = require('./trails') exports.Errors = require('./errors') exports.Pathfinder = require('./pathfinder') +exports.Core = require('./core') diff --git a/lib/trailpack.js b/lib/trailpack.js index 0dc08d7..2d5e47f 100644 --- a/lib/trailpack.js +++ b/lib/trailpack.js @@ -12,14 +12,6 @@ module.exports = { }, { }) }, - /** - * Return all non-system trailpacks. As of v1.0, the only system trailpack is - * "trailpack-core" - */ - getUserlandTrailpacks (packs) { - return packs.filter(pack => pack.name !== 'core') - }, - /** * Bind lifecycle boundary event listeners. That is, when all trailpacks have * completed a particular phase, e.g. "configure" or "initialize", emit an diff --git a/lib/trails.js b/lib/trails.js index f0e278b..fd132d7 100644 --- a/lib/trails.js +++ b/lib/trails.js @@ -3,47 +3,29 @@ const path = require('path') const lib = require('.') +//const defaultsDeep = require('lodash.defaultsdeep') +const merge = require('lodash.merge') module.exports = { - /** - * Detect if the item is a Object. - */ - isObject (item) { - return (item && typeof item === 'object' && - !Array.isArray(item) && item !== null) - }, - - /** - * Deep Merge Objects - */ - deepMerge (target, source) { - if (this.isObject(target) && this.isObject(source)) { - Object.keys(source).forEach(key => { - if (this.isObject(source[key])) { - if (!target[key]) Object.assign(target, { [key]: {} }) - this.deepMerge(target[key], source[key]) - } - else { - Object.assign(target, { [key]: source[key] }) - } - }) - } - return target - }, - /** * Copy and merge the provided configuration into a new object, decorated with * necessary default and environment-specific values. */ - buildConfig (config) { + buildConfig (initialConfig, nodeEnv) { + const root = path.resolve(path.dirname(require.main.filename)) + const temp = path.resolve(root, '.tmp') + const envConfig = initialConfig.env && initialConfig.env[nodeEnv] + const configTemplate = { main: { maxListeners: 128, - packs: (config.main || { }).packs || [ ], + packs: [ ], paths: { - root: path.resolve(path.dirname(require.main.filename)), - temp: path.resolve(path.dirname(require.main.filename), '.tmp') + root: root, + temp: temp, + sockets: path.resolve(temp, 'sockets'), + logs: path.resolve(temp, 'log') }, timeouts: { start: 10000, @@ -53,7 +35,11 @@ module.exports = { log: { } } + return merge(configTemplate, initialConfig, (envConfig || { })) + // merge config.env + // TODO refactor to lodash merge + /* config = this.deepMerge( (config || { }), ((config.env && config.env[process.env.NODE_ENV]) || { }) @@ -74,6 +60,7 @@ module.exports = { // merge remaining environment-specific config properties return Object.assign({ }, config, configTemplate) + */ }, /** @@ -120,7 +107,10 @@ module.exports = { /** * Deep freeze application config object. Exceptions are made for required * modules that are listed as dependencies in the application's - * package definition (package.json). + * package definition (package.json). Trails takes a firm stance that + * configuration should be modified only by the developer, the environment, + * and the trailpack configuration phase -- never by the application itself + * during runtime. * * @param config the configuration object to be frozen. * @param [pkg] the package definition to use for exceptions. optional. diff --git a/package.json b/package.json index 83b5fd6..5393f9b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "ci": "cd .. && ci" }, "pre-commit": [ - "test" + "test" ], "repository": { "type": "git", @@ -64,7 +64,17 @@ "url": "https://github.com/trailsjs/trails/issues" }, "homepage": "http://trailsjs.io", - "dependencies": {}, + "dependencies": { + "i18next": "^3.4.1", + "joi": "^9.2.0", + "lodash.defaultsdeep": "^4.6.0", + "lodash.foreach": "^4.5.0", + "lodash.includes": "^4.3.0", + "lodash.isfunction": "^3.0.8", + "lodash.isplainobject": "^4.0.6", + "lodash.mapvalues": "^4.6.0", + "lodash.merge": "^4.6.0" + }, "devDependencies": { "eslint": "^2.5.3", "eslint-config-trails": "^1.0", diff --git a/test/index.js b/test/index.js index 8f8c379..8ccb10e 100644 --- a/test/index.js +++ b/test/index.js @@ -6,6 +6,9 @@ const TrailsApp = require('..') const testAppDefinition = require('./testapp') const lib = require('../lib') +global.app = new TrailsApp(require('./integration/app')) +global.app.start() + describe('Trails', () => { describe('@TrailsApp', () => { describe('idempotence', () => { @@ -102,8 +105,7 @@ describe('Trails', () => { } } } - const app = new TrailsApp(def) - assert.throws(() => app.start(), lib.Errors.ApiNotDefinedError) + assert.throws(() => new TrailsApp(def), lib.Errors.ApiNotDefinedError) }) }) describe('@PackageNotDefinedError', () => { diff --git a/test/integration/app.js b/test/integration/app.js new file mode 100644 index 0000000..93ffee4 --- /dev/null +++ b/test/integration/app.js @@ -0,0 +1,52 @@ +const path = require('path') +const _ = require('lodash') +const smokesignals = require('smokesignals') + +const AppConfigLocales = { + en: { + helloworld: 'hello world', + hello: { + user: 'hello {{username}}' + } + }, + de: { + helloworld: 'hallo Welt', + hello: { + user: 'hallo {{username}}' + } + } +} + +const App = { + pkg: { + name: 'core-trailpack-test' + }, + api: { + customkey: {} + }, + config: { + main: { + packs: [ + smokesignals.Trailpack + ], + paths: { + testdir: path.resolve(__dirname, 'testdir') + } + }, + i18n: { + lng: 'en', + resources: { + en: { + translation: AppConfigLocales.en + }, + de: { + translation: AppConfigLocales.de + } + } + } + }, + locales: AppConfigLocales +} + +_.defaultsDeep(App, smokesignals.FailsafeConfig) +module.exports = App diff --git a/test/integration/i18n.test.js b/test/integration/i18n.test.js new file mode 100644 index 0000000..12f054b --- /dev/null +++ b/test/integration/i18n.test.js @@ -0,0 +1,34 @@ +const assert = require('assert') +const _ = require('lodash') + +describe('i18n', () => { + it('should expose the __ method on the app object', () => { + assert(global.app.__) + assert(_.isFunction(global.app.__)) + }) + describe('#__', () => { + describe('EN (default)', () => { + it('should render EN strings by default', () => { + assert.equal(global.app.__('helloworld'), 'hello world') + }) + it('should render nested string', () => { + assert.equal( + global.app.__('hello.user', { username: 'trails' }), + 'hello trails' + ) + }) + }) + describe('DE', () => { + it('should render DE string when specified', () => { + assert.equal(global.app.__('helloworld', { lng: 'de' }), 'hallo Welt') + }) + it('should render nested string', () => { + assert.equal( + global.app.__('hello.user', { lng: 'de', username: 'trails' }), + 'hallo trails' + ) + }) + }) + }) +}) + diff --git a/test/integration/index.js b/test/integration/index.js new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/trailpack.test.js b/test/integration/trailpack.test.js new file mode 100644 index 0000000..fddcd23 --- /dev/null +++ b/test/integration/trailpack.test.js @@ -0,0 +1,28 @@ +'use strict' + +const path = require('path') +const fs = require('fs') +const assert = require('assert') + +describe('Core Trailpack', () => { + describe('#validate', () => { + it('should validate an api object with arbitrary keys', () => { + assert(global.app.api.customkey) + }) + }) + describe('#configure', () => { + it('should create missing directories for configured paths', () => { + assert(fs.statSync(path.resolve(__dirname, 'testdir'))) + }) + it('should set paths.temp if not configured explicitly by user', () => { + assert(global.app.config.main.paths.temp) + }) + it('should set paths.logs if not configured explicitly by user', () => { + assert(global.app.config.main.paths.logs) + }) + it('should set paths.sockets if not configured explicitly by user', () => { + assert(global.app.config.main.paths.sockets) + }) + }) +}) + diff --git a/test/lib/context.test.js b/test/lib/context.test.js new file mode 100644 index 0000000..80fe6db --- /dev/null +++ b/test/lib/context.test.js @@ -0,0 +1,45 @@ +'use strict' + +const _ = require('lodash') +const assert = require('assert') +const lib = require('../../lib') + +describe('lib.Core', () => { + describe('#getClassMethods', () => { + const A = class A { + foo () { } + } + const B = class B extends A { + bar () { } + } + const C = class B extends A { + bar () { } + baz () { } + get getter () { + return 'getter' + } + static staticThing () { } + } + it('should return class methods for object', () => { + const methods = lib.Core.getClassMethods(new A(), A) + + assert.equal(methods.length, 1) + assert.equal(methods[0], 'foo') + }) + it('should return class methods for all objects in prototype chain', () => { + const methods = lib.Core.getClassMethods(new B(), B) + + assert.equal(methods.length, 2) + assert(_.includes(methods, 'foo')) + assert(_.includes(methods, 'bar')) + }) + it('should return only *instance methods* and no other type of thing', () => { + const methods = lib.Core.getClassMethods(new C(), C) + + assert.equal(methods.length, 3) + assert(_.includes(methods, 'bar')) + assert(_.includes(methods, 'foo')) + assert(_.includes(methods, 'baz')) + }) + }) +}) diff --git a/test/lib/trailpack.test.js b/test/lib/trailpack.test.js index 56f527c..2006464 100644 --- a/test/lib/trailpack.test.js +++ b/test/lib/trailpack.test.js @@ -13,25 +13,6 @@ describe('lib.Trailpack', () => { return app.start(testAppDefinition) }) - describe('#getUserlandTrailpacks', () => { - let testTrailpacks - before(() => { - testTrailpacks = [ - new Trailpack(app, { pkg: { name: 'trailpack-pack1' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack2' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack3' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack4' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-core' }, config: { } }) - ] - }) - - it('should exclude the core trailpack', () => { - const packs = lib.Trailpack.getUserlandTrailpacks(testTrailpacks) - - assert(packs) - assert.equal(packs.length, 4) - }) - }) describe('#getTrailpackMapping', () => { let testTrailpacks before(() => { diff --git a/test/lib/trails.test.js b/test/lib/trails.test.js index 7c6cc8e..0d4e786 100644 --- a/test/lib/trails.test.js +++ b/test/lib/trails.test.js @@ -85,8 +85,7 @@ describe('lib.Trails', () => { }) it('should merge basic env config', () => { - process.env.NODE_ENV = 'envTest1' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'envTest1') assert(config) assert.equal(config.log.merged, 'yes') @@ -99,7 +98,7 @@ describe('lib.Trails', () => { it('should merge nested env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'envTest2') assert(config) assert.equal(config.log.merged, 'yes') @@ -116,7 +115,7 @@ describe('lib.Trails', () => { it('should merge deeply nested env config', () => { process.env.NODE_ENV = 'envTest3' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'envTest3') assert(config) assert.equal(config.log.merged, 'yes') @@ -136,7 +135,7 @@ describe('lib.Trails', () => { it('should merge full custom env config', () => { process.env.NODE_ENV = 'envTest1' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'envTest1') assert(config) assert(typeof config.customObject === 'object') @@ -150,7 +149,7 @@ describe('lib.Trails', () => { it('should merge partial custom env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'envTest2') assert(config) assert(typeof config.customObject === 'object') @@ -164,7 +163,7 @@ describe('lib.Trails', () => { it('should merge new custom attr in env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'envTest2') assert(config) assert(typeof config.customObject === 'object') @@ -179,12 +178,11 @@ describe('lib.Trails', () => { it('should not override any configs if NODE_ENV matches no env', () => { process.env.NODE_ENV = 'notconfigured' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'notconfigured') assert(config) assert.equal(config.log.merged, 'no') assert.equal(config.log.normal, 'yes') - assert.equal(config.customObject, testConfig.customObject) assert(!config.log.extraneous) assert(config.env) @@ -193,7 +191,7 @@ describe('lib.Trails', () => { it('should keep "env" property from config', () => { process.env.NODE_ENV = 'mergetest2' - const config = lib.Trails.buildConfig(testConfig) + const config = lib.Trails.buildConfig(testConfig, 'mergetest2') assert(config.env) }) }) From c2054db065c47e24eafbcb51049b7480ba10dab1 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 15 Nov 2016 00:38:28 -0500 Subject: [PATCH 03/52] [refactor] added motd config --- index.js | 4 +-- lib/motd.js | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/trails.js | 53 ++++++++++++-------------------- package.json | 1 + 4 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 lib/motd.js diff --git a/index.js b/index.js index 2dc4274..57d09ae 100644 --- a/index.js +++ b/index.js @@ -142,7 +142,7 @@ module.exports = class TrailsApp extends events.EventEmitter { * @return Promise */ start () { - lib.Trails.bindEvents(this) + lib.Trails.bindListeners(this) lib.Trailpack.bindTrailpackPhaseListeners(this, this.loadedPacks) lib.Trailpack.bindTrailpackMethodListeners(this, this.loadedPacks) @@ -175,7 +175,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } this.emit('trails:stop') - lib.Trails.unbindEvents(this) + lib.Trails.unbindListeners(this) return Promise.all( this.loadedPacks.map(pack => { diff --git a/lib/motd.js b/lib/motd.js new file mode 100644 index 0000000..1bad058 --- /dev/null +++ b/lib/motd.js @@ -0,0 +1,85 @@ +/*eslint max-len: 0 */ +const _ = require('lodash') + +module.exports = { + hr: '---------------------------------------------------------------', + + info: { + start: 'Starting...', + stop: 'Shutting down...', + initialized: 'All trailpacks are loaded.', + ready (app) { + const baseUrl = _.get(app.config, 'web.baseUrl') || + `http://${_.get(app.config, 'web.host') || 'localhost'}:${_.get(app.config, 'web.port') || '80'}` + return ( + `--------------------------------------------------------------- + ${new Date()} + Basic Info + Application : ${app.pkg.name} + Application root : ${baseUrl} + Version : ${app.pkg.version} + Environment : ${process.env.NODE_ENV}` + ) + } + }, + + debug: { + ready (app) { + return ( + ` Database Info + ORM : ${_.get(app.config, 'database.orm') || 'NOT INSTALLED'} + Stores : ${_.get(app.config, 'database.orm') ? Object.keys(app.config.database.stores) : 'N/A'} + Web Server Info + Server : ${_.get(app.config, 'web.server') || 'NOT INSTALLED'} + View Engine : ${_.get(app.config, 'views.engine') || 'NOT INSTALLED'} + Port : ${_.get(app.config, 'web.port') || 'N/A'} + Routes : ${(app.routes || [ ]).length}` + ) + } + }, + + silly: { + stop: ` + Happy trails to you, until we meet again. + - Dale Evans + `, + + ready (app) { + return ( + ` API + Models : ${_.keys(app.api.models)} + Controllers : ${_.keys(app.api.controllers)} + Policies : ${_.keys(app.api.policies)} + Trailpacks : ${_.map(app.packs, pack => pack.name)}` + ) + }, + + initialized: ` + ..@@@@.. .@. @@ + .@@@@@@@@@@@@@@@@. @@@ @@ + .@@@@' '@@@@. @@@ @@ + @@@@ @@@@ @@@ .. @@ + .@@; '@@. @@@ .@@@@. @@ + @@@ .@@@. .@@@@. @@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@' '@@ @@ + @@' .@@@@@@@. .@@@@@@@@ '@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@. .@@ @@ + @@' @@@@@@@@@@@ :@@@@@@@@@@ '@@ @@@ '@@@@' @@ + @@@ :@@@@@@@@@@@. @@@@@@@@@@@@ @@@ @@@ '' @@ + @@ @@@@@'@@@@@@@ :@@@@'@@@@@@@ @@ @@@ @@ .@@@@@@@@@@@. +.@@ @@@@@ @@@@@@@ :@@@@ @@@@@@@ @@: @@@ @@ .@@@@@@@ .@@@@@@@ @@ @@ .@@@@@@@@@@@@@@' +@@@ @@@' @@@@@@. @@@@ @@@@@@@ @@+ @@@ @@ .@@@@: .@@@@' @@ @@ @@@' +@@@ @@ @@@@@ '@' @@@@@' @@: @@@ @@@@@' @@@' @@ @@ .@@ +'@@ @@ @@ @@ @@@ @@@@ @@@ @@ @@ @@@ + @@. @@ @@ .@@ @@@ @@@ @@@ @@ @@ .@@. + @@. '@@@. @@ .@@ @@@ @@@ @@' .. @@ .@@@@...... + '@@ '@@@@@@@@@@.@@ .@@' @@@ @@' @@. .@@ @@ .@@@@@@@@@@@. + '@@. @@@@ .@@' @@@ @@' @@@ +@@ @@ @@ '@@@. + @@@. '@@ @@@ @@@ @@' '@@. .@@@ @@ @@ @@@ + '@@@. @@ .@@@' @@@ @@' '@@. .@@@@ @@ @@ @@@ + .@@@@. @@@@@@' @@@ @@' '@@@. .@@@ @@ @@ @@ @@@ + .@@@@@@@..... @@@. @@@ @@' '@@@@@.....@@@@@' @@ @@ @@ .@@@@@@@@@@@@@@@ + '@@@@@@@@ @@@ @@. '@@@@@@@@@' @@ @@ @@ '@@@@@@@@@@@@' + + ` + } +} + diff --git a/lib/trails.js b/lib/trails.js index fd132d7..0d10624 100644 --- a/lib/trails.js +++ b/lib/trails.js @@ -3,7 +3,6 @@ const path = require('path') const lib = require('.') -//const defaultsDeep = require('lodash.defaultsdeep') const merge = require('lodash.merge') module.exports = { @@ -32,35 +31,11 @@ module.exports = { stop: 10000 } }, - log: { } + log: { }, + motd: require('./motd') } return merge(configTemplate, initialConfig, (envConfig || { })) - - // merge config.env - // TODO refactor to lodash merge - /* - config = this.deepMerge( - (config || { }), - ((config.env && config.env[process.env.NODE_ENV]) || { }) - ) - - // merge config.main.paths - Object.assign( - configTemplate.main.paths, (config.main || { }).paths - ) - - // merge config.main.timeouts - Object.assign( - configTemplate.main.timeouts, (config.main || { }).timeouts - ) - - // merge application log - Object.assign(configTemplate.log, config.log) - - // merge remaining environment-specific config properties - return Object.assign({ }, config, configTemplate) - */ }, /** @@ -185,22 +160,21 @@ module.exports = { /** * Bind listeners various application events */ - bindEvents (app) { + bindListeners (app) { if (app.bound) { - app.log.warn('Someone attempted to bindEvents() twice! Stacktrace below.') + app.log.warn('Someone attempted to bindListeners() twice! Stacktrace below.') app.log.warn(console.trace()) // eslint-disable-line no-console return } - lib.Trails.bindApplicationEvents(app) + lib.Trails.bindApplicationListeners(app) app.bound = true }, /** * Bind listeners to trails application events */ - bindApplicationEvents (app) { - app.once('error', err => app.stop(err)) + bindApplicationListeners (app) { app.once('trailpack:all:configured', () => { if (app.config.main.freezeConfig !== false) { app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') @@ -208,7 +182,20 @@ module.exports = { lib.Trails.freezeConfig(app.config, app.loadedModules) } }) + app.once('trailpack:all:initialized', () => { + app.log.silly(app.config.motd.silly.initialized) + app.log.info(app.config.motd.info.initialized) + }) + app.once('trails:ready', () => { + app.log.info(app.config.motd.info.ready(app)) + app.log.debug(app.config.motd.debug.ready(app)) + app.log.silly(app.config.motd.silly.ready(app)) + + app.log.info(app.config.motd.hr) + }) app.once('trails:stop', () => { + app.log.silly(app.config.motd.silly.stop) + app.log.info(app.config.motd.info.stop) lib.Trails.unfreezeConfig(app, app.loadedModules) }) app.once('trails:error:fatal', err => app.stop(err)) @@ -217,7 +204,7 @@ module.exports = { /** * Unbind all listeners that were bound during application startup */ - unbindEvents (app) { + unbindListeners (app) { app.removeAllListeners() app.bound = false } diff --git a/package.json b/package.json index 5393f9b..7b004f8 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "joi": "^9.2.0", "lodash.defaultsdeep": "^4.6.0", "lodash.foreach": "^4.5.0", + "lodash.get": "^4.4.2", "lodash.includes": "^4.3.0", "lodash.isfunction": "^3.0.8", "lodash.isplainobject": "^4.0.6", From 3aef1ca9ddd9f494c6f0f039fe3604c215e766fc Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 15 Nov 2016 00:47:37 -0500 Subject: [PATCH 04/52] [refactor] remove events falsy check from onceAny --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 57d09ae..666af54 100644 --- a/index.js +++ b/index.js @@ -203,10 +203,9 @@ module.exports = class TrailsApp extends events.EventEmitter { onceAny (events, handler) { const self = this - if (!events) - return - if (!Array.isArray(events)) + if (!Array.isArray(events)) { events = [events] + } function cb (e) { self.removeListener(e, cb) @@ -226,6 +225,7 @@ module.exports = class TrailsApp extends events.EventEmitter { if (!Array.isArray(events)) { events = [ events ] } + return Promise.all(events.map(eventName => { return new Promise(resolve => { if (eventName instanceof Array){ From 8da326f0f4bfcf3631defe2672336216fa38ddc3 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 15 Nov 2016 15:20:51 -0500 Subject: [PATCH 05/52] [refactor] after and onceAny - after and onceAny now support both Promise and handler callbacks - fixed parameter passing in after method - added unit tests --- index.js | 39 ++++++++++++++++++++++--------- lib/trails.js | 4 +++- test/index.js | 63 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 666af54..6e7f15f 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const events = require('events') const lib = require('./lib') const i18next = require('i18next') +const NOOP = function () { } /** * The Trails Application. Merges the configuration and API resources @@ -198,34 +199,49 @@ module.exports = class TrailsApp extends events.EventEmitter { } /** - * Extend the once emiter reader for accept multi valid events + * Resolve Promise once ANY of the events in the list have emitted. Also + * accepts a callback. + * @return Promise */ - onceAny (events, handler) { - const self = this - + onceAny (events, handler = NOOP) { if (!Array.isArray(events)) { events = [events] } - function cb (e) { - self.removeListener(e, cb) - handler.apply(this, Array.prototype.slice.call(arguments, 0)) + let resolveCallback + const handlerWrapper = (...args) => { + handler(args) + return args } - events.forEach(e => { - this.addListener(e, cb) + return Promise.race(events.map(eventName => { + return new Promise(resolve => { + resolveCallback = resolve + this.once(eventName, resolveCallback) + }) + })) + .then(handlerWrapper) + .then(args => { + events.forEach(eventName => this.removeListener(eventName, resolveCallback)) + return args }) } /** - * Resolve Promise once all events in the list have emitted + * Resolve Promise once all events in the list have emitted. Also accepts + * a callback. * @return Promise */ - after (events) { + after (events, handler = NOOP) { if (!Array.isArray(events)) { events = [ events ] } + const handlerWrapper = (args) => { + handler(args) + return args + } + return Promise.all(events.map(eventName => { return new Promise(resolve => { if (eventName instanceof Array){ @@ -236,6 +252,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } }) })) + .then(handlerWrapper) } /** diff --git a/lib/trails.js b/lib/trails.js index 0d10624..3a91a7e 100644 --- a/lib/trails.js +++ b/lib/trails.js @@ -2,8 +2,8 @@ 'use strict' const path = require('path') -const lib = require('.') const merge = require('lodash.merge') +const lib = require('.') module.exports = { @@ -60,6 +60,8 @@ module.exports = { if (config.env && config.env.env) { throw new lib.Errors.ConfigValueError('config.env cannot contain an "env" property') } + + }, /** diff --git a/test/index.js b/test/index.js index 8ccb10e..7d129f2 100644 --- a/test/index.js +++ b/test/index.js @@ -197,35 +197,70 @@ describe('Trails', () => { return eventPromise }) it('should accept a single event as an array or a string', () => { - const eventPromise = app.after('test1') - app.emit('test1') + const eventPromise = app.after('test2') + app.emit('test2') return eventPromise }) it('should invoke listener when listening for multiple events', () => { - const eventPromise = app.after([ 'test1', 'test2', 'test3' ]) - app.emit('test1') - app.emit('test2') + const eventPromise = app.after([ 'test3', 'test4', 'test5' ]) app.emit('test3') + app.emit('test4') + app.emit('test5') return eventPromise }) it('should invoke listener when listening for multiple possible events', () => { - const eventPromise = app.after([['test1', 'test2'], 'test3']) - app.emit('test1') - app.emit('test3') + const eventPromise = app.after([['test6', 'test7'], 'test8']) + app.emit('test6') + app.emit('test8') return eventPromise }) - it('should pass event parameters to callbacks added using `onceAny`', done => { - const sent = { test: true } + it('should pass event parameters through to handler', () => { + const eventPromise = app.after(['test9', 'test10']) + .then(([ t1, t2 ]) => { + assert.equal(t1, 9) + assert.equal(t2, 10) + }) - app.onceAny('test', received => { - assert.equal(received, sent) + app.emit('test9', 9) + app.emit('test10', 10) - return done() + return eventPromise + }) + it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { + app.after(['test11', 'test12'], ([t1, t2]) => { + assert.equal(t1, 11) + assert.equal(t2, 12) + done() }) + app.emit('test11', 11) + app.emit('test12', 12) + }) + }) - app.emit('test', sent) + describe('#onceAny', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + it('should pass event parameters through to handler', () => { + const eventPromise = app.onceAny('test1') + .then(t1 => { + assert.equal(t1, 1) + }) + + app.emit('test1', 1) + + return eventPromise + }) + it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { + app.onceAny(['test1', 'test2'], t1 => { + assert.equal(t1, 1) + done() + }) + app.emit('test1', 1) }) }) }) From 7214fda2f0fae19368631ccf9ca4cdf7b6bfa94d Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 15 Nov 2016 17:08:05 -0500 Subject: [PATCH 06/52] [refactor] validate config against joi schema --- index.js | 4 ++-- lib/index.js | 1 + lib/schemas/api.js | 9 +++++++++ lib/schemas/config.js | 20 ++++++++++++++++++++ lib/schemas/index.js | 3 +++ lib/schemas/package.js | 7 +++++++ lib/trails.js | 6 +++++- 7 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 lib/schemas/api.js create mode 100644 lib/schemas/config.js create mode 100644 lib/schemas/index.js create mode 100644 lib/schemas/package.js diff --git a/index.js b/index.js index 6e7f15f..3b54786 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,6 @@ module.exports = class TrailsApp extends events.EventEmitter { } const processEnv = Object.freeze(JSON.parse(JSON.stringify(process.env))) - lib.Trails.validateConfig(app.config) Object.defineProperties(this, { env: { @@ -123,6 +122,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } }) + lib.Trails.validateConfig(app.config) lib.Core.createDefaultPaths(this) this.setMaxListeners(this.config.main.maxListeners) @@ -134,7 +134,7 @@ module.exports = class TrailsApp extends events.EventEmitter { this.config.main.packs.forEach(Pack => new Pack(this)) this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) - delete this.config.env // Delete env config, now it has been merged + delete this.config.env } /** diff --git a/lib/index.js b/lib/index.js index 1a72bbe..ce169e1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,3 +6,4 @@ exports.Trails = require('./trails') exports.Errors = require('./errors') exports.Pathfinder = require('./pathfinder') exports.Core = require('./core') +exports.Schemas = require('./schemas') diff --git a/lib/schemas/api.js b/lib/schemas/api.js new file mode 100644 index 0000000..e9c51ef --- /dev/null +++ b/lib/schemas/api.js @@ -0,0 +1,9 @@ +const joi = require('joi') + +module.exports = joi.object().keys({ + controllers: joi.object().required(), + models: joi.object().required(), + policies: joi.object().required(), + services: joi.object().required(), + config: joi.object().optional() +}).unknown() diff --git a/lib/schemas/config.js b/lib/schemas/config.js new file mode 100644 index 0000000..5b166e3 --- /dev/null +++ b/lib/schemas/config.js @@ -0,0 +1,20 @@ +const joi = require('joi') + +module.exports = joi.object().keys({ + trailpack: joi.object().keys({ + packs: joi.array(), + paths: joi.object().keys({ + root: joi.string().required() + }).unknown(), + disabled: joi.array() + }).unknown(), + + env: joi.object().keys({ + [process.env.NODE_ENV]: joi.object().required() + }).unknown(), + + log: joi.object().keys({ + logger: joi.object() + }).unknown() + +}).unknown() diff --git a/lib/schemas/index.js b/lib/schemas/index.js new file mode 100644 index 0000000..3a7bce4 --- /dev/null +++ b/lib/schemas/index.js @@ -0,0 +1,3 @@ +exports.api = require('./api') +exports.config = require('./config') +exports.package = require('./package') diff --git a/lib/schemas/package.js b/lib/schemas/package.js new file mode 100644 index 0000000..020e312 --- /dev/null +++ b/lib/schemas/package.js @@ -0,0 +1,7 @@ +const joi = require('joi') + +module.exports = joi.object().keys({ + dependencies: joi.object().keys({ + trails: joi.string() + }).unknown() +}).unknown() diff --git a/lib/trails.js b/lib/trails.js index 3a91a7e..5521bb8 100644 --- a/lib/trails.js +++ b/lib/trails.js @@ -2,6 +2,7 @@ 'use strict' const path = require('path') +const joi = require('joi') const merge = require('lodash.merge') const lib = require('.') @@ -61,7 +62,10 @@ module.exports = { throw new lib.Errors.ConfigValueError('config.env cannot contain an "env" property') } - + const result = joi.validate(config, lib.Schemas.config) + if (result.error) { + throw new Error('Project Configuration Error:', result.error) + } }, /** From bc9e91bd63a0f3bb43d90e67e1638626e30d7a70 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Wed, 16 Nov 2016 15:49:27 -0500 Subject: [PATCH 07/52] [fix] node4 syntax incompatibility --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3b54786..8927e6e 100644 --- a/index.js +++ b/index.js @@ -203,7 +203,8 @@ module.exports = class TrailsApp extends events.EventEmitter { * accepts a callback. * @return Promise */ - onceAny (events, handler = NOOP) { + onceAny (events, handler) { + handler || (handler = NOOP) if (!Array.isArray(events)) { events = [events] } @@ -232,7 +233,8 @@ module.exports = class TrailsApp extends events.EventEmitter { * a callback. * @return Promise */ - after (events, handler = NOOP) { + after (events, handler) { + handler || (handler = NOOP) if (!Array.isArray(events)) { events = [ events ] } From f3cc086fbbdb05c41d8950ba3eedc632f0ac3cd2 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Wed, 16 Nov 2016 15:49:52 -0500 Subject: [PATCH 08/52] 2.0.0-rc0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b004f8..1d730e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "1.1.1", + "version": "2.0.0-rc0", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 2d9d3932cecc9fb85334c3f4bc367c2ea5cf4aea Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 17 Nov 2016 14:00:35 -0500 Subject: [PATCH 09/52] [refactor] check trails constructor "app" arg --- index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 8927e6e..366e9b4 100644 --- a/index.js +++ b/index.js @@ -23,10 +23,13 @@ module.exports = class TrailsApp extends events.EventEmitter { constructor (app) { super() + if (!app) { + throw new RangeError('No app definition provided to Trails constructor') + } if (!app.pkg) { throw new lib.Errors.PackageNotDefinedError() } - if (!app.api && !(app && app.api)) { + if (!app.api) { throw new lib.Errors.ApiNotDefinedError() } From c2dbc5c0424e0fe76adea492226ff7a76f3fb2cc Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 19 Nov 2016 14:13:23 -0500 Subject: [PATCH 10/52] [pkg] include resource modules, peg versions --- controller.js | 1 + index.js | 17 +- lib/core.js | 228 +++++++++++++++++++++++-- lib/index.js | 1 - lib/trails.js | 217 ------------------------ model.js | 1 + package.json | 31 ++-- policy.js | 1 + service.js | 1 + test/lib/context.test.js | 342 +++++++++++++++++++++++++++++++++++++- test/lib/trails.test.js | 349 --------------------------------------- 11 files changed, 594 insertions(+), 595 deletions(-) create mode 100644 controller.js delete mode 100644 lib/trails.js create mode 100644 model.js create mode 100644 policy.js create mode 100644 service.js delete mode 100644 test/lib/trails.test.js diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..dfc3ff7 --- /dev/null +++ b/controller.js @@ -0,0 +1 @@ +module.exports = require('trails-controller') diff --git a/index.js b/index.js index 366e9b4..2a00442 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,9 @@ const lib = require('./lib') const i18next = require('i18next') const NOOP = function () { } +// inject Error types into the global namespace +Object.assign(global, lib.Errors) + /** * The Trails Application. Merges the configuration and API resources * loads Trailpacks, initializes logging and event listeners. @@ -55,7 +58,7 @@ module.exports = class TrailsApp extends events.EventEmitter { value: process.versions }, config: { - value: lib.Trails.buildConfig(app.config, processEnv), + value: lib.Core.buildConfig(app.config, processEnv), configurable: true }, api: { @@ -77,7 +80,7 @@ module.exports = class TrailsApp extends events.EventEmitter { }, loadedModules: { enumerable: false, - value: lib.Trails.getExternalModules(this.pkg) + value: lib.Core.getExternalModules(this.pkg) }, bound: { enumerable: false, @@ -125,7 +128,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } }) - lib.Trails.validateConfig(app.config) + lib.Core.validateConfig(app.config) lib.Core.createDefaultPaths(this) this.setMaxListeners(this.config.main.maxListeners) @@ -146,7 +149,7 @@ module.exports = class TrailsApp extends events.EventEmitter { * @return Promise */ start () { - lib.Trails.bindListeners(this) + lib.Core.bindListeners(this) lib.Trailpack.bindTrailpackPhaseListeners(this, this.loadedPacks) lib.Trailpack.bindTrailpackMethodListeners(this, this.loadedPacks) @@ -179,7 +182,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } this.emit('trails:stop') - lib.Trails.unbindListeners(this) + lib.Core.unbindListeners(this) return Promise.all( this.loadedPacks.map(pack => { @@ -268,6 +271,10 @@ module.exports = class TrailsApp extends events.EventEmitter { return this.config.log.logger } + /** + * Expose the i18n translator on the app object. Internationalization can be + * configured in config.i18n + */ get __ () { return this.translate } diff --git a/lib/core.js b/lib/core.js index 12efb6c..e8aa47f 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,11 +1,11 @@ +/*eslint no-console: 0 */ 'use strict' +const path = require('path') const fs = require('fs') -const isPlainObject = require('lodash.isplainobject') -const mapValues = require('lodash.mapvalues') -const forEach = require('lodash.foreach') -const includes = require('lodash.includes') -const isFunction = require('lodash.isFunction') +const joi = require('joi') +const _ = require('lodash') +const schemas = require('./schemas') const Core = module.exports = { @@ -25,15 +25,15 @@ const Core = module.exports = { * Bind the context of API resource methods. */ bindMethods (app, resource) { - return mapValues(app.api[resource], (Resource, resourceName) => { - if (isPlainObject(Resource)) { + return _.mapValues(app.api[resource], (Resource, resourceName) => { + if (_.isPlainObject(Resource)) { throw new Error(`${resourceName} should be a class. It is a regular object`) } const obj = new Resource(app) obj.methods = Core.getClassMethods(obj) - forEach(obj.methods, method => { + _.forEach(obj.methods, method => { obj[method] = obj[method].bind(obj) }) return obj @@ -50,8 +50,8 @@ const Core = module.exports = { while (!obj.isPrototypeOf(objectRoot)) { Object.getOwnPropertyNames(obj).forEach(prop => { if (props.indexOf(prop) === -1 && - !includes(Core.reservedMethods, prop) && - isFunction(obj[prop])) { + !_.includes(Core.reservedMethods, prop) && + _.isFunction(obj[prop])) { props.push(prop) } @@ -93,6 +93,212 @@ const Core = module.exports = { fs.mkdirSync(dir) } } - } + }, + + /** + * Copy and merge the provided configuration into a new object, decorated with + * necessary default and environment-specific values. + */ + buildConfig (initialConfig, nodeEnv) { + const root = path.resolve(path.dirname(require.main.filename)) + const temp = path.resolve(root, '.tmp') + const envConfig = initialConfig.env && initialConfig.env[nodeEnv] + const configTemplate = { + main: { + maxListeners: 128, + packs: [ ], + paths: { + root: root, + temp: temp, + sockets: path.resolve(temp, 'sockets'), + logs: path.resolve(temp, 'log') + }, + timeouts: { + start: 10000, + stop: 10000 + } + }, + log: { }, + motd: require('./motd') + } + + return _.merge(configTemplate, initialConfig, (envConfig || { })) + }, + + /** + * Validate the structure and prerequisites of the configuration object. Throw + * an Error if invalid; invalid configurations are unrecoverable and require + * that programmer fix them. + */ + validateConfig (config) { + if (!config || !config.main) { + throw new ConfigNotDefinedError() + } + + if (!config.log || !config.log.logger) { + throw new LoggerNotDefinedError() + } + + const nestedEnvs = Core.getNestedEnv(config) + if (nestedEnvs.length) { + throw new ConfigValueError('Environment configs cannot contain an "env" property') + } + + if (config.env && config.env.env) { + throw new ConfigValueError('config.env cannot contain an "env" property') + } + + const result = joi.validate(config, schemas.config) + if (result.error) { + throw new Error('Project Configuration Error:', result.error) + } + }, + + /** + * During config object inspection, we need to determine whether an arbitrary + * object is an external module loaded from a require statement. For example, + * the config object will contain trailpack modules, and we do not necessarily + * want to deep freeze all of them. In general, messing directly with loaded + * modules is not safe. + */ + getExternalModules () { + const rootPath = path.resolve(path.dirname(require.main.filename)) + const modulePath = path.join(rootPath, 'node_modules') + const requiredModules = Object.keys(require.cache).filter(mod => { + return mod.indexOf(modulePath) >= 0 + }) + return requiredModules + }, + + + /** + * Deep freeze application config object. Exceptions are made for required + * modules that are listed as dependencies in the application's + * package definition (package.json). Trails takes a firm stance that + * configuration should be modified only by the developer, the environment, + * and the trailpack configuration phase -- never by the application itself + * during runtime. + * + * @param config the configuration object to be frozen. + * @param [pkg] the package definition to use for exceptions. optional. + */ + freezeConfig (config, modules) { + const propNames = Object.getOwnPropertyNames(config) + + propNames.forEach(name => { + const prop = config[name] + + if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { + return + } + + const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) + if (ignoreModule) { + return + } + Core.freezeConfig(prop, modules) + }) + + Object.freeze(config) + }, + + /** + * Copy the configuration into a normal, unfrozen object + */ + unfreezeConfig (app, modules) { + const unfreeze = (target, source) => { + const propNames = Object.getOwnPropertyNames(source) + + propNames.forEach(name => { + const prop = source[name] + + if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { + target[name] = prop + return + } + + const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) + if (ignoreModule) { + return + } + + target[name] = { } + unfreeze(target[name], prop) + }) + + return target + } + + Object.defineProperties(app, { + config: { + value: unfreeze({ }, app.config), + configurable: true + } + }) + }, + + /** + * Check to see if the user defined a property config.env[env].env + */ + getNestedEnv (config) { + const env = (config && config.env) + const nestedEnvs = Object.keys(env || { }).filter(key => { + return !!env[key].env + }) + + return nestedEnvs + }, + + /** + * Bind listeners various application events + */ + bindListeners (app) { + if (app.bound) { + app.log.warn('Someone attempted to bindListeners() twice! Stacktrace below.') + app.log.warn(console.trace()) // eslint-disable-line no-console + return + } + + Core.bindApplicationListeners(app) + app.bound = true + }, + + /** + * Bind listeners to trails application events + */ + bindApplicationListeners (app) { + app.once('trailpack:all:configured', () => { + if (app.config.main.freezeConfig !== false) { + app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') + app.log.warn('Please only use this flag for testing/debugging purposes.') + Core.freezeConfig(app.config, app.loadedModules) + } + }) + app.once('trailpack:all:initialized', () => { + app.log.silly(app.config.motd.silly.initialized) + app.log.info(app.config.motd.info.initialized) + }) + app.once('trails:ready', () => { + app.log.info(app.config.motd.info.ready(app)) + app.log.debug(app.config.motd.debug.ready(app)) + app.log.silly(app.config.motd.silly.ready(app)) + + app.log.info(app.config.motd.hr) + }) + app.once('trails:stop', () => { + app.log.silly(app.config.motd.silly.stop) + app.log.info(app.config.motd.info.stop) + Core.unfreezeConfig(app, app.loadedModules) + }) + app.once('trails:error:fatal', err => app.stop(err)) + }, + + /** + * Unbind all listeners that were bound during application startup + */ + unbindListeners (app) { + app.removeAllListeners() + app.bound = false + } } diff --git a/lib/index.js b/lib/index.js index ce169e1..530ee97 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,6 @@ exports.i18n = require('./i18n') exports.Trailpack = require('./trailpack') -exports.Trails = require('./trails') exports.Errors = require('./errors') exports.Pathfinder = require('./pathfinder') exports.Core = require('./core') diff --git a/lib/trails.js b/lib/trails.js deleted file mode 100644 index 5521bb8..0000000 --- a/lib/trails.js +++ /dev/null @@ -1,217 +0,0 @@ -/*eslint no-console: 0 */ -'use strict' - -const path = require('path') -const joi = require('joi') -const merge = require('lodash.merge') -const lib = require('.') - -module.exports = { - - /** - * Copy and merge the provided configuration into a new object, decorated with - * necessary default and environment-specific values. - */ - buildConfig (initialConfig, nodeEnv) { - const root = path.resolve(path.dirname(require.main.filename)) - const temp = path.resolve(root, '.tmp') - const envConfig = initialConfig.env && initialConfig.env[nodeEnv] - - const configTemplate = { - main: { - maxListeners: 128, - packs: [ ], - paths: { - root: root, - temp: temp, - sockets: path.resolve(temp, 'sockets'), - logs: path.resolve(temp, 'log') - }, - timeouts: { - start: 10000, - stop: 10000 - } - }, - log: { }, - motd: require('./motd') - } - - return merge(configTemplate, initialConfig, (envConfig || { })) - }, - - /** - * Validate the structure and prerequisites of the configuration object. Throw - * an Error if invalid; invalid configurations are unrecoverable and require - * that programmer fix them. - */ - validateConfig (config) { - if (!config || !config.main) { - throw new lib.Errors.ConfigNotDefinedError() - } - - if (!config.log || !config.log.logger) { - throw new lib.Errors.LoggerNotDefinedError() - } - - const nestedEnvs = lib.Trails.getNestedEnv(config) - if (nestedEnvs.length) { - throw new lib.Errors.ConfigValueError('Environment configs cannot contain an "env" property') - } - - if (config.env && config.env.env) { - throw new lib.Errors.ConfigValueError('config.env cannot contain an "env" property') - } - - const result = joi.validate(config, lib.Schemas.config) - if (result.error) { - throw new Error('Project Configuration Error:', result.error) - } - }, - - /** - * During config object inspection, we need to determine whether an arbitrary - * object is an external module loaded from a require statement. For example, - * the config object will contain trailpack modules, and we do not necessarily - * want to deep freeze all of them. In general, messing directly with loaded - * modules is not safe. - */ - getExternalModules () { - const rootPath = path.resolve(path.dirname(require.main.filename)) - const modulePath = path.join(rootPath, 'node_modules') - const requiredModules = Object.keys(require.cache).filter(mod => { - return mod.indexOf(modulePath) >= 0 - }) - return requiredModules - }, - - - /** - * Deep freeze application config object. Exceptions are made for required - * modules that are listed as dependencies in the application's - * package definition (package.json). Trails takes a firm stance that - * configuration should be modified only by the developer, the environment, - * and the trailpack configuration phase -- never by the application itself - * during runtime. - * - * @param config the configuration object to be frozen. - * @param [pkg] the package definition to use for exceptions. optional. - */ - freezeConfig (config, modules) { - const propNames = Object.getOwnPropertyNames(config) - - propNames.forEach(name => { - const prop = config[name] - - if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { - return - } - - const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) - if (ignoreModule) { - return - } - lib.Trails.freezeConfig(prop, modules) - }) - - Object.freeze(config) - }, - - /** - * Copy the configuration into a normal, unfrozen object - */ - unfreezeConfig (app, modules) { - const unfreeze = (target, source) => { - const propNames = Object.getOwnPropertyNames(source) - - propNames.forEach(name => { - const prop = source[name] - - if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { - target[name] = prop - return - } - - const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) - if (ignoreModule) { - return - } - - target[name] = { } - unfreeze(target[name], prop) - }) - - return target - } - - Object.defineProperties(app, { - config: { - value: unfreeze({ }, app.config), - configurable: true - } - }) - }, - - /** - * Check to see if the user defined a property config.env[env].env - */ - getNestedEnv (config) { - const env = (config && config.env) - const nestedEnvs = Object.keys(env || { }).filter(key => { - return !!env[key].env - }) - - return nestedEnvs - }, - - /** - * Bind listeners various application events - */ - bindListeners (app) { - if (app.bound) { - app.log.warn('Someone attempted to bindListeners() twice! Stacktrace below.') - app.log.warn(console.trace()) // eslint-disable-line no-console - return - } - - lib.Trails.bindApplicationListeners(app) - app.bound = true - }, - - /** - * Bind listeners to trails application events - */ - bindApplicationListeners (app) { - app.once('trailpack:all:configured', () => { - if (app.config.main.freezeConfig !== false) { - app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') - app.log.warn('Please only use this flag for testing/debugging purposes.') - lib.Trails.freezeConfig(app.config, app.loadedModules) - } - }) - app.once('trailpack:all:initialized', () => { - app.log.silly(app.config.motd.silly.initialized) - app.log.info(app.config.motd.info.initialized) - }) - app.once('trails:ready', () => { - app.log.info(app.config.motd.info.ready(app)) - app.log.debug(app.config.motd.debug.ready(app)) - app.log.silly(app.config.motd.silly.ready(app)) - - app.log.info(app.config.motd.hr) - }) - app.once('trails:stop', () => { - app.log.silly(app.config.motd.silly.stop) - app.log.info(app.config.motd.info.stop) - lib.Trails.unfreezeConfig(app, app.loadedModules) - }) - app.once('trails:error:fatal', err => app.stop(err)) - }, - - /** - * Unbind all listeners that were bound during application startup - */ - unbindListeners (app) { - app.removeAllListeners() - app.bound = false - } -} diff --git a/model.js b/model.js new file mode 100644 index 0000000..d11ebf2 --- /dev/null +++ b/model.js @@ -0,0 +1 @@ +module.exports = require('trails-model') diff --git a/package.json b/package.json index 1d730e0..855bfd8 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "hapi", "i18n", "orm", - "server" + "server", + "graphql" ], "scripts": { "test": "eslint . && mocha", @@ -65,16 +66,14 @@ }, "homepage": "http://trailsjs.io", "dependencies": { - "i18next": "^3.4.1", - "joi": "^9.2.0", - "lodash.defaultsdeep": "^4.6.0", - "lodash.foreach": "^4.5.0", - "lodash.get": "^4.4.2", - "lodash.includes": "^4.3.0", - "lodash.isfunction": "^3.0.8", - "lodash.isplainobject": "^4.0.6", - "lodash.mapvalues": "^4.6.0", - "lodash.merge": "^4.6.0" + "hoek": "4.1.0", + "i18next": "3.4.1", + "joi": "9.2.0", + "lodash": "4.17.2", + "trails-controller": "1.0.0", + "trails-model": "1.0.0", + "trails-policy": "1.0.1", + "trails-service": "1.0.0" }, "devDependencies": { "eslint": "^2.5.3", @@ -86,6 +85,16 @@ "trailpack": "^1.0.1", "winston": "^2.1.1" }, + "bundledDependencies": [ + "hoek", + "i18next", + "joi", + "lodash", + "trails-controller", + "trails-model", + "trails-policy", + "trails-service" + ], "eslintConfig": { "extends": "trails" }, diff --git a/policy.js b/policy.js new file mode 100644 index 0000000..856754e --- /dev/null +++ b/policy.js @@ -0,0 +1 @@ +module.exports = require('trails-policy') diff --git a/service.js b/service.js new file mode 100644 index 0000000..de13049 --- /dev/null +++ b/service.js @@ -0,0 +1 @@ +module.exports = require('trails-service') diff --git a/test/lib/context.test.js b/test/lib/context.test.js index 80fe6db..159cb71 100644 --- a/test/lib/context.test.js +++ b/test/lib/context.test.js @@ -1,7 +1,9 @@ 'use strict' -const _ = require('lodash') +const path = require('path') const assert = require('assert') +const _ = require('lodash') +const smokesignals = require('smokesignals') const lib = require('../../lib') describe('lib.Core', () => { @@ -42,4 +44,342 @@ describe('lib.Core', () => { assert(_.includes(methods, 'baz')) }) }) + describe('#buildConfig', () => { + const NODE_ENV = process.env.NODE_ENV + let testConfig + + beforeEach(() => { + testConfig = { + env: { + envTest1: { + log: { + merged: 'yes', + extraneous: 'assigned' + }, + customObject: { + string: 'b', + int: 2, + array: [2, 3, 4], + subobj: { + attr: 'b' + } + } + }, + envTest2: { + log: { + nested: { + merged: 'yes', + extraneous: 'assigned' + }, + merged: 'yes', + extraneous: 'assigned' + }, + customObject: { + subobj: { + attr: 'b' + }, + int2: 2 + } + }, + envTest3: { + log: { + nested: { + merged: 'yes', + extraneous: 'assigned', + deeplyNested: { + merged: 'yes', + extraneous: 'assigned' + } + }, + merged: 'yes', + extraneous: 'assigned' + } + } + }, + log: { + merged: 'no', + nested: { + merged: 'no', + deeplyNested: { + merged: 'no' + } + }, + normal: 'yes' + }, + customObject: { + string: 'a', + int: 1, + array: [1, 2, 3], + subobj: { + attr: 'a' + } + } + } + }) + + afterEach(() => { + process.env.NODE_ENV = NODE_ENV + }) + + it('should merge basic env config', () => { + const config = lib.Core.buildConfig(testConfig, 'envTest1') + + assert(config) + assert.equal(config.log.merged, 'yes') + assert.equal(config.log.extraneous, 'assigned') + assert.equal(config.log.normal, 'yes') + assert.equal(config.log.nested.merged, 'no') + + assert.equal(config.main.maxListeners, 128) + }) + + it('should merge nested env config', () => { + process.env.NODE_ENV = 'envTest2' + const config = lib.Core.buildConfig(testConfig, 'envTest2') + + assert(config) + assert.equal(config.log.merged, 'yes') + assert.equal(config.log.nested.merged, 'yes') + + assert.equal(config.log.extraneous, 'assigned') + assert.equal(config.log.nested.extraneous, 'assigned') + assert.equal(config.log.normal, 'yes') + assert.equal(config.log.extraneous, 'assigned') + assert.equal(config.log.normal, 'yes') + + assert.equal(config.main.maxListeners, 128) + }) + + it('should merge deeply nested env config', () => { + process.env.NODE_ENV = 'envTest3' + const config = lib.Core.buildConfig(testConfig, 'envTest3') + + assert(config) + assert.equal(config.log.merged, 'yes') + assert.equal(config.log.nested.merged, 'yes') + assert.equal(config.log.nested.deeplyNested.merged, 'yes') + + assert.equal(config.log.extraneous, 'assigned') + assert.equal(config.log.nested.extraneous, 'assigned') + assert.equal(config.log.nested.deeplyNested.extraneous, 'assigned') + + assert.equal(config.log.normal, 'yes') + assert.equal(config.log.extraneous, 'assigned') + assert.equal(config.log.normal, 'yes') + + assert.equal(config.main.maxListeners, 128) + }) + + it('should merge full custom env config', () => { + process.env.NODE_ENV = 'envTest1' + const config = lib.Core.buildConfig(testConfig, 'envTest1') + + assert(config) + assert(typeof config.customObject === 'object') + assert.equal(config.customObject.string, 'b') + assert.equal(config.customObject.int, 2) + assert(Array.isArray(config.customObject.array)) + assert.equal(config.customObject.array[0], 2) + assert(typeof config.customObject.subobj === 'object') + assert.equal(config.customObject.subobj.attr, 'b') + }) + + it('should merge partial custom env config', () => { + process.env.NODE_ENV = 'envTest2' + const config = lib.Core.buildConfig(testConfig, 'envTest2') + + assert(config) + assert(typeof config.customObject === 'object') + assert.equal(config.customObject.string, 'a') + assert.equal(config.customObject.int, 1) + assert(Array.isArray(config.customObject.array)) + assert.equal(config.customObject.array[0], 1) + assert(typeof config.customObject.subobj === 'object') + assert.equal(config.customObject.subobj.attr, 'b') + }) + + it('should merge new custom attr in env config', () => { + process.env.NODE_ENV = 'envTest2' + const config = lib.Core.buildConfig(testConfig, 'envTest2') + + assert(config) + assert(typeof config.customObject === 'object') + assert.equal(config.customObject.string, 'a') + assert.equal(config.customObject.int, 1) + assert(Array.isArray(config.customObject.array)) + assert.equal(config.customObject.array[0], 1) + assert(typeof config.customObject.subobj === 'object') + assert.equal(config.customObject.subobj.attr, 'b') + assert.equal(config.customObject.int2, 2) + }) + + it('should not override any configs if NODE_ENV matches no env', () => { + process.env.NODE_ENV = 'notconfigured' + const config = lib.Core.buildConfig(testConfig, 'notconfigured') + + assert(config) + assert.equal(config.log.merged, 'no') + assert.equal(config.log.normal, 'yes') + assert(!config.log.extraneous) + assert(config.env) + + assert.equal(config.main.maxListeners, 128) + }) + + it('should keep "env" property from config', () => { + process.env.NODE_ENV = 'mergetest2' + const config = lib.Core.buildConfig(testConfig, 'mergetest2') + assert(config.env) + }) + }) + + describe('#getNestedEnv', () => { + it('should return a list of envs if one contains a "env" property', () => { + const testConfig = { + main: { }, + log: { + logger: new smokesignals.Logger('silent') + }, + env: { + envtest: { + env: { + invalid: true + } + } + } + } + + const nestedEnvs = lib.Core.getNestedEnv(testConfig) + + assert.equal(nestedEnvs[0], 'envtest') + assert.equal(nestedEnvs.length, 1) + }) + }) + + describe('#validateConfig', () => { + it('should throw ConfigValueError if an env config contains the "env" property', () => { + const testConfig = { + main: { }, + log: { + logger: new smokesignals.Logger('silent') + }, + env: { + envtest: { + env: 'hello' + } + } + } + assert.throws(() => lib.Core.validateConfig(testConfig), lib.Errors.ConfigValueError) + assert.throws(() => lib.Core.validateConfig(testConfig), /Environment configs/) + }) + it('should throw ConfigValueError if config.env contains the "env" property', () => { + const testConfig = { + main: { }, + log: { + logger: new smokesignals.Logger('silent') + }, + env: { + env: 'hello' + } + } + assert.throws(() => lib.Core.validateConfig(testConfig), lib.Errors.ConfigValueError) + assert.throws(() => lib.Core.validateConfig(testConfig), /config.env/) + }) + }) + + describe('#freezeConfig', () => { + it('should freeze nested object', () => { + const o1 = { foo: { bar: 1 } } + lib.Core.freezeConfig(o1, [ ]) + + assert.throws(() => o1.foo = null, Error) + }) + it('should not freeze exernal modules required from config', () => { + const o1 = { + foo: require('smokesignals'), + bar: 1 + } + lib.Core.freezeConfig(o1, [ require.resolve('smokesignals') ]) + + assert.throws(() => o1.bar = null, Error) + + o1.foo.x = 1 + assert.equal(o1.foo.x, 1) + }) + + // https://bugs.chromium.org/p/v8/issues/detail?id=4460 + if (!/^v6/.test(process.version)) { + it('v8 issue 4460 exists', () => { + assert.throws(() => Object.freeze(new Int8Array()), TypeError) + //assert.throws(() => Object.freeze(new Buffer([1,2,3])), TypeError) + //assert.throws(() => Object.freeze(new DataView()), TypeError) + }) + } + it('should freeze objects containing unfreezable types without error', () => { + const o1 = { + typedArray: new Int8Array(), + buffer: new Buffer([ 1,2,3 ]), + fun: function () { } + } + lib.Core.freezeConfig(o1, [ ]) + + assert(o1.typedArray) + assert(Buffer.isBuffer(o1.buffer)) + assert(o1.fun) + }) + }) + + describe('#unfreezeConfig', () => { + it('should unfreeze shallow config object', () => { + const app = { + config: { + a: 1, + foo: 'bar' + } + } + lib.Core.freezeConfig(app.config, [ ]) + assert.throws(() => app.config.a = 2, Error) + + lib.Core.unfreezeConfig(app, [ ]) + app.config.a = 2 + assert.equal(app.config.a, 2) + }) + it('should unfreeze deep config object', () => { + const app = { + config: { + main: { + paths: { + root: 'rootpath', + temp: 'temppath' + }, + foo: 1 + } + } + } + lib.Core.freezeConfig(app.config, [ ]) + assert.throws(() => app.config.main.paths.root = 'newrootpath', Error) + + lib.Core.unfreezeConfig(app, [ ]) + app.config.main.paths.root = 'newrootpath' + assert.equal(app.config.main.paths.root, 'newrootpath') + assert.equal(app.config.main.paths.temp, 'temppath') + assert.equal(app.config.main.foo, 1) + }) + }) + + describe('#getExternalModules', () => { + const rmf = require.main.filename + + beforeEach(() => { + require.main.filename = path.resolve(__dirname, '..', '..', 'index.js') + }) + afterEach(() => { + require.main.filename = rmf + }) + it('should return external modules', () => { + const modules = lib.Core.getExternalModules() + assert(modules.indexOf(require.resolve('mocha')) !== -1) + }) + }) + }) diff --git a/test/lib/trails.test.js b/test/lib/trails.test.js deleted file mode 100644 index 0d4e786..0000000 --- a/test/lib/trails.test.js +++ /dev/null @@ -1,349 +0,0 @@ -'use strict' - -const path = require('path') -const assert = require('assert') -const smokesignals = require('smokesignals') -const lib = require('../../lib') - -describe('lib.Trails', () => { - - describe('#buildConfig', () => { - const NODE_ENV = process.env.NODE_ENV - let testConfig - - beforeEach(() => { - testConfig = { - env: { - envTest1: { - log: { - merged: 'yes', - extraneous: 'assigned' - }, - customObject: { - string: 'b', - int: 2, - array: [2, 3, 4], - subobj: { - attr: 'b' - } - } - }, - envTest2: { - log: { - nested: { - merged: 'yes', - extraneous: 'assigned' - }, - merged: 'yes', - extraneous: 'assigned' - }, - customObject: { - subobj: { - attr: 'b' - }, - int2: 2 - } - }, - envTest3: { - log: { - nested: { - merged: 'yes', - extraneous: 'assigned', - deeplyNested: { - merged: 'yes', - extraneous: 'assigned' - } - }, - merged: 'yes', - extraneous: 'assigned' - } - } - }, - log: { - merged: 'no', - nested: { - merged: 'no', - deeplyNested: { - merged: 'no' - } - }, - normal: 'yes' - }, - customObject: { - string: 'a', - int: 1, - array: [1, 2, 3], - subobj: { - attr: 'a' - } - } - } - }) - - afterEach(() => { - process.env.NODE_ENV = NODE_ENV - }) - - it('should merge basic env config', () => { - const config = lib.Trails.buildConfig(testConfig, 'envTest1') - - assert(config) - assert.equal(config.log.merged, 'yes') - assert.equal(config.log.extraneous, 'assigned') - assert.equal(config.log.normal, 'yes') - assert.equal(config.log.nested.merged, 'no') - - assert.equal(config.main.maxListeners, 128) - }) - - it('should merge nested env config', () => { - process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig, 'envTest2') - - assert(config) - assert.equal(config.log.merged, 'yes') - assert.equal(config.log.nested.merged, 'yes') - - assert.equal(config.log.extraneous, 'assigned') - assert.equal(config.log.nested.extraneous, 'assigned') - assert.equal(config.log.normal, 'yes') - assert.equal(config.log.extraneous, 'assigned') - assert.equal(config.log.normal, 'yes') - - assert.equal(config.main.maxListeners, 128) - }) - - it('should merge deeply nested env config', () => { - process.env.NODE_ENV = 'envTest3' - const config = lib.Trails.buildConfig(testConfig, 'envTest3') - - assert(config) - assert.equal(config.log.merged, 'yes') - assert.equal(config.log.nested.merged, 'yes') - assert.equal(config.log.nested.deeplyNested.merged, 'yes') - - assert.equal(config.log.extraneous, 'assigned') - assert.equal(config.log.nested.extraneous, 'assigned') - assert.equal(config.log.nested.deeplyNested.extraneous, 'assigned') - - assert.equal(config.log.normal, 'yes') - assert.equal(config.log.extraneous, 'assigned') - assert.equal(config.log.normal, 'yes') - - assert.equal(config.main.maxListeners, 128) - }) - - it('should merge full custom env config', () => { - process.env.NODE_ENV = 'envTest1' - const config = lib.Trails.buildConfig(testConfig, 'envTest1') - - assert(config) - assert(typeof config.customObject === 'object') - assert.equal(config.customObject.string, 'b') - assert.equal(config.customObject.int, 2) - assert(Array.isArray(config.customObject.array)) - assert.equal(config.customObject.array[0], 2) - assert(typeof config.customObject.subobj === 'object') - assert.equal(config.customObject.subobj.attr, 'b') - }) - - it('should merge partial custom env config', () => { - process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig, 'envTest2') - - assert(config) - assert(typeof config.customObject === 'object') - assert.equal(config.customObject.string, 'a') - assert.equal(config.customObject.int, 1) - assert(Array.isArray(config.customObject.array)) - assert.equal(config.customObject.array[0], 1) - assert(typeof config.customObject.subobj === 'object') - assert.equal(config.customObject.subobj.attr, 'b') - }) - - it('should merge new custom attr in env config', () => { - process.env.NODE_ENV = 'envTest2' - const config = lib.Trails.buildConfig(testConfig, 'envTest2') - - assert(config) - assert(typeof config.customObject === 'object') - assert.equal(config.customObject.string, 'a') - assert.equal(config.customObject.int, 1) - assert(Array.isArray(config.customObject.array)) - assert.equal(config.customObject.array[0], 1) - assert(typeof config.customObject.subobj === 'object') - assert.equal(config.customObject.subobj.attr, 'b') - assert.equal(config.customObject.int2, 2) - }) - - it('should not override any configs if NODE_ENV matches no env', () => { - process.env.NODE_ENV = 'notconfigured' - const config = lib.Trails.buildConfig(testConfig, 'notconfigured') - - assert(config) - assert.equal(config.log.merged, 'no') - assert.equal(config.log.normal, 'yes') - assert(!config.log.extraneous) - assert(config.env) - - assert.equal(config.main.maxListeners, 128) - }) - - it('should keep "env" property from config', () => { - process.env.NODE_ENV = 'mergetest2' - const config = lib.Trails.buildConfig(testConfig, 'mergetest2') - assert(config.env) - }) - }) - - describe('#getNestedEnv', () => { - it('should return a list of envs if one contains a "env" property', () => { - const testConfig = { - main: { }, - log: { - logger: new smokesignals.Logger('silent') - }, - env: { - envtest: { - env: { - invalid: true - } - } - } - } - - const nestedEnvs = lib.Trails.getNestedEnv(testConfig) - - assert.equal(nestedEnvs[0], 'envtest') - assert.equal(nestedEnvs.length, 1) - }) - }) - - describe('#validateConfig', () => { - it('should throw ConfigValueError if an env config contains the "env" property', () => { - const testConfig = { - main: { }, - log: { - logger: new smokesignals.Logger('silent') - }, - env: { - envtest: { - env: 'hello' - } - } - } - assert.throws(() => lib.Trails.validateConfig(testConfig), lib.Errors.ConfigValueError) - assert.throws(() => lib.Trails.validateConfig(testConfig), /Environment configs/) - }) - it('should throw ConfigValueError if config.env contains the "env" property', () => { - const testConfig = { - main: { }, - log: { - logger: new smokesignals.Logger('silent') - }, - env: { - env: 'hello' - } - } - assert.throws(() => lib.Trails.validateConfig(testConfig), lib.Errors.ConfigValueError) - assert.throws(() => lib.Trails.validateConfig(testConfig), /config.env/) - }) - }) - - describe('#freezeConfig', () => { - it('should freeze nested object', () => { - const o1 = { foo: { bar: 1 } } - lib.Trails.freezeConfig(o1, [ ]) - - assert.throws(() => o1.foo = null, Error) - }) - it('should not freeze exernal modules required from config', () => { - const o1 = { - foo: require('smokesignals'), - bar: 1 - } - lib.Trails.freezeConfig(o1, [ require.resolve('smokesignals') ]) - - assert.throws(() => o1.bar = null, Error) - - o1.foo.x = 1 - assert.equal(o1.foo.x, 1) - }) - - // https://bugs.chromium.org/p/v8/issues/detail?id=4460 - if (!/^v6/.test(process.version)) { - it('v8 issue 4460 exists', () => { - assert.throws(() => Object.freeze(new Int8Array()), TypeError) - //assert.throws(() => Object.freeze(new Buffer([1,2,3])), TypeError) - //assert.throws(() => Object.freeze(new DataView()), TypeError) - }) - } - it('should freeze objects containing unfreezable types without error', () => { - const o1 = { - typedArray: new Int8Array(), - buffer: new Buffer([ 1,2,3 ]), - fun: function () { } - } - lib.Trails.freezeConfig(o1, [ ]) - - assert(o1.typedArray) - assert(Buffer.isBuffer(o1.buffer)) - assert(o1.fun) - }) - }) - - describe('#unfreezeConfig', () => { - it('should unfreeze shallow config object', () => { - const app = { - config: { - a: 1, - foo: 'bar' - } - } - lib.Trails.freezeConfig(app.config, [ ]) - assert.throws(() => app.config.a = 2, Error) - - lib.Trails.unfreezeConfig(app, [ ]) - app.config.a = 2 - assert.equal(app.config.a, 2) - }) - it('should unfreeze deep config object', () => { - const app = { - config: { - main: { - paths: { - root: 'rootpath', - temp: 'temppath' - }, - foo: 1 - } - } - } - lib.Trails.freezeConfig(app.config, [ ]) - assert.throws(() => app.config.main.paths.root = 'newrootpath', Error) - - lib.Trails.unfreezeConfig(app, [ ]) - app.config.main.paths.root = 'newrootpath' - assert.equal(app.config.main.paths.root, 'newrootpath') - assert.equal(app.config.main.paths.temp, 'temppath') - assert.equal(app.config.main.foo, 1) - }) - }) - - describe('#getExternalModules', () => { - const rmf = require.main.filename - - beforeEach(() => { - require.main.filename = path.resolve(__dirname, '..', '..', 'index.js') - }) - afterEach(() => { - require.main.filename = rmf - }) - it('should return external modules', () => { - const modules = lib.Trails.getExternalModules() - assert(modules.indexOf(require.resolve('mocha')) !== -1) - }) - }) - -}) - From d90c893324a56da4cd034979f982dc9058e13ed2 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 19 Nov 2016 14:31:36 -0500 Subject: [PATCH 11/52] [fix] corrected stale config validation schema - config.env is set to the (string) value of NODE_ENV --- index.js | 6 ++---- lib/core.js | 6 +++++- lib/schemas/config.js | 11 ++++------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 2a00442..406d730 100644 --- a/index.js +++ b/index.js @@ -58,7 +58,7 @@ module.exports = class TrailsApp extends events.EventEmitter { value: process.versions }, config: { - value: lib.Core.buildConfig(app.config, processEnv), + value: lib.Core.buildConfig(app.config, processEnv.NODE_ENV), configurable: true }, api: { @@ -128,7 +128,7 @@ module.exports = class TrailsApp extends events.EventEmitter { } }) - lib.Core.validateConfig(app.config) + lib.Core.validateConfig(this.config) lib.Core.createDefaultPaths(this) this.setMaxListeners(this.config.main.maxListeners) @@ -139,8 +139,6 @@ module.exports = class TrailsApp extends events.EventEmitter { this.config.main.packs.forEach(Pack => new Pack(this)) this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) - - delete this.config.env } /** diff --git a/lib/core.js b/lib/core.js index e8aa47f..8fe662e 100644 --- a/lib/core.js +++ b/lib/core.js @@ -123,7 +123,10 @@ const Core = module.exports = { motd: require('./motd') } - return _.merge(configTemplate, initialConfig, (envConfig || { })) + const mergedConfig = _.merge(configTemplate, initialConfig, (envConfig || { })) + mergedConfig.env = nodeEnv + + return mergedConfig }, /** @@ -151,6 +154,7 @@ const Core = module.exports = { const result = joi.validate(config, schemas.config) if (result.error) { + console.error(result) throw new Error('Project Configuration Error:', result.error) } }, diff --git a/lib/schemas/config.js b/lib/schemas/config.js index 5b166e3..2a3024e 100644 --- a/lib/schemas/config.js +++ b/lib/schemas/config.js @@ -1,17 +1,14 @@ const joi = require('joi') module.exports = joi.object().keys({ - trailpack: joi.object().keys({ + main: joi.object().keys({ packs: joi.array(), paths: joi.object().keys({ root: joi.string().required() - }).unknown(), - disabled: joi.array() - }).unknown(), + }).unknown() + }).required().unknown(), - env: joi.object().keys({ - [process.env.NODE_ENV]: joi.object().required() - }).unknown(), + env: joi.string().required(), log: joi.object().keys({ logger: joi.object() From 5f8d178f0adf20f5d34c44f0c94fa762a14936d8 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 21 Nov 2016 17:25:53 -0500 Subject: [PATCH 12/52] [refactor] configuration encapsulated in own class - resolves #124 --- index.js | 27 +- lib/ConfigProxy.js | 5 + lib/Configuration.js | 183 +++++++++++ lib/core.js | 147 +-------- lib/index.js | 2 + lib/schemas/config.js | 3 +- ...{context.test.js => Configuration.test.js} | 298 +++++++++--------- test/lib/Core.test.js | 63 ++++ 8 files changed, 421 insertions(+), 307 deletions(-) create mode 100644 lib/ConfigProxy.js create mode 100644 lib/Configuration.js rename test/lib/{context.test.js => Configuration.test.js} (61%) create mode 100644 test/lib/Core.test.js diff --git a/index.js b/index.js index 406d730..007b67e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /*eslint no-console: 0 */ 'use strict' -const events = require('events') +const EventEmitter = require('events').EventEmitter const lib = require('./lib') const i18next = require('i18next') const NOOP = function () { } @@ -13,7 +13,7 @@ Object.assign(global, lib.Errors) * The Trails Application. Merges the configuration and API resources * loads Trailpacks, initializes logging and event listeners. */ -module.exports = class TrailsApp extends events.EventEmitter { +module.exports = class TrailsApp extends EventEmitter { /** * @param pkg The application package.json @@ -58,8 +58,9 @@ module.exports = class TrailsApp extends events.EventEmitter { value: process.versions }, config: { - value: lib.Core.buildConfig(app.config, processEnv.NODE_ENV), - configurable: true + value: new lib.Configuration(app.config, processEnv), + configurable: true, + writable: false }, api: { value: app.api, @@ -128,7 +129,6 @@ module.exports = class TrailsApp extends events.EventEmitter { } }) - lib.Core.validateConfig(this.config) lib.Core.createDefaultPaths(this) this.setMaxListeners(this.config.main.maxListeners) @@ -191,6 +191,10 @@ module.exports = class TrailsApp extends events.EventEmitter { this.log.debug('All trailpacks unloaded. Done.') return this }) + .catch(err => { + console.error(err) + return this + }) } /** @@ -261,6 +265,19 @@ module.exports = class TrailsApp extends events.EventEmitter { .then(handlerWrapper) } + freezeConfig () { + this.config.freeze(this.loadedModules) + } + + unfreezeConfig () { + Object.defineProperties(this, { + config: { + value: this.config.unfreeze(), + configurable: true + } + }) + } + /** * Expose the logger on the app object. The logger can be configured by * setting the "config.log.logger" config property. diff --git a/lib/ConfigProxy.js b/lib/ConfigProxy.js new file mode 100644 index 0000000..ca345e8 --- /dev/null +++ b/lib/ConfigProxy.js @@ -0,0 +1,5 @@ +/** + * TODO in v3.0 when node4 support is dropped + */ +module.exports = class ConfigProxy { +} diff --git a/lib/Configuration.js b/lib/Configuration.js new file mode 100644 index 0000000..892781e --- /dev/null +++ b/lib/Configuration.js @@ -0,0 +1,183 @@ +/*eslint no-console: 0 */ + +const _ = require('lodash') +const path = require('path') +const joi = require('joi') +const schemas = require('./schemas') + +module.exports = class Configuration extends Map { + constructor (configTree, processEnv) { + super() + + this.immutable = false + this.env = processEnv + this.tree = Configuration.buildConfig(configTree, processEnv.NODE_ENV) + + Configuration.validateConfig(this.tree) + + // this looks somewhat strange; I'd like to use a Proxy here, but Node 4 + // does not support it. These properties will be exposed via a Proxy + // for v3.0 when Node 4 support can be dropped. + Object.assign(this, this.tree) + } + + /** + * @override + */ + get (key) { + return _.get(this, key) + } + + /** + * @override + */ + set (key, val) { + return _.set(this, key, val) + } + + /** + * @override + */ + freeze (modules) { + this.immutable = true + this.modules = modules + Configuration.freezeConfig(this, modules) + } + + unfreeze () { + return Configuration.unfreezeConfig(this, this.modules) + } + + /** + * Copy and merge the provided configuration into a new object, decorated with + * necessary default and environment-specific values. + */ + static buildConfig (initialConfig, nodeEnv) { + const root = path.resolve(path.dirname(require.main.filename)) + const temp = path.resolve(root, '.tmp') + const envConfig = initialConfig.env && initialConfig.env[nodeEnv] + + const configTemplate = { + main: { + maxListeners: 128, + packs: [ ], + paths: { + root: root, + temp: temp, + sockets: path.resolve(temp, 'sockets'), + logs: path.resolve(temp, 'log') + }, + timeouts: { + start: 10000, + stop: 10000 + }, + freezeConfig: true + }, + log: { }, + motd: require('./motd') + } + + const mergedConfig = _.merge(configTemplate, initialConfig, (envConfig || { })) + mergedConfig.env = nodeEnv + + return mergedConfig + } + + /** + * Validate the structure and prerequisites of the configuration object. Throw + * an Error if invalid; invalid configurations are unrecoverable and require + * that programmer fix them. + */ + static validateConfig (config) { + if (!config || !config.main) { + throw new ConfigNotDefinedError() + } + + if (!config.log || !config.log.logger) { + throw new LoggerNotDefinedError() + } + + const nestedEnvs = Configuration.getNestedEnv(config) + if (nestedEnvs.length) { + throw new ConfigValueError('Environment configs cannot contain an "env" property') + } + + if (config.env && config.env.env) { + throw new ConfigValueError('config.env cannot contain an "env" property') + } + + const result = joi.validate(config, schemas.config) + if (result.error) { + console.error(result) + throw new Error('Project Configuration Error:', result.error) + } + } + + /** + * Check to see if the user defined a property config.env[env].env + */ + static getNestedEnv (config) { + const env = (config && config.env) + const nestedEnvs = Object.keys(env || { }).filter(key => { + return !!env[key].env + }) + + return nestedEnvs + } + + /** + * Deep freeze application config object. Exceptions are made for required + * modules that are listed as dependencies in the application's + * package definition (package.json). Trails takes a firm stance that + * configuration should be modified only by the developer, the environment, + * and the trailpack configuration phase -- never by the application itself + * during runtime. + * + * @param config the configuration object to be frozen. + * @param [pkg] the package definition to use for exceptions. optional. + */ + static freezeConfig (config, modules) { + _.each(config, (prop, name) => { + if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { + return + } + + const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) + if (ignoreModule) { + return + } + Configuration.freezeConfig(prop, modules) + }) + + Object.freeze(config) + } + + /** + * Copy the configuration into a normal, unfrozen object + */ + static unfreezeConfig (config, modules) { + const unfreeze = (target, source) => { + const propNames = Object.getOwnPropertyNames(source) + + propNames.forEach(name => { + const prop = source[name] + + if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { + target[name] = prop + return + } + + const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) + if (ignoreModule) { + return + } + + target[name] = { } + unfreeze(target[name], prop) + }) + + return target + } + return unfreeze({ }, config) + } +} diff --git a/lib/core.js b/lib/core.js index 8fe662e..14ebf57 100644 --- a/lib/core.js +++ b/lib/core.js @@ -3,9 +3,7 @@ const path = require('path') const fs = require('fs') -const joi = require('joi') const _ = require('lodash') -const schemas = require('./schemas') const Core = module.exports = { @@ -95,69 +93,6 @@ const Core = module.exports = { } }, - /** - * Copy and merge the provided configuration into a new object, decorated with - * necessary default and environment-specific values. - */ - buildConfig (initialConfig, nodeEnv) { - const root = path.resolve(path.dirname(require.main.filename)) - const temp = path.resolve(root, '.tmp') - const envConfig = initialConfig.env && initialConfig.env[nodeEnv] - - const configTemplate = { - main: { - maxListeners: 128, - packs: [ ], - paths: { - root: root, - temp: temp, - sockets: path.resolve(temp, 'sockets'), - logs: path.resolve(temp, 'log') - }, - timeouts: { - start: 10000, - stop: 10000 - } - }, - log: { }, - motd: require('./motd') - } - - const mergedConfig = _.merge(configTemplate, initialConfig, (envConfig || { })) - mergedConfig.env = nodeEnv - - return mergedConfig - }, - - /** - * Validate the structure and prerequisites of the configuration object. Throw - * an Error if invalid; invalid configurations are unrecoverable and require - * that programmer fix them. - */ - validateConfig (config) { - if (!config || !config.main) { - throw new ConfigNotDefinedError() - } - - if (!config.log || !config.log.logger) { - throw new LoggerNotDefinedError() - } - - const nestedEnvs = Core.getNestedEnv(config) - if (nestedEnvs.length) { - throw new ConfigValueError('Environment configs cannot contain an "env" property') - } - - if (config.env && config.env.env) { - throw new ConfigValueError('config.env cannot contain an "env" property') - } - - const result = joi.validate(config, schemas.config) - if (result.error) { - console.error(result) - throw new Error('Project Configuration Error:', result.error) - } - }, /** * During config object inspection, we need to determine whether an arbitrary @@ -176,84 +111,6 @@ const Core = module.exports = { }, - /** - * Deep freeze application config object. Exceptions are made for required - * modules that are listed as dependencies in the application's - * package definition (package.json). Trails takes a firm stance that - * configuration should be modified only by the developer, the environment, - * and the trailpack configuration phase -- never by the application itself - * during runtime. - * - * @param config the configuration object to be frozen. - * @param [pkg] the package definition to use for exceptions. optional. - */ - freezeConfig (config, modules) { - const propNames = Object.getOwnPropertyNames(config) - - propNames.forEach(name => { - const prop = config[name] - - if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { - return - } - - const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) - if (ignoreModule) { - return - } - Core.freezeConfig(prop, modules) - }) - - Object.freeze(config) - }, - - /** - * Copy the configuration into a normal, unfrozen object - */ - unfreezeConfig (app, modules) { - const unfreeze = (target, source) => { - const propNames = Object.getOwnPropertyNames(source) - - propNames.forEach(name => { - const prop = source[name] - - if (!prop || typeof prop !== 'object' || prop.constructor !== Object) { - target[name] = prop - return - } - - const ignoreModule = modules.find(moduleId => require.cache[moduleId].exports === prop) - if (ignoreModule) { - return - } - - target[name] = { } - unfreeze(target[name], prop) - }) - - return target - } - - Object.defineProperties(app, { - config: { - value: unfreeze({ }, app.config), - configurable: true - } - }) - }, - - /** - * Check to see if the user defined a property config.env[env].env - */ - getNestedEnv (config) { - const env = (config && config.env) - const nestedEnvs = Object.keys(env || { }).filter(key => { - return !!env[key].env - }) - - return nestedEnvs - }, - /** * Bind listeners various application events */ @@ -276,7 +133,7 @@ const Core = module.exports = { if (app.config.main.freezeConfig !== false) { app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') app.log.warn('Please only use this flag for testing/debugging purposes.') - Core.freezeConfig(app.config, app.loadedModules) + app.freezeConfig() } }) app.once('trailpack:all:initialized', () => { @@ -293,7 +150,7 @@ const Core = module.exports = { app.once('trails:stop', () => { app.log.silly(app.config.motd.silly.stop) app.log.info(app.config.motd.info.stop) - Core.unfreezeConfig(app, app.loadedModules) + app.unfreezeConfig() }) app.once('trails:error:fatal', err => app.stop(err)) }, diff --git a/lib/index.js b/lib/index.js index 530ee97..f187041 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,3 +6,5 @@ exports.Errors = require('./errors') exports.Pathfinder = require('./pathfinder') exports.Core = require('./core') exports.Schemas = require('./schemas') +exports.Configuration = require('./Configuration') +//exports.ConfigProxy = require('./ConfigProxy') diff --git a/lib/schemas/config.js b/lib/schemas/config.js index 2a3024e..30938f4 100644 --- a/lib/schemas/config.js +++ b/lib/schemas/config.js @@ -5,7 +5,8 @@ module.exports = joi.object().keys({ packs: joi.array(), paths: joi.object().keys({ root: joi.string().required() - }).unknown() + }).unknown(), + freezeConfig: joi.bool() }).required().unknown(), env: joi.string().required(), diff --git a/test/lib/context.test.js b/test/lib/Configuration.test.js similarity index 61% rename from test/lib/context.test.js rename to test/lib/Configuration.test.js index 159cb71..87c3c71 100644 --- a/test/lib/context.test.js +++ b/test/lib/Configuration.test.js @@ -1,128 +1,92 @@ 'use strict' -const path = require('path') const assert = require('assert') -const _ = require('lodash') -const smokesignals = require('smokesignals') const lib = require('../../lib') +const smokesignals = require('smokesignals') +const _ = require('lodash') -describe('lib.Core', () => { - describe('#getClassMethods', () => { - const A = class A { - foo () { } - } - const B = class B extends A { - bar () { } - } - const C = class B extends A { - bar () { } - baz () { } - get getter () { - return 'getter' - } - static staticThing () { } - } - it('should return class methods for object', () => { - const methods = lib.Core.getClassMethods(new A(), A) - - assert.equal(methods.length, 1) - assert.equal(methods[0], 'foo') - }) - it('should return class methods for all objects in prototype chain', () => { - const methods = lib.Core.getClassMethods(new B(), B) - - assert.equal(methods.length, 2) - assert(_.includes(methods, 'foo')) - assert(_.includes(methods, 'bar')) - }) - it('should return only *instance methods* and no other type of thing', () => { - const methods = lib.Core.getClassMethods(new C(), C) - - assert.equal(methods.length, 3) - assert(_.includes(methods, 'bar')) - assert(_.includes(methods, 'foo')) - assert(_.includes(methods, 'baz')) - }) - }) - describe('#buildConfig', () => { - const NODE_ENV = process.env.NODE_ENV - let testConfig - - beforeEach(() => { - testConfig = { - env: { - envTest1: { - log: { +describe('lib.Configuration', () => { + const NODE_ENV = process.env.NODE_ENV + let testConfig + + beforeEach(() => { + testConfig = { + env: { + envTest1: { + log: { + merged: 'yes', + extraneous: 'assigned' + }, + customObject: { + string: 'b', + int: 2, + array: [2, 3, 4], + subobj: { + attr: 'b' + } + } + }, + envTest2: { + log: { + nested: { merged: 'yes', extraneous: 'assigned' }, - customObject: { - string: 'b', - int: 2, - array: [2, 3, 4], - subobj: { - attr: 'b' - } - } + merged: 'yes', + extraneous: 'assigned' }, - envTest2: { - log: { - nested: { + customObject: { + subobj: { + attr: 'b' + }, + int2: 2 + } + }, + envTest3: { + log: { + nested: { + merged: 'yes', + extraneous: 'assigned', + deeplyNested: { merged: 'yes', extraneous: 'assigned' - }, - merged: 'yes', - extraneous: 'assigned' + } }, - customObject: { - subobj: { - attr: 'b' - }, - int2: 2 - } - }, - envTest3: { - log: { - nested: { - merged: 'yes', - extraneous: 'assigned', - deeplyNested: { - merged: 'yes', - extraneous: 'assigned' - } - }, - merged: 'yes', - extraneous: 'assigned' - } + merged: 'yes', + extraneous: 'assigned' } - }, - log: { + } + }, + log: { + logger: new smokesignals.Logger('silent'), + merged: 'no', + nested: { merged: 'no', - nested: { - merged: 'no', - deeplyNested: { - merged: 'no' - } - }, - normal: 'yes' - }, - customObject: { - string: 'a', - int: 1, - array: [1, 2, 3], - subobj: { - attr: 'a' + deeplyNested: { + merged: 'no' } + }, + normal: 'yes' + }, + customObject: { + string: 'a', + int: 1, + array: [1, 2, 3], + subobj: { + attr: 'a' } } - }) + } + }) - afterEach(() => { - process.env.NODE_ENV = NODE_ENV - }) + afterEach(() => { + process.env.NODE_ENV = NODE_ENV + }) + + describe('#buildConfig', () => { it('should merge basic env config', () => { - const config = lib.Core.buildConfig(testConfig, 'envTest1') + const config = lib.Configuration.buildConfig(testConfig, 'envTest1') assert(config) assert.equal(config.log.merged, 'yes') @@ -135,7 +99,7 @@ describe('lib.Core', () => { it('should merge nested env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Core.buildConfig(testConfig, 'envTest2') + const config = lib.Configuration.buildConfig(testConfig, 'envTest2') assert(config) assert.equal(config.log.merged, 'yes') @@ -152,7 +116,7 @@ describe('lib.Core', () => { it('should merge deeply nested env config', () => { process.env.NODE_ENV = 'envTest3' - const config = lib.Core.buildConfig(testConfig, 'envTest3') + const config = lib.Configuration.buildConfig(testConfig, 'envTest3') assert(config) assert.equal(config.log.merged, 'yes') @@ -172,7 +136,7 @@ describe('lib.Core', () => { it('should merge full custom env config', () => { process.env.NODE_ENV = 'envTest1' - const config = lib.Core.buildConfig(testConfig, 'envTest1') + const config = lib.Configuration.buildConfig(testConfig, 'envTest1') assert(config) assert(typeof config.customObject === 'object') @@ -186,7 +150,7 @@ describe('lib.Core', () => { it('should merge partial custom env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Core.buildConfig(testConfig, 'envTest2') + const config = lib.Configuration.buildConfig(testConfig, 'envTest2') assert(config) assert(typeof config.customObject === 'object') @@ -200,7 +164,7 @@ describe('lib.Core', () => { it('should merge new custom attr in env config', () => { process.env.NODE_ENV = 'envTest2' - const config = lib.Core.buildConfig(testConfig, 'envTest2') + const config = lib.Configuration.buildConfig(testConfig, 'envTest2') assert(config) assert(typeof config.customObject === 'object') @@ -215,7 +179,7 @@ describe('lib.Core', () => { it('should not override any configs if NODE_ENV matches no env', () => { process.env.NODE_ENV = 'notconfigured' - const config = lib.Core.buildConfig(testConfig, 'notconfigured') + const config = lib.Configuration.buildConfig(testConfig, 'notconfigured') assert(config) assert.equal(config.log.merged, 'no') @@ -228,13 +192,12 @@ describe('lib.Core', () => { it('should keep "env" property from config', () => { process.env.NODE_ENV = 'mergetest2' - const config = lib.Core.buildConfig(testConfig, 'mergetest2') + const config = lib.Configuration.buildConfig(testConfig, 'mergetest2') assert(config.env) }) }) - - describe('#getNestedEnv', () => { - it('should return a list of envs if one contains a "env" property', () => { + describe('#validateConfig', () => { + it('should throw ConfigValueError if an env config contains the "env" property', () => { const testConfig = { main: { }, log: { @@ -242,56 +205,56 @@ describe('lib.Core', () => { }, env: { envtest: { - env: { - invalid: true - } + env: 'hello' } } } - - const nestedEnvs = lib.Core.getNestedEnv(testConfig) - - assert.equal(nestedEnvs[0], 'envtest') - assert.equal(nestedEnvs.length, 1) + assert.throws(() => lib.Configuration.validateConfig(testConfig), lib.Errors.ConfigValueError) + assert.throws(() => lib.Configuration.validateConfig(testConfig), /Environment configs/) }) - }) - - describe('#validateConfig', () => { - it('should throw ConfigValueError if an env config contains the "env" property', () => { + it('should throw ConfigValueError if config.env contains the "env" property', () => { const testConfig = { main: { }, log: { logger: new smokesignals.Logger('silent') }, env: { - envtest: { - env: 'hello' - } + env: 'hello' } } - assert.throws(() => lib.Core.validateConfig(testConfig), lib.Errors.ConfigValueError) - assert.throws(() => lib.Core.validateConfig(testConfig), /Environment configs/) + assert.throws(() => lib.Configuration.validateConfig(testConfig), lib.Errors.ConfigValueError) + assert.throws(() => lib.Configuration.validateConfig(testConfig), /config.env/) }) - it('should throw ConfigValueError if config.env contains the "env" property', () => { + }) + describe('#getNestedEnv', () => { + it('should return a list of envs if one contains a "env" property', () => { const testConfig = { main: { }, log: { logger: new smokesignals.Logger('silent') }, env: { - env: 'hello' + envtest: { + env: { + invalid: true + } + } } } - assert.throws(() => lib.Core.validateConfig(testConfig), lib.Errors.ConfigValueError) - assert.throws(() => lib.Core.validateConfig(testConfig), /config.env/) + + const nestedEnvs = lib.Configuration.getNestedEnv(testConfig) + + assert.equal(nestedEnvs[0], 'envtest') + assert.equal(nestedEnvs.length, 1) }) }) - describe('#freezeConfig', () => { it('should freeze nested object', () => { const o1 = { foo: { bar: 1 } } - lib.Core.freezeConfig(o1, [ ]) + lib.Configuration.freezeConfig(o1, [ ]) + assert(Object.isFrozen(o1)) + assert(Object.isFrozen(o1.foo)) assert.throws(() => o1.foo = null, Error) }) it('should not freeze exernal modules required from config', () => { @@ -299,7 +262,7 @@ describe('lib.Core', () => { foo: require('smokesignals'), bar: 1 } - lib.Core.freezeConfig(o1, [ require.resolve('smokesignals') ]) + lib.Configuration.freezeConfig(o1, [ require.resolve('smokesignals') ]) assert.throws(() => o1.bar = null, Error) @@ -321,14 +284,13 @@ describe('lib.Core', () => { buffer: new Buffer([ 1,2,3 ]), fun: function () { } } - lib.Core.freezeConfig(o1, [ ]) + lib.Configuration.freezeConfig(o1, [ ]) assert(o1.typedArray) assert(Buffer.isBuffer(o1.buffer)) assert(o1.fun) }) }) - describe('#unfreezeConfig', () => { it('should unfreeze shallow config object', () => { const app = { @@ -337,10 +299,10 @@ describe('lib.Core', () => { foo: 'bar' } } - lib.Core.freezeConfig(app.config, [ ]) + lib.Configuration.freezeConfig(app.config, [ ]) assert.throws(() => app.config.a = 2, Error) - lib.Core.unfreezeConfig(app, [ ]) + app.config = lib.Configuration.unfreezeConfig(app.config, [ ]) app.config.a = 2 assert.equal(app.config.a, 2) }) @@ -356,30 +318,54 @@ describe('lib.Core', () => { } } } - lib.Core.freezeConfig(app.config, [ ]) + lib.Configuration.freezeConfig(app.config, [ ]) assert.throws(() => app.config.main.paths.root = 'newrootpath', Error) - lib.Core.unfreezeConfig(app, [ ]) + app.config = lib.Configuration.unfreezeConfig(app.config, [ ]) app.config.main.paths.root = 'newrootpath' assert.equal(app.config.main.paths.root, 'newrootpath') assert.equal(app.config.main.paths.temp, 'temppath') assert.equal(app.config.main.foo, 1) }) }) - - describe('#getExternalModules', () => { - const rmf = require.main.filename - - beforeEach(() => { - require.main.filename = path.resolve(__dirname, '..', '..', 'index.js') + describe('#get', () => { + it('should return nested config value if it exists', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('customObject.string'), 'a') + }) + it('should return undefined if config value does not exist', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('customObject.nobody'), undefined) }) - afterEach(() => { - require.main.filename = rmf + it('should return undefined if any config tree path segment does not exist', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('i.dont.exist'), undefined) }) - it('should return external modules', () => { - const modules = lib.Core.getExternalModules() - assert(modules.indexOf(require.resolve('mocha')) !== -1) + it('should return the nested config object if a path is given to an internal node', () => { + const config = new lib.Configuration(testConfig, { NODE_ENV: 'test' }) + assert.equal(config.get('customObject').string, 'a') }) }) + describe('#set', () => { + it('should set the value of a leaf node', () => { + const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' }) + config.set('customObject.testValue', 'test') + + assert.equal(config.get('customObject.testValue'), 'test') + assert.equal(config.customObject.testValue, 'test') + }) + it('should set the value of a new, nested leaf node with no pre-existing path', () => { + const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' }) + + assert(!config.foo) + config.set('foo.bar.new.path', 'test') + + assert.equal(config.get('foo.bar.new.path'), 'test') + assert.equal(config.foo.bar.new.path, 'test') + assert(_.isPlainObject(config.foo)) + assert(_.isPlainObject(config.foo.bar)) + assert(_.isPlainObject(config.foo.bar.new)) + }) + }) }) diff --git a/test/lib/Core.test.js b/test/lib/Core.test.js new file mode 100644 index 0000000..6f4ebbc --- /dev/null +++ b/test/lib/Core.test.js @@ -0,0 +1,63 @@ +'use strict' + +const path = require('path') +const assert = require('assert') +const _ = require('lodash') +const smokesignals = require('smokesignals') +const lib = require('../../lib') + +describe('lib.Core', () => { + describe('#getClassMethods', () => { + const A = class A { + foo () { } + } + const B = class B extends A { + bar () { } + } + const C = class B extends A { + bar () { } + baz () { } + get getter () { + return 'getter' + } + static staticThing () { } + } + it('should return class methods for object', () => { + const methods = lib.Core.getClassMethods(new A(), A) + + assert.equal(methods.length, 1) + assert.equal(methods[0], 'foo') + }) + it('should return class methods for all objects in prototype chain', () => { + const methods = lib.Core.getClassMethods(new B(), B) + + assert.equal(methods.length, 2) + assert(_.includes(methods, 'foo')) + assert(_.includes(methods, 'bar')) + }) + it('should return only *instance methods* and no other type of thing', () => { + const methods = lib.Core.getClassMethods(new C(), C) + + assert.equal(methods.length, 3) + assert(_.includes(methods, 'bar')) + assert(_.includes(methods, 'foo')) + assert(_.includes(methods, 'baz')) + }) + }) + + describe('#getExternalModules', () => { + const rmf = require.main.filename + + beforeEach(() => { + require.main.filename = path.resolve(__dirname, '..', '..', 'index.js') + }) + afterEach(() => { + require.main.filename = rmf + }) + it('should return external modules', () => { + const modules = lib.Core.getExternalModules() + assert(modules.indexOf(require.resolve('mocha')) !== -1) + }) + }) + +}) From eb613903240a2ed271d9765efbb9e84b5cb3c9dc Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 21 Nov 2016 17:32:11 -0500 Subject: [PATCH 13/52] [pkg] upgrade eslint module to 2.x series --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 855bfd8..db3e109 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ }, "devDependencies": { "eslint": "^2.5.3", - "eslint-config-trails": "^1.0", + "eslint-config-trails": "^2.0", "istanbul": "^0.4.2", "mocha": "^2.3.4", "pre-commit": "^1.1.3", From 55f56e71c726ecd7593b864ce0aff1661367c363 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 21 Nov 2016 20:50:13 -0500 Subject: [PATCH 14/52] [fix] remove syntax unsupported by node4 --- index.js | 6 +++--- lib/Configuration.js | 1 + test/index.js | 16 ++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 007b67e..b263868 100644 --- a/index.js +++ b/index.js @@ -218,9 +218,9 @@ module.exports = class TrailsApp extends EventEmitter { } let resolveCallback - const handlerWrapper = (...args) => { - handler(args) - return args + const handlerWrapper = function () { + handler.apply(null, arguments) + return arguments } return Promise.race(events.map(eventName => { diff --git a/lib/Configuration.js b/lib/Configuration.js index 892781e..c964a47 100644 --- a/lib/Configuration.js +++ b/lib/Configuration.js @@ -1,4 +1,5 @@ /*eslint no-console: 0 */ +'use strict' const _ = require('lodash') const path = require('path') diff --git a/test/index.js b/test/index.js index 7d129f2..e93fc78 100644 --- a/test/index.js +++ b/test/index.js @@ -218,9 +218,9 @@ describe('Trails', () => { }) it('should pass event parameters through to handler', () => { const eventPromise = app.after(['test9', 'test10']) - .then(([ t1, t2 ]) => { - assert.equal(t1, 9) - assert.equal(t2, 10) + .then(results => { + assert.equal(results[0], 9) + assert.equal(results[1], 10) }) app.emit('test9', 9) @@ -229,9 +229,9 @@ describe('Trails', () => { return eventPromise }) it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { - app.after(['test11', 'test12'], ([t1, t2]) => { - assert.equal(t1, 11) - assert.equal(t2, 12) + app.after(['test11', 'test12'], results => { + assert.equal(results[0], 11) + assert.equal(results[1], 12) done() }) app.emit('test11', 11) @@ -247,8 +247,8 @@ describe('Trails', () => { it('should pass event parameters through to handler', () => { const eventPromise = app.onceAny('test1') - .then(t1 => { - assert.equal(t1, 1) + .then(result => { + assert.equal(result[0], 1) }) app.emit('test1', 1) From e7e53e3dc4aff70479b1d9da8422cadef90e0863 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 22 Nov 2016 02:05:39 -0500 Subject: [PATCH 15/52] [fix] nested path creation issue --- index.js | 10 +++++++-- lib/Configuration.js | 1 + lib/core.js | 48 ++++++++++++++++++++++---------------------- lib/trailpack.js | 1 + package.json | 1 + 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index b263868..ef30f5b 100644 --- a/index.js +++ b/index.js @@ -129,7 +129,6 @@ module.exports = class TrailsApp extends EventEmitter { } }) - lib.Core.createDefaultPaths(this) this.setMaxListeners(this.config.main.maxListeners) Object.assign(this.models, lib.Core.bindMethods(this, 'models')) @@ -272,12 +271,19 @@ module.exports = class TrailsApp extends EventEmitter { unfreezeConfig () { Object.defineProperties(this, { config: { - value: this.config.unfreeze(), + value: new lib.Configuration(this.config.unfreeze(), this.env), configurable: true } }) } + createPaths () { + if (this.config.main.createPaths !== true) { + this.log.warn('createPaths is disabled. Configured paths will not be created') + } + return lib.Core.createDefaultPaths(this) + } + /** * Expose the logger on the app object. The logger can be configured by * setting the "config.log.logger" config property. diff --git a/lib/Configuration.js b/lib/Configuration.js index c964a47..5939dff 100644 --- a/lib/Configuration.js +++ b/lib/Configuration.js @@ -10,6 +10,7 @@ module.exports = class Configuration extends Map { constructor (configTree, processEnv) { super() + this.modules = [ ] this.immutable = false this.env = processEnv this.tree = Configuration.buildConfig(configTree, processEnv.NODE_ENV) diff --git a/lib/core.js b/lib/core.js index 14ebf57..4cdb11f 100644 --- a/lib/core.js +++ b/lib/core.js @@ -4,6 +4,7 @@ const path = require('path') const fs = require('fs') const _ = require('lodash') +const mkdirp = require('mkdirp') const Core = module.exports = { @@ -61,38 +62,36 @@ const Core = module.exports = { }, /** - * create paths if they don't exist + * Create configured paths if they don't exist */ createDefaultPaths (app) { const paths = app.config.main.paths - return Promise.all(Object.keys(paths).map(pathName => { - const dir = paths[pathName] - if (Array.isArray(dir)) { - dir.map(item => { - pathCreate(item.path, pathName) + return Promise.all(_.map(paths, (dir, pathName) => { + return new Promise((resolve, reject) => { + fs.stat(dir, (err, stats) => { + resolve({ err, stats }) }) - } - else { - pathCreate(dir, pathName) - } - })) - function pathCreate(dir, pathName) { - try { - const stats = fs.statSync(dir) + }) + .then(result => { + const stats = result.stats - if (!stats.isDirectory()) { - app.log.error('The path "', pathName, '" is not a directory.') + if (stats && !stats.isDirectory()) { + app.log.error('The configured path "', pathName, '" is not a directory.') app.log.error('config.main.paths should only contain paths to directories') return Promise.reject() } - } - catch (e) { - fs.mkdirSync(dir) - } - } - }, + return result + }) + .then(stat => { + if (stat.err && /no such file or directory/.test(stat.err.message)) { + app.log.debug('Trails is creating the path (', pathName, ') at', dir) + } + mkdirp.sync(dir) + }) + })) + }, /** * During config object inspection, we need to determine whether an arbitrary @@ -130,11 +129,12 @@ const Core = module.exports = { */ bindApplicationListeners (app) { app.once('trailpack:all:configured', () => { - if (app.config.main.freezeConfig !== false) { + if (app.config.main.freezeConfig !== true) { app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') app.log.warn('Please only use this flag for testing/debugging purposes.') - app.freezeConfig() } + + app.freezeConfig() }) app.once('trailpack:all:initialized', () => { app.log.silly(app.config.motd.silly.initialized) diff --git a/lib/trailpack.js b/lib/trailpack.js index 2d5e47f..5e143e5 100644 --- a/lib/trailpack.js +++ b/lib/trailpack.js @@ -23,6 +23,7 @@ module.exports = { const initializedEvents = packs.map(pack => `trailpack:${pack.name}:initialized`) app.after(configuredEvents) + .then(() => app.createPaths()) .then(() => app.emit('trailpack:all:configured')) .catch(err => app.stop(err)) diff --git a/package.json b/package.json index db3e109..b9518ff 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "i18next": "3.4.1", "joi": "9.2.0", "lodash": "4.17.2", + "mkdirp": "^0.5.1", "trails-controller": "1.0.0", "trails-model": "1.0.0", "trails-policy": "1.0.1", From bc0b65802972801d6460d689ab6626e7ca955d8f Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 22 Nov 2016 12:56:40 -0500 Subject: [PATCH 16/52] [refactor] remove unused method --- lib/trailpack.js | 10 ---------- test/lib/trailpack.test.js | 25 ++----------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/lib/trailpack.js b/lib/trailpack.js index 5e143e5..d388432 100644 --- a/lib/trailpack.js +++ b/lib/trailpack.js @@ -2,16 +2,6 @@ module.exports = { - /** - * Index trailpacks by name - */ - getTrailpackMapping (packs) { - return packs.reduce((mapping, pack) => { - mapping[pack.name] = pack - return mapping - }, { }) - }, - /** * Bind lifecycle boundary event listeners. That is, when all trailpacks have * completed a particular phase, e.g. "configure" or "initialize", emit an diff --git a/test/lib/trailpack.test.js b/test/lib/trailpack.test.js index 2006464..325131a 100644 --- a/test/lib/trailpack.test.js +++ b/test/lib/trailpack.test.js @@ -12,29 +12,8 @@ describe('lib.Trailpack', () => { before(() => { return app.start(testAppDefinition) }) - - describe('#getTrailpackMapping', () => { - let testTrailpacks - before(() => { - testTrailpacks = [ - new Trailpack(app, { pkg: { name: 'trailpack-pack1' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack2' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack3' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-pack4' }, config: { } }), - new Trailpack(app, { pkg: { name: 'trailpack-core' }, config: { } }) - ] - }) - - it('should index packs by name', () => { - const packs = lib.Trailpack.getTrailpackMapping(testTrailpacks) - - assert(packs.pack1) - assert(packs.pack2) - assert(packs.pack3) - assert(packs.pack4) - assert(packs.core) - }) - + after(() => { + return app.stop() }) }) From aaf94b267a174ba83311b89815f2e9c6c2adefe0 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Fri, 25 Nov 2016 18:17:51 -0500 Subject: [PATCH 17/52] [pkg] remove bundledDependencies --- package.json | 6 +----- test/lib/trailpack.test.js | 19 ------------------- 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 test/lib/trailpack.test.js diff --git a/package.json b/package.json index b9518ff..150aa54 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "i18next": "3.4.1", "joi": "9.2.0", "lodash": "4.17.2", - "mkdirp": "^0.5.1", + "mkdirp": "0.5.1", "trails-controller": "1.0.0", "trails-model": "1.0.0", "trails-policy": "1.0.1", @@ -87,10 +87,6 @@ "winston": "^2.1.1" }, "bundledDependencies": [ - "hoek", - "i18next", - "joi", - "lodash", "trails-controller", "trails-model", "trails-policy", diff --git a/test/lib/trailpack.test.js b/test/lib/trailpack.test.js deleted file mode 100644 index 325131a..0000000 --- a/test/lib/trailpack.test.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const assert = require('assert') -const lib = require('../../lib') -const Trailpack = require('trailpack') -const testAppDefinition = require('../testapp') -const TrailsApp = require('../../') - -describe('lib.Trailpack', () => { - const app = new TrailsApp(testAppDefinition) - - before(() => { - return app.start(testAppDefinition) - }) - after(() => { - return app.stop() - }) -}) - From 2ee42b994be52332a3a1c611224b1c809314e17b Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Fri, 25 Nov 2016 18:19:08 -0500 Subject: [PATCH 18/52] 2.0.0-rc4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 150aa54..ae5a4a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc0", + "version": "2.0.0-rc4", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 0e776ed74c28cadbad13c58fca547dfb14cf912f Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 18:53:44 -0500 Subject: [PATCH 19/52] [ux] improve trailpack logging --- lib/trailpack.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/trailpack.js b/lib/trailpack.js index d388432..d1d51ea 100644 --- a/lib/trailpack.js +++ b/lib/trailpack.js @@ -38,19 +38,22 @@ module.exports = { const lifecycle = pack.config.lifecycle app.after(lifecycle.initialize.listen.concat('trailpack:all:configured')) + .then(() => app.log.debug('trailpack: initializing', pack.name)) .then(() => pack.initialize()) - .then(() => app.emit(`trailpack:${pack.name}:initialized`)) .catch(err => app.stop(err)) + .then(() => app.emit(`trailpack:${pack.name}:initialized`)) app.after(lifecycle.configure.listen.concat('trailpack:all:validated')) + .then(() => app.log.debug('trailpack: configuring', pack.name)) .then(() => pack.configure()) - .then(() => app.emit(`trailpack:${pack.name}:configured`)) .catch(err => app.stop(err)) + .then(() => app.emit(`trailpack:${pack.name}:configured`)) app.after('trails:start') + .then(() => app.log.debug('trailpack: validating', pack.name)) .then(() => pack.validate()) - .then(() => app.emit(`trailpack:${pack.name}:validated`)) .catch(err => app.stop(err)) + .then(() => app.emit(`trailpack:${pack.name}:validated`)) }) } } From db9423b344ee722ae56021f1e2fa84df29188e4b Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:02:03 -0500 Subject: [PATCH 20/52] [fix] correct unintended functionality change --- index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index ef30f5b..af15271 100644 --- a/index.js +++ b/index.js @@ -131,13 +131,15 @@ module.exports = class TrailsApp extends EventEmitter { this.setMaxListeners(this.config.main.maxListeners) + // instatiate trailpacks + this.config.main.packs.forEach(Pack => new Pack(this)) + this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) + + // bind resource methods to 'app' Object.assign(this.models, lib.Core.bindMethods(this, 'models')) Object.assign(this.services, lib.Core.bindMethods(this, 'services')) Object.assign(this.controllers, lib.Core.bindMethods(this, 'controllers')) Object.assign(this.policies, lib.Core.bindMethods(this, 'policies')) - - this.config.main.packs.forEach(Pack => new Pack(this)) - this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) } /** From aa27944bda736616d7c78622f4d1983a13d21907 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:05:17 -0500 Subject: [PATCH 21/52] [fix] correct createPaths warning --- index.js | 2 +- lib/Configuration.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index af15271..7253cbe 100644 --- a/index.js +++ b/index.js @@ -280,7 +280,7 @@ module.exports = class TrailsApp extends EventEmitter { } createPaths () { - if (this.config.main.createPaths !== true) { + if (this.config.main.createPaths === false) { this.log.warn('createPaths is disabled. Configured paths will not be created') } return lib.Core.createDefaultPaths(this) diff --git a/lib/Configuration.js b/lib/Configuration.js index 5939dff..b7da9ee 100644 --- a/lib/Configuration.js +++ b/lib/Configuration.js @@ -73,7 +73,8 @@ module.exports = class Configuration extends Map { start: 10000, stop: 10000 }, - freezeConfig: true + freezeConfig: true, + createPaths: true }, log: { }, motd: require('./motd') From 3a9e28d9979a5d4d4b2a11e067d9f2fc68f7da3b Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:05:36 -0500 Subject: [PATCH 22/52] 2.0.0-rc5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae5a4a7..7f5e055 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc4", + "version": "2.0.0-rc5", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 28d5474ad1fd02025cccfaaed2c905a7049a64fc Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:07:51 -0500 Subject: [PATCH 23/52] [pkg] add publishConfig --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 7f5e055..7de613c 100644 --- a/package.json +++ b/package.json @@ -98,5 +98,8 @@ "engines": { "node": ">= 4.0.0", "npm": ">= 2.14.2" + }, + "publishConfig": { + "tag": "next" } } From 82512a6889877953372869b3d804a59e540a16b4 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:08:32 -0500 Subject: [PATCH 24/52] 2.0.0-rc6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7de613c..7fea608 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc5", + "version": "2.0.0-rc6", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From ba2ba137978a1edce97e606944efe8e05d7e9889 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:14:27 -0500 Subject: [PATCH 25/52] [pkg] upgrade joi --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fea608..a388cfd 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "dependencies": { "hoek": "4.1.0", "i18next": "3.4.1", - "joi": "9.2.0", + "joi": "^10.0.1", "lodash": "4.17.2", "mkdirp": "0.5.1", "trails-controller": "1.0.0", From ba053ac313ebbb2a4c7588b32b2a8668aba0c586 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sat, 26 Nov 2016 19:14:43 -0500 Subject: [PATCH 26/52] 2.0.0-rc7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a388cfd..f247956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc6", + "version": "2.0.0-rc7", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 838fe158bbe42560c659a6969b3c166fab0d7769 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sun, 27 Nov 2016 00:48:33 -0500 Subject: [PATCH 27/52] [archetype] upgrade to trails v2 --- archetype/package.json | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/archetype/package.json b/archetype/package.json index 11bbc25..d664849 100644 --- a/archetype/package.json +++ b/archetype/package.json @@ -6,19 +6,14 @@ ], "main": "index.js", "dependencies": { - "trailpack-core": "^1.0.1", - "trailpack-repl": "^1.1.0", - "trailpack-router": "^1.0.8", - "trails": "^1.1.0", - "trails-controller": "^1.0.0-beta-2", - "trails-model": "^1.0.0-beta-2", - "trails-policy": "^1.0.1", - "trails-service": "1.0.0-beta-2", + "trailpack-repl": "^2", + "trailpack-router": "^2", + "trails": "v2-latest", "winston": "^2.2" }, "devDependencies": { "eslint": "^2.11", - "eslint-config-trails": "latest", + "eslint-config-trails": "^2", "mocha": "^2.5", "supertest": "^1.2" }, From aed6d73b7c28a448d1f13ce23b91b74f21836d53 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sun, 27 Nov 2016 00:48:51 -0500 Subject: [PATCH 28/52] 2.0.0-rc8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f247956..a162a5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc7", + "version": "2.0.0-rc8", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 83ad62e8f70c211b028f04e653f409013a1168ff Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sun, 27 Nov 2016 00:49:46 -0500 Subject: [PATCH 29/52] [archetype] use v2 service require --- archetype/api/services/DefaultService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archetype/api/services/DefaultService.js b/archetype/api/services/DefaultService.js index 564b054..3482c67 100644 --- a/archetype/api/services/DefaultService.js +++ b/archetype/api/services/DefaultService.js @@ -1,6 +1,6 @@ 'use strict' -const Service = require('trails-service') +const Service = require('trails/service') /** * @module DefaultService From 76f55a2e1c6d05d4e442f54d6a4ce9c2080527df Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Sun, 27 Nov 2016 00:50:16 -0500 Subject: [PATCH 30/52] 2.0.0-rc9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a162a5c..ab6c744 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc8", + "version": "2.0.0-rc9", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From ca946aefe9b1836c87653e9509bd3193e6b5bcd5 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 28 Nov 2016 15:24:09 -0500 Subject: [PATCH 31/52] [archetype] remove trailpack-core --- archetype/config/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/archetype/config/main.js b/archetype/config/main.js index e2c184f..543f41c 100644 --- a/archetype/config/main.js +++ b/archetype/config/main.js @@ -16,7 +16,6 @@ module.exports = { * requirements. */ packs: [ - require('trailpack-core'), require('trailpack-repl'), require('trailpack-router'), require('<%- trailpacks %>') From 692e9b9227f2fcd47a6ccafb77f7d49b65effa4d Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 28 Nov 2016 15:24:42 -0500 Subject: [PATCH 32/52] 2.0.0-rc10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab6c744..97b6d4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc9", + "version": "2.0.0-rc10", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 5c91c577989e90ac08ee4bbe2dc6a0ce16c8d766 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 6 Dec 2016 18:12:30 -0500 Subject: [PATCH 33/52] [api] inject resource types into global ns --- archetype/package.json | 6 +- archetype/server.js | 2 +- index.js | 18 +- lib/core.js | 31 ++++ lib/errors.js | 16 ++ test/index.js | 267 +-------------------------- test/integration/index.js | 284 +++++++++++++++++++++++++++++ test/{ => integration}/testapp.js | 0 test/integration/trailpack.test.js | 28 --- test/lib/Core.test.js | 25 ++- 10 files changed, 376 insertions(+), 301 deletions(-) rename test/{ => integration}/testapp.js (100%) delete mode 100644 test/integration/trailpack.test.js diff --git a/archetype/package.json b/archetype/package.json index d664849..008bfc6 100644 --- a/archetype/package.json +++ b/archetype/package.json @@ -6,10 +6,10 @@ ], "main": "index.js", "dependencies": { - "trailpack-repl": "^2", - "trailpack-router": "^2", + "trailpack-repl": "v2-latest", + "trailpack-router": "v2-latest", "trails": "v2-latest", - "winston": "^2.2" + "winston": "^2.3" }, "devDependencies": { "eslint": "^2.11", diff --git a/archetype/server.js b/archetype/server.js index 991ab17..af7e8d9 100644 --- a/archetype/server.js +++ b/archetype/server.js @@ -6,8 +6,8 @@ 'use strict' -const app = require('./') const TrailsApp = require('trails') +const app = require('./') const server = new TrailsApp(app) server.start().catch(err => server.stop(err)) diff --git a/index.js b/index.js index 7253cbe..932fe41 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,8 @@ const lib = require('./lib') const i18next = require('i18next') const NOOP = function () { } -// inject Error types into the global namespace -Object.assign(global, lib.Errors) +// inject Error and Resource types into the global namespace +lib.Core.assignGlobals() /** * The Trails Application. Merges the configuration and API resources @@ -132,7 +132,15 @@ module.exports = class TrailsApp extends EventEmitter { this.setMaxListeners(this.config.main.maxListeners) // instatiate trailpacks - this.config.main.packs.forEach(Pack => new Pack(this)) + this.config.main.packs.forEach(Pack => { + try { + new Pack(this) + } + catch (e) { + console.error('Error loading Trailpack') + console.error(e) + } + }) this.loadedPacks = Object.keys(this.packs).map(name => this.packs[name]) // bind resource methods to 'app' @@ -153,7 +161,9 @@ module.exports = class TrailsApp extends EventEmitter { lib.Trailpack.bindTrailpackMethodListeners(this, this.loadedPacks) i18next.init(this.config.i18n, (err, t) => { - if (err) throw err + if (err) { + this.log.error('Problem loading i18n:', err) + } this.translate = t this.emit('trails:start') diff --git a/lib/core.js b/lib/core.js index 4cdb11f..02a2e9b 100644 --- a/lib/core.js +++ b/lib/core.js @@ -5,6 +5,7 @@ const path = require('path') const fs = require('fs') const _ = require('lodash') const mkdirp = require('mkdirp') +const lib = require('./') const Core = module.exports = { @@ -20,6 +21,36 @@ const Core = module.exports = { 'schema' ], + globals: Object.freeze(Object.assign({ + Service: require('../service'), + Controller: require('../controller'), + Policy: require('../policy'), + Model: require('../model') + }, lib.Errors)), + + globalPropertyOptions: Object.freeze({ + writable: false, + enumerable: false, + configurable: false + }), + + /** + * Prepare the global namespace with required Trails types. Ignore identical + * values already present; fail on non-matching values. + * + * @throw NamespaceConflictError + */ + assignGlobals () { + _.each(lib.Core.globals, (type, name) => { + if (global[name] === type) return + if (global[name] && global[name] !== type) { + throw new lib.Errors.NamespaceConflictError(name, Object.keys(lib.Core.globals)) + } + const descriptor = Object.assign({ value: type }, lib.Core.globalPropertyOptions) + Object.defineProperty(global, name, descriptor) + }) + }, + /** * Bind the context of API resource methods. */ diff --git a/lib/errors.js b/lib/errors.js index e8368e2..4cf60f7 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -140,3 +140,19 @@ exports.GraphCompletenessError = class extends RangeError { this.name = 'GraphCompletenessError' } } + +exports.NamespaceConflictError = class extends Error { + constructor (key, globals) { + super(` + The extant global variable "${key}" conflicts with the value provided by + Trails. + + Trails defines the following variables in the global namespace: + ${globals} + `) + } + + static get name () { + return 'NamespaceConflictError' + } +} diff --git a/test/index.js b/test/index.js index e93fc78..0cd0379 100644 --- a/test/index.js +++ b/test/index.js @@ -1,267 +1,6 @@ -'use strict' - -const assert = require('assert') -const smokesignals = require('smokesignals') const TrailsApp = require('..') -const testAppDefinition = require('./testapp') -const lib = require('../lib') - -global.app = new TrailsApp(require('./integration/app')) -global.app.start() - -describe('Trails', () => { - describe('@TrailsApp', () => { - describe('idempotence', () => { - it('should be able to start and stop many instances in a single node process', () => { - const cycles = [ ] - for (let i = 0; i < 10; ++i) { - cycles.push(new Promise (resolve => { - const app = new TrailsApp(testAppDefinition) - app.start(testAppDefinition) - .then(app => { - assert.equal(app.started, true) - resolve(app) - }) - })) - } - - return Promise.all(cycles) - .then(apps => { - return Promise.all(apps.map(app => { - return app.stop() - })) - }) - .then(apps => { - apps.map(app => { - assert.equal(app.stopped, true) - }) - }) - }) - it('should be able to stop, then start the same app', () => { - const app = new TrailsApp(testAppDefinition) - return app.start(testAppDefinition) - .then(app => { - assert.equal(app.started, true) - return app.stop() - }) - .then(app => { - assert.equal(app.stopped, true) - return app.start(testAppDefinition) - }) - .then(app => { - assert.equal(app.started, true) - return app.stop() - }) - }) - }) - describe('#constructor', () => { - let app - before(() => { - app = new TrailsApp(testAppDefinition) - }) - - describe('typical usage', () => { - it('should be instance of EventEmitter', () => { - assert(app instanceof require('events').EventEmitter) - }) - it('should set max number of event listeners', () => { - assert.equal(app.getMaxListeners(), 128) - }) - it('should set app properties', () => { - assert(app.pkg) - assert(app.config) - assert(app.api) - }) - it('should set NODE_ENV', () => { - assert.equal(process.env.NODE_ENV, 'development') - }) - }) - - describe('errors', () => { - describe('@LoggerNotDefinedError', () => { - it('should throw LoggerNotDefinedError if logger is missing', () => { - const def = { - pkg: { }, - api: { }, - config: { - main: { - paths: { root: __dirname } - } - } - } - assert.throws(() => new TrailsApp(def), lib.Errors.LoggerNotDefinedError) - }) - }) - describe('@ApiNotDefinedError', () => { - it('should throw ApiNotDefinedError if no api definition is provided', () => { - const def = { - pkg: { }, - config: { - main: { - paths: { root: __dirname } - }, - log: { - logger: new smokesignals.Logger('silent') - } - } - } - assert.throws(() => new TrailsApp(def), lib.Errors.ApiNotDefinedError) - }) - }) - describe('@PackageNotDefinedError', () => { - it('should throw PackageNotDefinedError if no pkg definition is provided', () => { - const def = { - config: { - main: { - paths: { root: __dirname } - }, - log: { - logger: new smokesignals.Logger('silent') - } - } - } - assert.throws(() => new TrailsApp(def), lib.Errors.PackageNotDefinedError) - }) - }) - - it('should cache and freeze process.env', () => { - process.env.FOO = 'bar' - const def = { - api: { }, - config: { - main: { }, - log: { - logger: new smokesignals.Logger('silent') - } - }, - pkg: { } - } - const app = new TrailsApp(def) - - assert.equal(process.env.FOO, 'bar') - assert.equal(app.env.FOO, 'bar') - assert.throws(() => app.env.FOO = 1, TypeError) - }) - - it('should freeze config object after trailpacks are loaded', () => { - const def = { - pkg: { }, - api: { }, - config: { - main: { - packs: [ smokesignals.Trailpack ] - }, - log: { logger: new smokesignals.Logger('silent') }, - foo: 'bar' - } - } - const app = new TrailsApp(def) - assert.equal(app.config.foo, 'bar') - - app.start() - return app.after('trailpack:all:configured').then(() => { - assert.equal(app.config.foo, 'bar') - assert.throws(() => app.config.foo = 1, TypeError) - return app.stop() - }) - }) - - it('should disallow re-assignment of config object', () => { - const def = { - pkg: { }, - api: { }, - config: { - main: { - packs: [ smokesignals.Trailpack ] - }, - log: { logger: new smokesignals.Logger('silent') }, - foo: 'bar' - } - } - const app = new TrailsApp(def) - assert.equal(app.config.foo, 'bar') - assert.throws(() => app.config = { }, Error) - }) - }) - }) - - describe('#after', () => { - let app - before(() => { - app = new TrailsApp(testAppDefinition) - }) - - it('should invoke listener when listening for a single event', () => { - const eventPromise = app.after([ 'test1' ]) - app.emit('test1') - return eventPromise - }) - it('should accept a single event as an array or a string', () => { - const eventPromise = app.after('test2') - app.emit('test2') - return eventPromise - }) - it('should invoke listener when listening for multiple events', () => { - const eventPromise = app.after([ 'test3', 'test4', 'test5' ]) - app.emit('test3') - app.emit('test4') - app.emit('test5') - - return eventPromise - }) - it('should invoke listener when listening for multiple possible events', () => { - const eventPromise = app.after([['test6', 'test7'], 'test8']) - app.emit('test6') - app.emit('test8') - - return eventPromise - }) - it('should pass event parameters through to handler', () => { - const eventPromise = app.after(['test9', 'test10']) - .then(results => { - assert.equal(results[0], 9) - assert.equal(results[1], 10) - }) - - app.emit('test9', 9) - app.emit('test10', 10) - - return eventPromise - }) - it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { - app.after(['test11', 'test12'], results => { - assert.equal(results[0], 11) - assert.equal(results[1], 12) - done() - }) - app.emit('test11', 11) - app.emit('test12', 12) - }) - }) - - describe('#onceAny', () => { - let app - before(() => { - app = new TrailsApp(testAppDefinition) - }) - - it('should pass event parameters through to handler', () => { - const eventPromise = app.onceAny('test1') - .then(result => { - assert.equal(result[0], 1) - }) - - app.emit('test1', 1) - return eventPromise - }) - it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { - app.onceAny(['test1', 'test2'], t1 => { - assert.equal(t1, 1) - done() - }) - app.emit('test1', 1) - }) - }) - }) +before(() => { + global.app = new TrailsApp(require('./integration/app')) + return global.app.start() }) diff --git a/test/integration/index.js b/test/integration/index.js index e69de29..086911f 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -0,0 +1,284 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const assert = require('assert') +const smokesignals = require('smokesignals') +const TrailsApp = require('../..') +const testAppDefinition = require('./testapp') +const lib = require('../../lib') + +describe('Trails', () => { + describe('@TrailsApp', () => { + describe('idempotence', () => { + it('should be able to start and stop many instances in a single node process', () => { + const cycles = [ ] + for (let i = 0; i < 10; ++i) { + cycles.push(new Promise (resolve => { + const app = new TrailsApp(testAppDefinition) + app.start(testAppDefinition) + .then(app => { + assert.equal(app.started, true) + resolve(app) + }) + })) + } + + return Promise.all(cycles) + .then(apps => { + return Promise.all(apps.map(app => { + return app.stop() + })) + }) + .then(apps => { + apps.map(app => { + assert.equal(app.stopped, true) + }) + }) + }) + it('should be able to stop, then start the same app', () => { + const app = new TrailsApp(testAppDefinition) + return app.start(testAppDefinition) + .then(app => { + assert.equal(app.started, true) + return app.stop() + }) + .then(app => { + assert.equal(app.stopped, true) + return app.start(testAppDefinition) + }) + .then(app => { + assert.equal(app.started, true) + return app.stop() + }) + }) + }) + describe('#constructor', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + describe('typical usage', () => { + it('should be instance of EventEmitter', () => { + assert(app instanceof require('events').EventEmitter) + }) + it('should set max number of event listeners', () => { + assert.equal(app.getMaxListeners(), 128) + }) + it('should set app properties', () => { + assert(app.pkg) + assert(app.config) + assert(app.api) + }) + it('should set NODE_ENV', () => { + assert.equal(process.env.NODE_ENV, 'development') + }) + }) + + describe('configuration', () => { + it('should validate an api object with arbitrary keys', () => { + assert(global.app.api.customkey) + }) + it('should create missing directories for configured paths', () => { + assert(fs.statSync(path.resolve(__dirname, 'testdir'))) + }) + it('should set paths.temp if not configured explicitly by user', () => { + assert(global.app.config.main.paths.temp) + }) + it('should set paths.logs if not configured explicitly by user', () => { + assert(global.app.config.main.paths.logs) + }) + it('should set paths.sockets if not configured explicitly by user', () => { + assert(global.app.config.main.paths.sockets) + }) + }) + + describe('errors', () => { + describe('@LoggerNotDefinedError', () => { + it('should throw LoggerNotDefinedError if logger is missing', () => { + const def = { + pkg: { }, + api: { }, + config: { + main: { + paths: { root: __dirname } + } + } + } + assert.throws(() => new TrailsApp(def), lib.Errors.LoggerNotDefinedError) + }) + }) + describe('@ApiNotDefinedError', () => { + it('should throw ApiNotDefinedError if no api definition is provided', () => { + const def = { + pkg: { }, + config: { + main: { + paths: { root: __dirname } + }, + log: { + logger: new smokesignals.Logger('silent') + } + } + } + assert.throws(() => new TrailsApp(def), lib.Errors.ApiNotDefinedError) + }) + }) + describe('@PackageNotDefinedError', () => { + it('should throw PackageNotDefinedError if no pkg definition is provided', () => { + const def = { + config: { + main: { + paths: { root: __dirname } + }, + log: { + logger: new smokesignals.Logger('silent') + } + } + } + assert.throws(() => new TrailsApp(def), lib.Errors.PackageNotDefinedError) + }) + }) + + it('should cache and freeze process.env', () => { + process.env.FOO = 'bar' + const def = { + api: { }, + config: { + main: { }, + log: { + logger: new smokesignals.Logger('silent') + } + }, + pkg: { } + } + const app = new TrailsApp(def) + + assert.equal(process.env.FOO, 'bar') + assert.equal(app.env.FOO, 'bar') + assert.throws(() => app.env.FOO = 1, TypeError) + }) + + it('should freeze config object after trailpacks are loaded', () => { + const def = { + pkg: { }, + api: { }, + config: { + main: { + packs: [ smokesignals.Trailpack ] + }, + log: { logger: new smokesignals.Logger('silent') }, + foo: 'bar' + } + } + const app = new TrailsApp(def) + assert.equal(app.config.foo, 'bar') + + app.start() + return app.after('trailpack:all:configured').then(() => { + assert.equal(app.config.foo, 'bar') + assert.throws(() => app.config.foo = 1, TypeError) + return app.stop() + }) + }) + + it('should disallow re-assignment of config object', () => { + const def = { + pkg: { }, + api: { }, + config: { + main: { + packs: [ smokesignals.Trailpack ] + }, + log: { logger: new smokesignals.Logger('silent') }, + foo: 'bar' + } + } + const app = new TrailsApp(def) + assert.equal(app.config.foo, 'bar') + assert.throws(() => app.config = { }, Error) + }) + }) + }) + + describe('#after', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + it('should invoke listener when listening for a single event', () => { + const eventPromise = app.after([ 'test1' ]) + app.emit('test1') + return eventPromise + }) + it('should accept a single event as an array or a string', () => { + const eventPromise = app.after('test2') + app.emit('test2') + return eventPromise + }) + it('should invoke listener when listening for multiple events', () => { + const eventPromise = app.after([ 'test3', 'test4', 'test5' ]) + app.emit('test3') + app.emit('test4') + app.emit('test5') + + return eventPromise + }) + it('should invoke listener when listening for multiple possible events', () => { + const eventPromise = app.after([['test6', 'test7'], 'test8']) + app.emit('test6') + app.emit('test8') + + return eventPromise + }) + it('should pass event parameters through to handler', () => { + const eventPromise = app.after(['test9', 'test10']) + .then(results => { + assert.equal(results[0], 9) + assert.equal(results[1], 10) + }) + + app.emit('test9', 9) + app.emit('test10', 10) + + return eventPromise + }) + it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { + app.after(['test11', 'test12'], results => { + assert.equal(results[0], 11) + assert.equal(results[1], 12) + done() + }) + app.emit('test11', 11) + app.emit('test12', 12) + }) + }) + + describe('#onceAny', () => { + let app + before(() => { + app = new TrailsApp(testAppDefinition) + }) + + it('should pass event parameters through to handler', () => { + const eventPromise = app.onceAny('test1') + .then(result => { + assert.equal(result[0], 1) + }) + + app.emit('test1', 1) + + return eventPromise + }) + it('should accept a callback as the 2nd argument to invoke instead of returning a Promise', done => { + app.onceAny(['test1', 'test2'], t1 => { + assert.equal(t1, 1) + done() + }) + app.emit('test1', 1) + }) + }) + }) +}) diff --git a/test/testapp.js b/test/integration/testapp.js similarity index 100% rename from test/testapp.js rename to test/integration/testapp.js diff --git a/test/integration/trailpack.test.js b/test/integration/trailpack.test.js deleted file mode 100644 index fddcd23..0000000 --- a/test/integration/trailpack.test.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' - -const path = require('path') -const fs = require('fs') -const assert = require('assert') - -describe('Core Trailpack', () => { - describe('#validate', () => { - it('should validate an api object with arbitrary keys', () => { - assert(global.app.api.customkey) - }) - }) - describe('#configure', () => { - it('should create missing directories for configured paths', () => { - assert(fs.statSync(path.resolve(__dirname, 'testdir'))) - }) - it('should set paths.temp if not configured explicitly by user', () => { - assert(global.app.config.main.paths.temp) - }) - it('should set paths.logs if not configured explicitly by user', () => { - assert(global.app.config.main.paths.logs) - }) - it('should set paths.sockets if not configured explicitly by user', () => { - assert(global.app.config.main.paths.sockets) - }) - }) -}) - diff --git a/test/lib/Core.test.js b/test/lib/Core.test.js index 6f4ebbc..919acbd 100644 --- a/test/lib/Core.test.js +++ b/test/lib/Core.test.js @@ -3,7 +3,6 @@ const path = require('path') const assert = require('assert') const _ = require('lodash') -const smokesignals = require('smokesignals') const lib = require('../../lib') describe('lib.Core', () => { @@ -60,4 +59,28 @@ describe('lib.Core', () => { }) }) + describe('#assignGlobals', () => { + it('should assign variables to the global namespace', () => { + lib.Core.assignGlobals() + + assert(global.Service) + assert(Service) + }) + it('global variables should be immutable and error if mutation is attempted', () => { + assert.throws(() => delete global.Service, Error) + assert(global.Service) + assert(Service) + }) + it('should ignore conflicts for identical values', () => { + const s1 = Service + lib.Core.assignGlobals() + lib.Core.assignGlobals() + lib.Core.assignGlobals() + lib.Core.assignGlobals() + + assert(global.Service) + assert(Service) + assert.equal(s1, Service) + }) + }) }) From b340afd9eafdc88cd960ee3ceeeb9bd89a9a65ae Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 6 Dec 2016 18:33:48 -0500 Subject: [PATCH 34/52] 2.0.0-rc11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97b6d4a..ed6cc17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc10", + "version": "2.0.0-rc11", "description": "Modern Web Application Framework for Node.js", "keywords": [ "mvc", From 0688784ab6bedee5a49ca9d80ddb87a2a4595eae Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 6 Dec 2016 21:50:04 -0500 Subject: [PATCH 35/52] [pkg] update devDependencies to v2-latest --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ed6cc17..0d86857 100644 --- a/package.json +++ b/package.json @@ -82,8 +82,8 @@ "istanbul": "^0.4.2", "mocha": "^2.3.4", "pre-commit": "^1.1.3", - "smokesignals": "^1.2.0", - "trailpack": "^1.0.1", + "smokesignals": "v2-latest", + "trailpack": "v2-latest", "winston": "^2.1.1" }, "bundledDependencies": [ From 361420f9bf668cefe9dc6036026ae59ac905b520 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 6 Dec 2016 22:43:05 -0500 Subject: [PATCH 36/52] [test] removed smokesignals.Trailpack refs --- lib/Configuration.js | 4 +- test/integration/app.js | 3 +- test/integration/index.js | 5 +- test/integration/testapp.js | 6 +- test/lib/pathfinder.test.js | 210 +++++++++++++++++++++--------------- 5 files changed, 138 insertions(+), 90 deletions(-) diff --git a/lib/Configuration.js b/lib/Configuration.js index b7da9ee..6bb7729 100644 --- a/lib/Configuration.js +++ b/lib/Configuration.js @@ -111,8 +111,8 @@ module.exports = class Configuration extends Map { const result = joi.validate(config, schemas.config) if (result.error) { - console.error(result) - throw new Error('Project Configuration Error:', result.error) + console.error(result.error) + throw new Error(`Project Configuration Error: ${result.error}`) } } diff --git a/test/integration/app.js b/test/integration/app.js index 93ffee4..2ef233f 100644 --- a/test/integration/app.js +++ b/test/integration/app.js @@ -1,6 +1,7 @@ const path = require('path') const _ = require('lodash') const smokesignals = require('smokesignals') +const Trailpack = require('trailpack') const AppConfigLocales = { en: { @@ -27,7 +28,7 @@ const App = { config: { main: { packs: [ - smokesignals.Trailpack + Trailpack ], paths: { testdir: path.resolve(__dirname, 'testdir') diff --git a/test/integration/index.js b/test/integration/index.js index 086911f..323851c 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -5,6 +5,7 @@ const path = require('path') const assert = require('assert') const smokesignals = require('smokesignals') const TrailsApp = require('../..') +const Trailpack = require('trailpack') const testAppDefinition = require('./testapp') const lib = require('../../lib') @@ -166,7 +167,7 @@ describe('Trails', () => { api: { }, config: { main: { - packs: [ smokesignals.Trailpack ] + packs: [ Trailpack ] }, log: { logger: new smokesignals.Logger('silent') }, foo: 'bar' @@ -189,7 +190,7 @@ describe('Trails', () => { api: { }, config: { main: { - packs: [ smokesignals.Trailpack ] + packs: [ Trailpack ] }, log: { logger: new smokesignals.Logger('silent') }, foo: 'bar' diff --git a/test/integration/testapp.js b/test/integration/testapp.js index 513a03b..7dd92df 100644 --- a/test/integration/testapp.js +++ b/test/integration/testapp.js @@ -1,4 +1,5 @@ const smokesignals = require('smokesignals') +const Trailpack = require('trailpack') module.exports = { api: { @@ -8,7 +9,10 @@ module.exports = { main: { paths: { root: __dirname - } + }, + packs: [ + Trailpack + ] }, log: { logger: new smokesignals.Logger('silent') diff --git a/test/lib/pathfinder.test.js b/test/lib/pathfinder.test.js index 3d73ddf..d0e7982 100644 --- a/test/lib/pathfinder.test.js +++ b/test/lib/pathfinder.test.js @@ -1,8 +1,9 @@ 'use strict' +const EventEmitter = require('events').EventEmitter const assert = require('assert') const lib = require('../../lib') -const smokesignals = require('smokesignals') +const Trailpack = require('trailpack') describe('lib.Pathfinder', () => { describe('#getPathErrors', () => { @@ -121,27 +122,38 @@ describe('lib.Pathfinder', () => { }) }) describe('#getEventProducer', () => { + const app = new EventEmitter() const packs = [ - new smokesignals.Trailpack(new smokesignals.TrailsApp(), { - trailpack: { - lifecycle: { - configure: { - emit: [ 'pack1:configured', 'pack1:custom' ] - }, - initialize: { - emit: [ 'pack1:initialized', 'pack1:custom' ] + new Trailpack(app, { + pkg: { + name: 'pack1' + }, + config: { + trailpack: { + lifecycle: { + configure: { + emit: [ 'pack1:configured', 'pack1:custom' ] + }, + initialize: { + emit: [ 'pack1:initialized', 'pack1:custom' ] + } } } } }), - new smokesignals.Trailpack(new smokesignals.TrailsApp(), { - trailpack: { - lifecycle: { - configure: { - emit: [ 'pack2:configured' ] - }, - initialize: { - emit: [ 'pack2:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack2' + }, + config: { + trailpack: { + lifecycle: { + configure: { + emit: [ 'pack2:configured' ] + }, + initialize: { + emit: [ 'pack2:initialized' ] + } } } } @@ -162,98 +174,128 @@ describe('lib.Pathfinder', () => { }) describe('Lifecycle', () => { - const app = new smokesignals.TrailsApp() + const app = new EventEmitter() const packs = [ - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ ], - emit: [ 'pack0:configured' ] - }, - initialize: { - listen: [ ], - emit: [ 'pack0:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack0' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ ], + emit: [ 'pack0:configured' ] + }, + initialize: { + listen: [ ], + emit: [ 'pack0:initialized' ] + } } } } - }, 'pack0'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack0:configured' ], - emit: [ 'pack1:configured' ] - }, - initialize: { - emit: [ 'pack1:initialized', 'pack1:custom' ] + new Trailpack(app, { + pkg: { + name: 'pack1' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack0:configured' ], + emit: [ 'pack1:configured' ] + }, + initialize: { + emit: [ 'pack1:initialized', 'pack1:custom' ] + } } } } - }, 'pack1'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack1:configured' ], - emit: [ 'pack2:configured' ] - }, - initialize: { - listen: [ 'pack1:initialized', 'pack1:custom' ], - emit: [ 'pack2:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack2' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack1:configured' ], + emit: [ 'pack2:configured' ] + }, + initialize: { + listen: [ 'pack1:initialized', 'pack1:custom' ], + emit: [ 'pack2:initialized' ] + } } } } - }, 'pack2'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack2:configured' ], - emit: [ 'pack3:configured' ] - }, - initialize: { - listen: [ 'pack2:initialized', 'pack1:custom' ], - emit: [ 'pack3:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack3' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack2:configured' ], + emit: [ 'pack3:configured' ] + }, + initialize: { + listen: [ 'pack2:initialized', 'pack1:custom' ], + emit: [ 'pack3:initialized' ] + } } } } - }, 'pack3'), + }), - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - // dependency with no route to source - configure: { - listen: [ 'packX:configured' ], - emit: [ 'pack4:configured' ] - }, - // dependency on pack with circular dependency - initialize: { - listen: [ 'pack5:initialized', 'pack0:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack4' + }, + config: { + trailpack: { + lifecycle: { + // dependency with no route to source + configure: { + listen: [ 'packX:configured' ], + emit: [ 'pack4:configured' ] + }, + // dependency on pack with circular dependency + initialize: { + listen: [ 'pack5:initialized', 'pack0:initialized' ] + } } } } - }, 'pack4'), + }), // circular dependency - new smokesignals.Trailpack(app, { - trailpack: { - lifecycle: { - configure: { - listen: [ 'pack5:configured' ], - emit: [ 'pack5:configured' ] - }, - initialize: { - listen: [ 'pack4:initialized' ], - emit: [ 'pack5:initialized' ] + new Trailpack(app, { + pkg: { + name: 'pack5' + }, + config: { + trailpack: { + lifecycle: { + configure: { + listen: [ 'pack5:configured' ], + emit: [ 'pack5:configured' ] + }, + initialize: { + listen: [ 'pack4:initialized' ], + emit: [ 'pack5:initialized' ] + } } } } - }, 'pack5') + }) ] describe('#getLifecyclePath', () => { From ec3eeb3c6908f263eaaef824a2216e49a6cfdf60 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 14:07:05 -0500 Subject: [PATCH 37/52] [refactor] use static getter for Error name prop --- lib/errors.js | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/errors.js b/lib/errors.js index 4cf60f7..c25b07c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -18,6 +18,10 @@ exports.ConfigNotDefinedError = class extends RangeError { - https://git.io/vw84F `) } + + static get name () { + return 'ConfigNotDefinedError' + } } exports.LoggerNotDefinedError = class extends RangeError { @@ -37,7 +41,10 @@ exports.LoggerNotDefinedError = class extends RangeError { For more info, see the config.log archetype: https://git.io/vVvUI `) - this.name = 'LoggerNotDefinedError' + } + + static get name () { + return 'LoggerNotDefinedError' } } @@ -68,15 +75,22 @@ exports.ApiNotDefinedError = class extends RangeError { - https://git.io/vw845 - https://git.io/vw84F `) - this.name = 'ApiNotDefinedError' + } + + static get name () { + return 'ApiNotDefinedError' } } exports.ConfigValueError = class extends RangeError { constructor(msg) { super(msg) - this.name = 'ConfigValueError' } + + static get name () { + return 'ConfigValueError' + } + } exports.PackageNotDefinedError = class extends RangeError { @@ -94,14 +108,20 @@ exports.PackageNotDefinedError = class extends RangeError { - https://git.io/vw845 - https://git.io/vw84F `) - this.name = 'PackageNotDefinedError' + } + + static get name () { + return 'PackageNotDefinedError' } } exports.IllegalAccessError = class extends Error { constructor(msg) { super(msg) - this.name = 'IllegalAccessError' + } + + static get name () { + return 'IllegalAccessError' } } @@ -110,8 +130,10 @@ exports.TimeoutError = class extends Error { super(` Timeout during "${phase}". Exceeded configured timeout of ${timeout}ms `) + } - this.name = 'TimeoutError' + static get name () { + return 'TimeoutError' } } @@ -137,7 +159,10 @@ exports.GraphCompletenessError = class extends RangeError { https://github.com/trailsjs/trails/issues. `) - this.name = 'GraphCompletenessError' + } + + static get name () { + return 'GraphCompletenessError' } } From 3981babe7db6efbd9d373393577ef585a9cdd9e8 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 14:28:29 -0500 Subject: [PATCH 38/52] [test] update v8 4460 test --- test/lib/Configuration.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/lib/Configuration.test.js b/test/lib/Configuration.test.js index 87c3c71..9cf862a 100644 --- a/test/lib/Configuration.test.js +++ b/test/lib/Configuration.test.js @@ -271,13 +271,19 @@ describe('lib.Configuration', () => { }) // https://bugs.chromium.org/p/v8/issues/detail?id=4460 - if (!/^v6/.test(process.version)) { - it('v8 issue 4460 exists', () => { + if (/^(v4)|(v5)/.test(process.version)) { + it('v8 issue 4460 exists in node v4, v5 series (cannot naively freeze Int8Aray)', () => { assert.throws(() => Object.freeze(new Int8Array()), TypeError) //assert.throws(() => Object.freeze(new Buffer([1,2,3])), TypeError) //assert.throws(() => Object.freeze(new DataView()), TypeError) }) } + else { + it('v8 issue 4460 is resolved (node 6 and newer)', () => { + assert(true) + }) + } + it('should freeze objects containing unfreezable types without error', () => { const o1 = { typedArray: new Int8Array(), From 89d23c1a265655ca5186500d67f8e79f5aecdf8f Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 14:49:42 -0500 Subject: [PATCH 39/52] [test] fix naked trailpack test issues --- test/integration/app.js | 4 ++-- test/integration/index.js | 6 +++--- test/integration/testapp.js | 4 ++-- test/integration/testpack.js | 11 +++++++++++ 4 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 test/integration/testpack.js diff --git a/test/integration/app.js b/test/integration/app.js index 2ef233f..dc11c4d 100644 --- a/test/integration/app.js +++ b/test/integration/app.js @@ -1,7 +1,7 @@ const path = require('path') const _ = require('lodash') const smokesignals = require('smokesignals') -const Trailpack = require('trailpack') +const Testpack = require('./testpack') const AppConfigLocales = { en: { @@ -28,7 +28,7 @@ const App = { config: { main: { packs: [ - Trailpack + Testpack ], paths: { testdir: path.resolve(__dirname, 'testdir') diff --git a/test/integration/index.js b/test/integration/index.js index 323851c..e77d960 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -5,7 +5,7 @@ const path = require('path') const assert = require('assert') const smokesignals = require('smokesignals') const TrailsApp = require('../..') -const Trailpack = require('trailpack') +const Testpack = require('./testpack') const testAppDefinition = require('./testapp') const lib = require('../../lib') @@ -167,7 +167,7 @@ describe('Trails', () => { api: { }, config: { main: { - packs: [ Trailpack ] + packs: [ Testpack ] }, log: { logger: new smokesignals.Logger('silent') }, foo: 'bar' @@ -190,7 +190,7 @@ describe('Trails', () => { api: { }, config: { main: { - packs: [ Trailpack ] + packs: [ Testpack ] }, log: { logger: new smokesignals.Logger('silent') }, foo: 'bar' diff --git a/test/integration/testapp.js b/test/integration/testapp.js index 7dd92df..c81919c 100644 --- a/test/integration/testapp.js +++ b/test/integration/testapp.js @@ -1,5 +1,5 @@ const smokesignals = require('smokesignals') -const Trailpack = require('trailpack') +const Testpack = require('./testpack') module.exports = { api: { @@ -11,7 +11,7 @@ module.exports = { root: __dirname }, packs: [ - Trailpack + Testpack ] }, log: { diff --git a/test/integration/testpack.js b/test/integration/testpack.js new file mode 100644 index 0000000..3648408 --- /dev/null +++ b/test/integration/testpack.js @@ -0,0 +1,11 @@ +const Trailpack = require('trailpack') + +module.exports = class Testpack extends Trailpack { + constructor (app) { + super(app, { + pkg: { + name: 'testpack' + } + }) + } +} From 2c949e785d82d26136d65694cfa0db1e380a7392 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 14:55:23 -0500 Subject: [PATCH 40/52] [test] hardened getExternalModules test --- test/lib/Core.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/Core.test.js b/test/lib/Core.test.js index 919acbd..5f69b7a 100644 --- a/test/lib/Core.test.js +++ b/test/lib/Core.test.js @@ -55,7 +55,7 @@ describe('lib.Core', () => { }) it('should return external modules', () => { const modules = lib.Core.getExternalModules() - assert(modules.indexOf(require.resolve('mocha')) !== -1) + assert.notEqual(modules.indexOf(require.resolve('trailpack')), -1) }) }) From a3059794c03530f60324d278c921300dce6aaa4b Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 15:12:30 -0500 Subject: [PATCH 41/52] [fix] strict mode for node4 --- test/integration/testpack.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/testpack.js b/test/integration/testpack.js index 3648408..4b5d088 100644 --- a/test/integration/testpack.js +++ b/test/integration/testpack.js @@ -1,3 +1,5 @@ +'use strict' + const Trailpack = require('trailpack') module.exports = class Testpack extends Trailpack { From 967afc459de0a4a6ca4f4390745ff09c32b7170f Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 15:34:35 -0500 Subject: [PATCH 42/52] [doc] update README --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6799839..d5b5e35 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,14 @@ capabilities and allow developers to leverage existing ecosystem tools through a simple and well-defined API. New features, behavior, APIs, and other functionality can be added to the Trails framework through Trailpacks. -Out of the box, Trails includes a small suite of trailpacks: +Many Trails installations will include some of the following Trailpacks: -- [core](https://github.com/trailsjs/trailpack-core) - [router](https://github.com/trailsjs/trailpack-router) - [repl](https://github.com/trailsjs/trailpack-repl) - [hapi](https://github.com/trailsjs/trailpack-hapi) +- [express](https://github.com/trailsjs/trailpack-express) - [waterline](https://github.com/trailsjs/trailpack-waterline) +- [knex](https://github.com/trailsjs/trailpack-knex) ## Compatibility @@ -86,7 +87,9 @@ Out of the box, Trails includes a small suite of trailpacks: - [Getting Started with Trails.js](https://www.youtube.com/watch?v=AbSp8jqFDAY) #### Support -- [Gitter chat room](https://gitter.im/trailsjs/trails) +- [Stackoverflow](http://stackoverflow.com/questions/tagged/trailsjs) +- [Live Gitter Chat](https://gitter.im/trailsjs/trails) +- [Twitter](https://twitter.com/trailsjs) ## FAQ From a05e228cbdb278e184fe6614200b433e04124382 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 15:38:46 -0500 Subject: [PATCH 43/52] [ci] integrate codeclimate coverage --- .travis.yml | 4 ++++ package.json | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca9e39a..f87f358 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,10 @@ script: - npm test - npm run ci +after_script: + - npm install -g codeclimate-test-reporter + - codeclimate-test-reporter < coverage/lcov.info + notifications: email: false webhooks: diff --git a/package.json b/package.json index 0d86857..66d7985 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "graphql" ], "scripts": { - "test": "eslint . && mocha", - "coverage": "istanbul cover _mocha", + "test": "eslint . && istanbul cover _mocha -- -R spec", "ci": "cd .. && ci" }, "pre-commit": [ From 74430e5f8f779d4cd4207a6c97da5e2fb757486e Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Thu, 8 Dec 2016 15:39:04 -0500 Subject: [PATCH 44/52] [ci] quite down duplication warnings --- .codeclimate.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..0dc8dc2 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,5 @@ +engines: + eslint: + enabled: true + duplication: + enabled: false From 757687320fc2b681e712556499a3ec830c6cb64a Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Fri, 9 Dec 2016 13:46:47 -0500 Subject: [PATCH 45/52] [archetype] remove whitespace --- archetype/config/i18n.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/archetype/config/i18n.js b/archetype/config/i18n.js index 2e67af5..34f0ce8 100644 --- a/archetype/config/i18n.js +++ b/archetype/config/i18n.js @@ -5,9 +5,7 @@ * If your app will touch people from all over the world, i18n (or internationalization) * may be an important part of your international strategy. * - * * @see http://trailsjs.io/doc/config/i18n - * */ 'use strict' From 957577872123b8ed38c3a9748b6b35ff49cc48fb Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Fri, 9 Dec 2016 16:49:50 -0500 Subject: [PATCH 46/52] [pkg] simplify test command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66d7985..6ee4ded 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "graphql" ], "scripts": { - "test": "eslint . && istanbul cover _mocha -- -R spec", + "test": "eslint . && istanbul cover _mocha", "ci": "cd .. && ci" }, "pre-commit": [ From 2c78b3822fa70286cda03e10b25eddc21bceebfb Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Fri, 9 Dec 2016 17:13:41 -0500 Subject: [PATCH 47/52] [pkg] remove eslintignore, use gitignore --- .eslintignore | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 4ebc8ae..0000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -coverage diff --git a/package.json b/package.json index 6ee4ded..ffff884 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "graphql" ], "scripts": { - "test": "eslint . && istanbul cover _mocha", + "test": "eslint --ignore-path .gitignore . && istanbul cover _mocha", "ci": "cd .. && ci" }, "pre-commit": [ From 8ad48ad92b3d64cd365a2341f30b97e388cb5a90 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Tue, 13 Dec 2016 16:44:07 -0500 Subject: [PATCH 48/52] [pkg] try to work with windows --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffff884..12414f6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "graphql" ], "scripts": { - "test": "eslint --ignore-path .gitignore . && istanbul cover _mocha", + "test": "eslint --ignore-path .gitignore . && istanbul cover node_modules/mocha/bin/_mocha", "ci": "cd .. && ci" }, "pre-commit": [ From 587a10aa927d7011cc1e9d909c57f4580523038a Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Wed, 21 Dec 2016 12:02:01 -0500 Subject: [PATCH 49/52] [pkg] keywords were getting out of hand --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index 8f9aaeb..ffc4e8c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "2.0.0-rc11", "description": "Modern Web Application Framework for Node.js", "keywords": [ - "mvc", "framework", "platform", "rest", @@ -13,10 +12,6 @@ "sails", "trails", "trailsjs", - "realtime", - "hapi", - "i18n", - "orm", "server", "graphql" ], From bafa56a13c0daae0ff9cc97633379aee43cfe058 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Wed, 21 Dec 2016 12:10:33 -0500 Subject: [PATCH 50/52] [doc] add method documentation --- index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/index.js b/index.js index 932fe41..e3c3a96 100644 --- a/index.js +++ b/index.js @@ -160,6 +160,7 @@ module.exports = class TrailsApp extends EventEmitter { lib.Trailpack.bindTrailpackPhaseListeners(this, this.loadedPacks) lib.Trailpack.bindTrailpackMethodListeners(this, this.loadedPacks) + // initialize i18n i18next.init(this.config.i18n, (err, t) => { if (err) { this.log.error('Problem loading i18n:', err) @@ -276,10 +277,16 @@ module.exports = class TrailsApp extends EventEmitter { .then(handlerWrapper) } + /** + * Prevent changes to the app configuration + */ freezeConfig () { this.config.freeze(this.loadedModules) } + /** + * Allow changes to the app configuration + */ unfreezeConfig () { Object.defineProperties(this, { config: { @@ -289,6 +296,9 @@ module.exports = class TrailsApp extends EventEmitter { }) } + /** + * Create any configured paths which may not already exist. + */ createPaths () { if (this.config.main.createPaths === false) { this.log.warn('createPaths is disabled. Configured paths will not be created') From 8d8edfeb78b0a58c2619bb30d4aea51e6305c078 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Wed, 21 Dec 2016 20:43:11 -0500 Subject: [PATCH 51/52] [pkg] use bumped v2-series trails types --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ffc4e8c..f277a25 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,10 @@ "joi": "^10.0.1", "lodash": "4.17.2", "mkdirp": "0.5.1", - "trails-controller": "1.0.0", - "trails-model": "1.0.0", - "trails-policy": "1.0.1", - "trails-service": "1.0.0" + "trails-controller": "^2", + "trails-model": "^2", + "trails-policy": "^2", + "trails-service": "^2" }, "devDependencies": { "eslint": "^2.5.3", From 62ee391c3867667c214827c79e453713c0057e71 Mon Sep 17 00:00:00 2001 From: "Travis J. Webb" Date: Mon, 26 Dec 2016 21:52:56 -0500 Subject: [PATCH 52/52] 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f277a25..e45547a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trails", - "version": "2.0.0-rc11", + "version": "2.0.0", "description": "Modern Web Application Framework for Node.js", "keywords": [ "framework",