All files Router.js

83.61% Statements 51/61
76.47% Branches 26/34
94.12% Functions 16/17
83.61% Lines 51/61
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 1391x 1x   1x       33x 33x       41x 41x 1x         3x       3x       3x       3x       41x   40x 7x 7x     40x 1x     39x 21x     39x   39x 39x   4x     39x 39x       67x 109x   109x 35x   74x           32x 32x 32x 74x 20x 20x       32x 12x                   32x 32x       32x 20x     20x       20x     12x 12x 12x 12x         12x                                   1x  
const debug = require('debug')('socketcluster-api:Router');
const { serialize, deserialize } = require('./protobufCodec');
 
const NotFound = Symbol('404 NOT FOUND');
 
class Router {
  constructor(pbRoot) {
    this._routes = [];
    this._pbRoot = pbRoot;
  }
 
  _verifyMethod(method) {
    const allowedMethods = [ 'get', 'post', 'put', 'delete' ];
    if (!allowedMethods.includes(method)) {
      throw new Error(`Invalid method. Allowed methods are: ${allowedMethods.join(',')}`);
    }
  }
 
  get(path, handler) {
    return this.use('get', path, handler);
  }
 
  post(path, handler) {
    return this.use('post', path, handler);
  }
 
  put(path, handler) {
    return this.use('put', path, handler);
  }
 
  delete(path, handler) {
    return this.use('delete', path, handler);
  }
 
  use(method, path, handler) {
    this._verifyMethod(method);
 
    if (typeof path === 'function') {
      handler = path;
      path = '/';
    }
 
    if (typeof handler !== 'function' && !(handler instanceof Router)) {
      throw new Error('A handler (a function or a Router) must be passed to `use`.');
    }
 
    if (path[0] !== '/') {
      path = '/' + path;
    }
 
    const parts = path.split('/').slice(1); // First element is empty (as path always start with '/').
 
    let nextHandler = handler;
    if (parts.length > 1) {
      // This path contains multiple folders. Build `Router`s appropriately
      nextHandler = new Router(this._pbRoot).use(method, parts.slice(1).join('/'), handler);
    }
 
    this._routes.push([ method, `/${parts[0]}`, nextHandler ]);
    return this;
  }
 
  traverse(callback, absolutePath = '') {
    this._routes.forEach(([ method, path, handler ]) => {
      const fullPath = `${absolutePath}${path}`;
 
      if (handler instanceof Router) {
        handler.traverse(callback, fullPath);
      } else {
        callback(method, fullPath, handler);
      }
    });
  }
 
  _find(searchMethod, searchRoute) {
    let resolved = false;
    return new Promise((resolve, reject) => {
      this.traverse((method, fullPath, handler) => {
        if (!resolved && searchMethod === method && fullPath === searchRoute) {
          resolved = true;
          resolve(handler);
        }
      });
 
      if (!resolved) {
        reject(NotFound);
      }
    });
  }
 
  register(scSocket) {
    scSocket.on('#api', this._handleEvent.bind(this));
  }
 
  _handleEvent(data, callback) {
    let plain = {};
    Iif (data.dataType) {
      plain = deserialize(this._pbRoot.lookupType(data.dataType), data.buffer);
    }
 
    return this._find(data.method, data.resource)
      .then(handler => handler(plain))
      .then(({ dataType, responseData } = {}) => {
        let buffer;
        Iif (dataType) {
          buffer = serialize(this._pbRoot.lookupType(dataType), responseData);
        }
 
        callback(null, { dataType, buffer, isError: false });
      })
      .catch(err => {
        const dataType = '.socketclusterapi.ApiError';
        Eif (err === NotFound) {
          debug("No route for %o", data.resource);
          const buffer = serialize(this._pbRoot.lookupType(dataType), {
            code: 404,
            reason: 'Not Found',
            description: `Requested resource '${data.method} ${data.resource}' has no handler defined.`
          });
          callback(null, { dataType, buffer, isError: true });
        } else if ( err.dataType && typeof err.datType === 'string' && err.data) {
          const [ dataType, data ] = err;
          const buffer = serialize(this._pbRoot.lookupType(dataType), data);
          callback(null, { dataType: dataType, buffer, isError: true });
        } else {
          debug("Handler threw unexpectedly: %O", err);
          const buffer = serialize(this._pbRoot.lookupType(dataType), {
            code: 500,
            reason: 'Internal Server Error',
            description: `The resource threw an unexpected error.`
          });
          callback(null, { dataType, buffer, isError: true });
        }
      });
  }
}
 
module.exports = Router;