trailsjs/trails

View on GitHub
index.js

Summary

Maintainability
B
4 hrs
Test Coverage
/*eslint no-process-env: 0, no-console: 0 */
const merge = require('lodash.merge')
const EventEmitter = require('events').EventEmitter
const lib = require('./lib')

// inject Error and Resource types into the global namespace
lib.Core.assignGlobals()

/**
 * The Trails Application. Merges the configuration and API resources
 * loads Trailpacks, initializes logging and event listeners.
 */
module.exports = class TrailsApp extends EventEmitter {

  /**
   * @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.
   * @param app config to create Trails instance
   */
  constructor(app) {
    super()

    if (!app) {
      throw new RangeError('No app definition provided to Trails constructor')
    }
    if (!app.pkg) {
      throw new PackageNotDefinedError()
    }
    if (!app.api) {
      throw new ApiNotDefinedError()
    }

    app.api.models || (app.api.models = {})
    app.api.services || (app.api.services = {})
    app.api.resolvers || (app.api.resolvers = {})
    app.api.policies || (app.api.policies = {})
    app.api.controllers || (app.api.controllers = {})

    if (!process.env.NODE_ENV) {
      process.env.NODE_ENV = 'development'
    }

    const processEnv = Object.freeze(JSON.parse(JSON.stringify(process.env)))

    Object.defineProperties(this, {
      logger: {
        value: new lib.LoggerProxy(this),
        enumerable: false,
        writable: false
      },
      env: {
        enumerable: false,
        value: processEnv
      },
      pkg: {
        enumerable: false,
        value: app.pkg
      },
      versions: {
        enumerable: false,
        writable: false,
        configurable: false,
        value: process.versions
      },
      api: {
        value: app.api,
        writable: true,
        configurable: true
      },
      _trails: {
        enumerable: false,
        value: require('./package')
      },
      packs: {
        value: {}
      }
    })

    let trailpacksConfig = {}
    // instantiate trailpacks
    if (app.config.main && app.config.main.packs) {
      app.config.main.packs.forEach(Pack => {
        try {
          const pack = new Pack(this)
          this.packs[pack.name] = pack
          trailpacksConfig = merge(trailpacksConfig, pack.config)
          lib.Core.mergeApi(this, pack)
          lib.Core.bindTrailpackMethodListeners(this, pack)
        }
        catch (e) {
          console.log(e.stack)
          throw new TrailpackError(Pack, e, 'constructor')
        }
      })
    }

    Object.defineProperties(this, {
      config: {
        value: new lib.Configuration(trailpacksConfig, processEnv),
        configurable: true,
        writable: false
      }
    })


    this.config.merge(app.config)

    const envConfig = app.config.env && app.config.env[processEnv.NODE_ENV]
    if (envConfig) {
      this.config.merge(envConfig)
    }

    this.setMaxListeners(this.config.get('main.maxListeners'))

    // instantiate resource classes and bind resource methods
    this.controllers = lib.Core.bindMethods(this, 'controllers')
    this.policies = lib.Core.bindMethods(this, 'policies')
    this.services = lib.Core.bindMethods(this, 'services')
    this.models = lib.Core.bindMethods(this, 'models')
    this.resolvers = lib.Core.bindMethods(this, 'resolvers')

    this.emit('trails:configured')

    lib.Core.bindApplicationListeners(this)
    lib.Core.bindTrailpackPhaseListeners(this, Object.values(this.packs))
  }

  /**
   * Start the App. Load all Trailpacks.
   *
   * @return Promise
   */
  async start() {
    this.emit('trails:start')
    await this.after('trails:ready')
    return this
  }

  /**
   * Shutdown. Unbind listeners, unload trailpacks.
   * @return Promise
   */
  async stop() {
    this.emit('trails:stop')

    await Promise
      .all(Object.values(this.packs).map(pack => {
        this.log.debug('Unloading trailpack', pack.name, '...')
        return pack.unload()
      }))
      .then(() => {
        this.log.debug('All trailpacks unloaded. Done.')
        this.removeAllListeners()
      })

    return this
  }

  /**
   * Resolve Promise once ANY of the events in the list have emitted.
   *
   * @return Promise
   */
  async onceAny(events) {
    if (!Array.isArray(events)) {
      events = [events]
    }

    let resolveCallback

    return Promise
      .race(events.map(eventName => {
        return new Promise(resolve => {
          resolveCallback = resolve
          this.once(eventName, resolveCallback)
        })
      }))
      .then((...args) => {
        events.forEach(eventName => this.removeListener(eventName, resolveCallback))
        return args
      })
      .catch(err => {
        this.log.error(err, 'handling onceAny events', events)
        throw err
      })
  }

  /**
   * Resolve Promise once all events in the list have emitted. Also accepts
   *
   * a callback.
   * @return Promise
   */
  async after(events) {
    if (!Array.isArray(events)) {
      events = [events]
    }

    return Promise
      .all(events.map(eventName => {
        return new Promise(resolve => {
          if (eventName instanceof Array) {
            resolve(this.onceAny(eventName))
          }
          else {
            this.once(eventName, resolve)
          }
        })
      }))
      .catch(err => {
        this.log.error(err, 'handling after events', events)
        throw err
      })
  }

  /**
   * Return the Trails logger
   * @fires trails:log:* log events
   */
  get log() {
    return this.logger
  }
}