laundree/laundree

View on GitHub
src/handlers/handler.js

Summary

Maintainability
A
0 mins
Test Coverage
// @flow
import * as regex from '../utils/regex'
import EventEmitter from 'events'
import { linkEmitter } from '../db/redis'
import Debug from 'debug'
import type { ObjectId, Model, QueryOptions, QueryConditions } from 'mongoose'
import type { Resource } from 'laundree-sdk/lib/sdk'
import type { Action } from 'laundree-sdk/lib/redux'
import { longIdToShort } from '../utils/string'

const debug = Debug('laundree.handlers.handler')

// eslint-disable-next-line no-use-before-define
type ReduxActionCreator<A> = (a: Handler<*, A, *> | string) => ?Action

type ActionCreators<A> = {
  create?: ReduxActionCreator<A>,
  delete?: ReduxActionCreator<A>,
  update?: ReduxActionCreator<A>
}

export class HandlerLibrary<ReduxModel: {}, M: Model<*>, RestModel: Resource, H: Handler<M, ReduxModel, RestModel>> {
  _Handler: Class<H>
  _Model: Class<M>
  subEmitter: EventEmitter = new EventEmitter()
  pubEmitter: EventEmitter = new EventEmitter()
  redux: EventEmitter

  constructor (_Handler: Class<H>, _Model: Class<M>, actionCreators: ActionCreators<ReduxModel> = {}) {
    this._Handler = _Handler
    this._Model = _Model
    linkEmitter(
      this.subEmitter,
      this.pubEmitter,
      _Handler.name,
      ['create', 'update'],
      (instance) => Promise.resolve(instance.model.id),
      id => this.findFromId(id))
    linkEmitter(
      this.subEmitter,
      this.pubEmitter,
      _Handler.name,
      ['delete'],
      (instance) => Promise.resolve(instance.model.id),
      id => Promise.resolve(id))
    this.redux = this._buildReduxEventEmitter(actionCreators)
  }

  _buildReduxEventEmitter (actionCreators: ActionCreators<ReduxModel>) {
    const emitter = new EventEmitter()
    if (actionCreators.delete) this._setupListener(emitter, 'delete', actionCreators.delete)
    if (actionCreators.create) this._setupListener(emitter, 'create', actionCreators.create)
    if (actionCreators.update) this._setupListener(emitter, 'update', actionCreators.update)
    return emitter
  }

  _setupListener (emitter: EventEmitter, event: string, action: ReduxActionCreator<ReduxModel>) {
    debug(`Setting up socket with event "${event}" on "${this._Handler.name}"`)
    this.on(event, handler => {
      if (!handler) {
        debug('Handler not found, returning')
        return
      }
      const laundries = findLaundries(handler)
      const id = handler.model ? handler.model.id : handler
      debug(`Emitting ${this._Handler.name} ${event} action`)
      emitter.emit('action', {id, laundries, action: action(handler)})
    })
  }

  async findFromId (id: string): Promise<?H> {
    if (!regex.mongoDbId.exec(id)) {
      return null
    }
    const model = await this._Model.findById(id)
      .exec()
    return model && (new this._Handler(model)).updateDocument()
  }

  async find (filter: QueryConditions, options: QueryOptions = {}): Promise<H[]> {
    const models = await this._Model
      .find(filter, null, options)
      .exec()
    return Promise.all(models.map((model) => (new this._Handler(model)).updateDocument()))
  }

  on (event: string, callback: Function) {
    this.pubEmitter.on(event, callback)
  }

  removeListener (event: string, callback: Function) {
    this.pubEmitter.removeListener(event, callback)
  }

  fetchCount (criteria: QueryConditions = {}): Promise<number> {
    return this._Model.count(criteria).exec()
  }

  emitEvent (event: 'create' | 'update' | 'delete', instance: H) {
    this.subEmitter.emit(event, instance)
  }
}

function findLaundries (handler: { model: { _id: ObjectId, laundry?: ObjectId, laundries?: ObjectId[] } } | string): string[] {
  const {_id, laundry, laundries} = typeof handler === 'string' ? {} : handler.model
  if (laundry) return [laundry.toString()]
  if (laundries) return laundries.map(id => id.toString())
  if (_id) return [_id.toString()]
  return []
}

type UpdateAction<A> = (a: A) => Promise<A>

export class Handler<M: Model<*>, ReduxModel: {}, RestModel: Resource> {
  model: M
  lib: HandlerLibrary<ReduxModel, M, RestModel, *>
  updateActions: UpdateAction<*>[] = []

  constructor (model: M) {
    this.model = model
  }

  static handlerOrObjectIdToString (i: ObjectId | Handler<*, *, *>): string {
    // $FlowFixMe This is right
    const id: ObjectId = (i.model && i.model ? i.model._id : i)
    return id.toString()
  }

  async save () {
    await this.model.save()
    await this.lib.emitEvent('update', this)
    return this
  }

  shortId (): string {
    return longIdToShort(this.model.id)
  }

  /**
   * Update this document
   * @returns {Promise.<Handler>}
   */
  async updateDocument () {
    if (typeof this.model.docVersion !== 'number') {
      return this
    }
    const updater = this.updateActions[this.model.docVersion]
    if (!updater) {
      return this
    }
    const name = this.constructor.name
    debug(`Updating ${name} document`)
    const newHandler = await (updater: UpdateAction<*>)(this)
    debug(`${name} document updated to version ${typeof newHandler.model.docVersion === 'number' ? newHandler.model.docVersion : 'X'}`)
    return newHandler.updateDocument()
  }

  reduxModel (): ReduxModel {
    throw new Error('Not implemented!')
  }

  toRest (): Promise<RestModel> | RestModel {
    throw new Error('Not implemented!')
  }
}