<commit_before>/* See license.txt for terms of usage */

/* The following implementation serves as a View (the V in MVC pattern) */

define(function(require/*, exports, module*/) {

"use strict";

// Firebug.SDK
const { createFactories } = require("reps/rep-utils");
const { PanelView, createView } = require("firebug.sdk/lib/panel-view");

// ReactJS & Redux
const React = require("react");
const ReactDOM = require("react-dom");

const { Provider } = createFactories(require("react-redux"));

// WebSockets Monitor
const { App } = createFactories(require("./containers/app"));
const { configureStore } = require("./store/configure-store");
const { addFrames, filterFrames, clear } = require("./actions/frames");
const { showTableView, showListView } = require("./actions/perspective");

var store = configureStore();

/**
 * This object represents a view that is responsible for rendering
 * WebSockets panel content. The view is running inside panel's frame
 * with content scope privileges.
 *
 * Rendering logic is based on React and Redux.
 */
var WebSocketsView = createView(PanelView,
/** @lends WebSocketsView */
{
  // New frames are rendered asynchronously in batches.
  timeout: null,
  newFrames: [],

  // Every frame has assigned unique ID.
  uuid: 0,

  /**
   * Render the top level application component.
   */
  initialize: function(config) {
    this.onAddFrames = this.onAddFrames.bind(this);

    // Pass additional properties/callbacks down to
    // the hierarchy of React components.
    config.togglePause = this.onTogglePause.bind(this);

    // Render the top level application component.
    this.content = document.getElementById("content");
    this.theApp = ReactDOM.render(Provider({store: store},
      App(config)
    ), this.content);
  },

  // Chrome Messages

  removeFrames: function() {
    store.dispatch(clear());
  },

  // nsIWebSocketEventService events

  webSocketCreated: function(data) {
    data.type = "event";
    this.lazyAdd(data);
  },

  webSocketOpened: function(data) {
  },

  webSocketClosed: function(data) {
    data.type = "event";
    this.lazyAdd(data);
  },

  /**
   * Event handlers are executed directly according to the
   * event types sent from the chrome scope.
   * See panel-view.js and panel-frame-script.js for details
   * about the mapping event -> method.
   */
  frameReceived: function(frame) {
    frame = JSON.parse(frame);
    frame.received = true;
    frame.type = "frame";
    frame.data.webSocketSerialID = frame.webSocketSerialID;

    this.lazyAdd(frame);
  },

  frameSent: function(frame) {
    frame = JSON.parse(frame);
    frame.sent = true;
    frame.type = "frame";
    frame.data.webSocketSerialID = frame.webSocketSerialID;

    this.lazyAdd(frame);
  },

  lazyAdd: function(frame) {
    frame.id = ++this.uuid;

    this.newFrames.push(frame);

    if (!this.timeout) {
      this.timeout = setTimeout(this.onAddFrames, 300);
    }
  },

  onAddFrames: function() {
    // Support for auto-scroll to the bottom. If the vertical
    // scrollbar of the main panel content is at the bottom
    // keep it at the bottom after new frames are rendered.
    var node = this.content.querySelector(".mainPanelContent");
    var shouldScrollBottom = node.scrollTop == node.scrollTopMax;

    store.dispatch(addFrames(this.newFrames));

    // Scroll to the bottom if required.
    if (shouldScrollBottom) {
      node.scrollTop = node.scrollTopMax;
    }

    this.newFrames = [];
    this.timeout = null;
  },

  onTogglePause: function (paused) {
    this.postChromeMessage("togglePause", paused);
  },

  // Search/Filter

  onSearch: function(filter) {
    store.dispatch(filterFrames(filter));
  },

  // Preferences

  onPrefChanged: function(event) {
    var prefName = event.prefName;
    if (prefName != "tabularView") {
      return;
    }

    // Update the way how frames are displayed.
    if (event.newValue) {
      store.dispatch(showTableView());
    } else {
      store.dispatch(showListView());
    }
  }
});

// End of main.js
});
<commit_msg>Add ISO-8859-1 to UTF-8 encoding before frame parsing<commit_after>/* See license.txt for terms of usage */

/* The following implementation serves as a View (the V in MVC pattern) */

define(function(require/*, exports, module*/) {

"use strict";

// Firebug.SDK
const { createFactories } = require("reps/rep-utils");
const { PanelView, createView } = require("firebug.sdk/lib/panel-view");

// ReactJS & Redux
const React = require("react");
const ReactDOM = require("react-dom");

const { Provider } = createFactories(require("react-redux"));

// WebSockets Monitor
const { App } = createFactories(require("./containers/app"));
const { configureStore } = require("./store/configure-store");
const { addFrames, filterFrames, clear } = require("./actions/frames");
const { showTableView, showListView } = require("./actions/perspective");

var store = configureStore();

/**
 * This object represents a view that is responsible for rendering
 * WebSockets panel content. The view is running inside panel's frame
 * with content scope privileges.
 *
 * Rendering logic is based on React and Redux.
 */
var WebSocketsView = createView(PanelView,
/** @lends WebSocketsView */
{
  // New frames are rendered asynchronously in batches.
  timeout: null,
  newFrames: [],

  // Every frame has assigned unique ID.
  uuid: 0,

  /**
   * Render the top level application component.
   */
  initialize: function(config) {
    this.onAddFrames = this.onAddFrames.bind(this);

    // Pass additional properties/callbacks down to
    // the hierarchy of React components.
    config.togglePause = this.onTogglePause.bind(this);

    // Render the top level application component.
    this.content = document.getElementById("content");
    this.theApp = ReactDOM.render(Provider({store: store},
      App(config)
    ), this.content);
  },

  // Chrome Messages

  removeFrames: function() {
    store.dispatch(clear());
  },

  // nsIWebSocketEventService events

  webSocketCreated: function(data) {
    data.type = "event";
    this.lazyAdd(data);
  },

  webSocketOpened: function(data) {
  },

  webSocketClosed: function(data) {
    data.type = "event";
    this.lazyAdd(data);
  },

  /**
   * Event handlers are executed directly according to the
   * event types sent from the chrome scope.
   * See panel-view.js and panel-frame-script.js for details
   * about the mapping event -> method.
   */
  frameReceived: function(frame) {
    frame = JSON.parse(decodeURIComponent(escape(frame)));
    frame.received = true;
    frame.type = "frame";
    frame.data.webSocketSerialID = frame.webSocketSerialID;

    this.lazyAdd(frame);
  },

  frameSent: function(frame) {
    frame = JSON.parse(decodeURIComponent(escape(frame)));
    frame.sent = true;
    frame.type = "frame";
    frame.data.webSocketSerialID = frame.webSocketSerialID;

    this.lazyAdd(frame);
  },

  lazyAdd: function(frame) {
    frame.id = ++this.uuid;

    this.newFrames.push(frame);

    if (!this.timeout) {
      this.timeout = setTimeout(this.onAddFrames, 300);
    }
  },

  onAddFrames: function() {
    // Support for auto-scroll to the bottom. If the vertical
    // scrollbar of the main panel content is at the bottom
    // keep it at the bottom after new frames are rendered.
    var node = this.content.querySelector(".mainPanelContent");
    var shouldScrollBottom = node.scrollTop == node.scrollTopMax;

    store.dispatch(addFrames(this.newFrames));

    // Scroll to the bottom if required.
    if (shouldScrollBottom) {
      node.scrollTop = node.scrollTopMax;
    }

    this.newFrames = [];
    this.timeout = null;
  },

  onTogglePause: function (paused) {
    this.postChromeMessage("togglePause", paused);
  },

  // Search/Filter

  onSearch: function(filter) {
    store.dispatch(filterFrames(filter));
  },

  // Preferences

  onPrefChanged: function(event) {
    var prefName = event.prefName;
    if (prefName != "tabularView") {
      return;
    }

    // Update the way how frames are displayed.
    if (event.newValue) {
      store.dispatch(showTableView());
    } else {
      store.dispatch(showListView());
    }
  }
});

// End of main.js
});
<|endoftext|>


<commit_before>
var imports     = require('soop').imports();
var EventEmitter= imports.EventEmitter || require('events').EventEmitter;
var bitcore     = require('bitcore');
var util        = bitcore.util;
var extend      = require('util')._extend;
/*
 * Emits
 *  'connect'
 *    when network layout has change (new/lost peers, etc)
 *
 *  'data'
 *    when an unknown data type arrives
 *
 * Provides
 *  send(toPeerIds, {data}, cb?)
 *
 */

function Network(opts) {
  var self            = this;
  opts                = opts || {};
  this.apiKey         = opts.apiKey || 'lwjd5qra8257b9';
  this.debug          = opts.debug || 3;
  this.maxPeers       = opts.maxPeers || 10;
  this.reconnectAttempts = opts.reconnectAttempts || 3;
  this.sjclParams     = opts.sjclParams || {
    salt: 'f28bfb49ef70573c',
    iter:500,
    mode:'ccm',
    ts:parseInt(64),
  };
  this.opts = {};
  ['config', 'port', 'host', 'path', 'debug', 'key', 'secure'].forEach(function(k) {
    if (opts[k]) self.opts[k] = opts[k];
  });
  this.cleanUp();
}

Network.parent = EventEmitter;

Network.prototype.cleanUp = function() {
  this.started = false;
  this.connectedPeers = [];
  this.peerId = null;
  this.netKey = null;
  this.copayerId = null;
  this.signingKey = null;
  this.allowedCopayerIds=null;
  this.isInboundPeerAuth=[];
  this.copayerForPeer={};
  this.connections={};
  this.criticalErr='';
  if (this.peer) {
    this.peer.disconnect();
    this.peer.destroy();
    this.peer.removeAllListeners();
    this.peer = null;
  }
  this.closing = 0;
  this.tries = 0;
};

Network.parent=EventEmitter;

// Array helpers
Network._arrayDiff = function(a, b) {
  var seen = [];
  var diff = [];

  for (var i = 0; i < b.length; i++)
    seen[b[i]] = true;

  for (var j = 0; j < a.length; j++)
    if (!seen[a[j]])
      diff.push(a[j]);

  return diff;
};

Network._inArray = function(el, array) {
  return array.indexOf(el) > -1;
};

Network._arrayPushOnce = function(el, array) {
  var ret = false;
  if (!Network._inArray(el, array)) {
    array.push(el);
    ret = true;
  }
  return ret;
};

Network._arrayRemove = function(el, array) {
  var pos = array.indexOf(el);
  if (pos >= 0) array.splice(pos, 1);
  return array;
};

Network.prototype.connectedCopayers = function() {
  var ret =[];
  for(var i in this.connectedPeers){
    var copayerId =this.copayerForPeer[this.connectedPeers[i]];
    if (copayerId) ret.push(copayerId);
  }
  return ret;
};

Network.prototype._deletePeer = function(peerId) {

  delete this.isInboundPeerAuth[peerId];
  delete this.copayerForPeer[peerId];

  if (this.connections[peerId]) {
    this.connections[peerId].close();
  }
  delete this.connections[peerId];
  this.connectedPeers = Network._arrayRemove(peerId, this.connectedPeers);
};

Network.prototype._onClose = function(peerID) {
  this._deletePeer(peerID);
  this.emit('disconnect', peerID);
};

Network.prototype.connectToCopayers = function(copayerIds) {
  var self = this;
  var arrayDiff= Network._arrayDiff(copayerIds, this.connectedCopayers());

  arrayDiff.forEach(function(copayerId) {
    if (this.allowedCopayerIds && !this.allowedCopayerIds[copayerId]) {
      this._deletePeer(this.peerFromCopayer(copayerId));
    } else {
      self.connectTo(copayerId);
    }
  });
};

Network.prototype._sendHello = function(copayerId) {
  this.send(copayerId, {
    type: 'hello',
    copayerId: this.copayerId,
  });
};

Network.prototype._addConnectedCopayer = function(copayerId, isInbound) {
  var peerId = this.peerFromCopayer(copayerId);
  this._addCopayerMap(peerId,copayerId);
  Network._arrayPushOnce(peerId, this.connectedPeers);
  this.emit('connect', copayerId);
};

Network.prototype._onData = function(encStr, isInbound, peerId) {
  var sig, payload;

  try {
    var data = this._decrypt(encStr);
    payload=  JSON.parse(data);
  } catch (e) {
    this._deletePeer(peerId);
    return;
  }

  if(isInbound && payload.type === 'hello') {
    var payloadStr = JSON.stringify(payload);

    if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) {
      this._deletePeer(peerId);
      return;
    }

    this._addConnectedCopayer(payload.copayerId, isInbound);
    this._setInboundPeerAuth(peerId, true);
    return;
  }

  if ( !this.copayerForPeer[peerId] || (isInbound && !this.isInboundPeerAuth[peerId]) ) {
    this._deletePeer(peerId);
    return;
  }

  var self=this;
  switch(payload.type) {
    case 'disconnect':
      this._onClose(peerId);
      break;
    default:
      this.emit('data', self.copayerForPeer[peerId], payload, isInbound);
  }
};

Network.prototype._checkAnyPeer = function(msg) {
  if (this.connectedPeers.length === 1) {
    this.emit('onlyYou');
  }
};

Network.prototype._setupConnectionHandlers = function(dataConn, toCopayerId) {
  var self = this;

  var isInbound = toCopayerId ? false : true;

  dataConn.on('open', function() {
    if (!Network._inArray(dataConn.peer, self.connectedPeers) &&
        !self.connections[dataConn.peer]) {

      self.connections[dataConn.peer] = dataConn;

      // The connecting peer send hello
      if(toCopayerId) {
        self.emit('connected');
        self._sendHello(toCopayerId);
        self._addConnectedCopayer(toCopayerId);
      }
    }
  });

  dataConn.on('data', function(data) {
    self._onData(data, isInbound, dataConn.peer);
  });

  dataConn.on('error', function(e) {
    self._onClose(dataConn.peer);
    self._checkAnyPeer();
    self.emit('dataError');
  });

  dataConn.on('close', function() {
    if (self.closing) return;

    self._onClose(dataConn.peer);
    self._checkAnyPeer();
  });
};

Network.prototype._setupPeerHandlers = function(openCallback) {
  var self = this;
  var p = this.peer;

  p.on('open', function() {
    self.connectedPeers = [self.peerId];
    self.copayerForPeer[self.peerId]= self.copayerId;
    return openCallback();
  });

  p.on('error', function(err) {
    console.log('RECV ERROR: ', err); //TODO
    if (!err.message.match(/Could\snot\sconnect\sto peer/) ) {
      self.criticalError=err.message;
    }
  });

  p.on('connection', function(dataConn) {
    if (self.connectedPeers.length >= self.maxPeers) {
      dataConn.on('open', function() {
        dataConn.close();
      });
    } else {
      self._setInboundPeerAuth(dataConn.peer, false);
      self._setupConnectionHandlers(dataConn);
    }
  });
};

Network.prototype._addCopayerMap = function(peerId, copayerId) {
  if (!this.copayerForPeer[peerId]) {
    if(Object.keys(this.copayerForPeer).length < this.maxPeers) {
      this.copayerForPeer[peerId]=copayerId;
    }
    else {
    }
  }
};

Network.prototype._setInboundPeerAuth = function(peerId, isAuthenticated) {
  this.isInboundPeerAuth[peerId] = isAuthenticated;
};

Network.prototype.setCopayerId = function(copayerId) {
  if (this.started) {
    throw new Error('network already started: can not change peerId')
  }
  this.copayerId = copayerId;
  this.copayerIdBuf = new Buffer(copayerId,'hex');
  this.peerId = this.peerFromCopayer(this.copayerId);
  this._addCopayerMap(this.peerId,copayerId);
};

// TODO cache this.
Network.prototype.peerFromCopayer = function(hex) {
  var SIN = bitcore.SIN;
  return new SIN(new Buffer(hex,'hex')).toString();
};

Network.prototype.start = function(opts, openCallback) {
  opts = opts || {};

  if (this.started) return openCallback();

  this.netKey = opts.netKey;
  this.maxPeers = opts.maxPeers || this.maxPeers;

  if (opts.token)
    this.opts.token = opts.token;

  if (!this.copayerId)
    this.setCopayerId(opts.copayerId);

  var self = this;
  var setupPeer = function () {
    if (self.connectedPeers.length > 0) return; // Already connected!
    if (self.peer) {
      self.peer.destroy();
      self.peer.removeAllListeners();
    }

    if (!self.criticalError && self.tries < self.reconnectAttempts) {
      self.tries++;
      self.peer = new Peer(self.peerId, self.opts);
      self.started = true;
      self._setupPeerHandlers(openCallback);
      setTimeout(setupPeer, 3000); // Schedule retry
      return;
    }
    if (self.criticalError && self.criticalError.match(/taken/)) {
      self.criticalError=' Looks like you are already connected to this wallet please close all other Copay Wallets '
    }
    self.emit('serverError', self.criticalError);
    self.cleanUp();
  }

  this.tries = 0;
  setupPeer();
};

Network.prototype.getOnlinePeerIDs = function() {
  return this.connectedPeers;
};

Network.prototype.getPeer = function() {
  return this.peer;
};

Network.prototype._encrypt = function(payloadStr) {
  var plainText = sjcl.codec.utf8String.toBits(payloadStr);
  var p = this.sjclParams;
  ct = sjcl.encrypt(this.netKey, plainText, p);//,p, rp);
  var c = JSON.parse(ct);
  var toSend = {
    iv: c.iv,
    ct: c.ct,
  };
  return JSON.stringify(toSend);
};

Network.prototype._decrypt = function(encStr) {
  var i = JSON.parse(encStr);
  for (var k in this.sjclParams) {
    i[k] = this.sjclParams[k];
  }
  var str= JSON.stringify(i);
  var pt = sjcl.decrypt(this.netKey, str);
  return pt;
};

Network.prototype._sendToOne = function(copayerId, payload, sig, cb) {
  var peerId = this.peerFromCopayer(copayerId);
  if (peerId !== this.peerId) {
    var dataConn = this.connections[peerId];
    if (dataConn) {
      dataConn.send(payload);
    }
  }
  if (typeof cb === 'function') cb();
};

Network.prototype.send = function(copayerIds, payload, cb) {
  if (!payload || !this.netKey) return cb();

  var self=this;
  if (!copayerIds) {
    copayerIds = this.connectedCopayers();
    payload.isBroadcast = 1;
  }

  var sig;
  var payloadStr = JSON.stringify(payload);
  var encPayload = this._encrypt(payloadStr);
  if (Array.isArray(copayerIds)) {
    var l = copayerIds.length;
    var i = 0;
    copayerIds.forEach(function(copayerId) {
      self._sendToOne(copayerId, encPayload, sig, function () {
        if (++i === l && typeof cb === 'function') cb();
      });
    });
  }
  else if (typeof copayerIds === 'string')
    self._sendToOne(copayerIds, encPayload, sig, cb);
};

Network.prototype.isOnline = function() {
  return !!this.peer;
};

Network.prototype.connectTo = function(copayerId) {
  var self = this;

  var peerId = this.peerFromCopayer(copayerId);
  var dataConn = this.peer.connect(peerId, {
    serialization: 'none',
    reliable: true,
  });
  self._setupConnectionHandlers(dataConn, copayerId);
};

Network.prototype.lockIncommingConnections = function(allowedCopayerIdsArray) {
  this.allowedCopayerIds={};
  for(var i in allowedCopayerIdsArray) {
    this.allowedCopayerIds[ allowedCopayerIdsArray[i] ] = true;
  }
};

Network.prototype.disconnect = function(cb, forced) {
  var self = this;
  self.closing = 1;
  self.send(null, { type: 'disconnect' }, function(){
    self.cleanUp();
    if (typeof cb === 'function') cb();
  });
};

module.exports = require('soop')(Network);
<commit_msg>fix secure:false<commit_after>
var imports     = require('soop').imports();
var EventEmitter= imports.EventEmitter || require('events').EventEmitter;
var bitcore     = require('bitcore');
var util        = bitcore.util;
var extend      = require('util')._extend;
/*
 * Emits
 *  'connect'
 *    when network layout has change (new/lost peers, etc)
 *
 *  'data'
 *    when an unknown data type arrives
 *
 * Provides
 *  send(toPeerIds, {data}, cb?)
 *
 */

function Network(opts) {
  var self            = this;
  opts                = opts || {};
  this.apiKey         = opts.apiKey || 'lwjd5qra8257b9';
  this.debug          = opts.debug || 3;
  this.maxPeers       = opts.maxPeers || 10;
  this.reconnectAttempts = opts.reconnectAttempts || 3;
  this.sjclParams     = opts.sjclParams || {
    salt: 'f28bfb49ef70573c',
    iter:500,
    mode:'ccm',
    ts:parseInt(64),
  };
  this.opts = {};
  ['config', 'port', 'host', 'path', 'debug', 'key', 'secure'].forEach(function(k) {
    if (opts.hasOwnProperty(k)) self.opts[k] = opts[k];
  });
  this.cleanUp();
}

Network.parent = EventEmitter;

Network.prototype.cleanUp = function() {
  this.started = false;
  this.connectedPeers = [];
  this.peerId = null;
  this.netKey = null;
  this.copayerId = null;
  this.signingKey = null;
  this.allowedCopayerIds=null;
  this.isInboundPeerAuth=[];
  this.copayerForPeer={};
  this.connections={};
  this.criticalErr='';
  if (this.peer) {
    this.peer.disconnect();
    this.peer.destroy();
    this.peer.removeAllListeners();
    this.peer = null;
  }
  this.closing = 0;
  this.tries = 0;
};

Network.parent=EventEmitter;

// Array helpers
Network._arrayDiff = function(a, b) {
  var seen = [];
  var diff = [];

  for (var i = 0; i < b.length; i++)
    seen[b[i]] = true;

  for (var j = 0; j < a.length; j++)
    if (!seen[a[j]])
      diff.push(a[j]);

  return diff;
};

Network._inArray = function(el, array) {
  return array.indexOf(el) > -1;
};

Network._arrayPushOnce = function(el, array) {
  var ret = false;
  if (!Network._inArray(el, array)) {
    array.push(el);
    ret = true;
  }
  return ret;
};

Network._arrayRemove = function(el, array) {
  var pos = array.indexOf(el);
  if (pos >= 0) array.splice(pos, 1);
  return array;
};

Network.prototype.connectedCopayers = function() {
  var ret =[];
  for(var i in this.connectedPeers){
    var copayerId =this.copayerForPeer[this.connectedPeers[i]];
    if (copayerId) ret.push(copayerId);
  }
  return ret;
};

Network.prototype._deletePeer = function(peerId) {

  delete this.isInboundPeerAuth[peerId];
  delete this.copayerForPeer[peerId];

  if (this.connections[peerId]) {
    this.connections[peerId].close();
  }
  delete this.connections[peerId];
  this.connectedPeers = Network._arrayRemove(peerId, this.connectedPeers);
};

Network.prototype._onClose = function(peerID) {
  this._deletePeer(peerID);
  this.emit('disconnect', peerID);
};

Network.prototype.connectToCopayers = function(copayerIds) {
  var self = this;
  var arrayDiff= Network._arrayDiff(copayerIds, this.connectedCopayers());

  arrayDiff.forEach(function(copayerId) {
    if (this.allowedCopayerIds && !this.allowedCopayerIds[copayerId]) {
      this._deletePeer(this.peerFromCopayer(copayerId));
    } else {
      self.connectTo(copayerId);
    }
  });
};

Network.prototype._sendHello = function(copayerId) {
  this.send(copayerId, {
    type: 'hello',
    copayerId: this.copayerId,
  });
};

Network.prototype._addConnectedCopayer = function(copayerId, isInbound) {
  var peerId = this.peerFromCopayer(copayerId);
  this._addCopayerMap(peerId,copayerId);
  Network._arrayPushOnce(peerId, this.connectedPeers);
  this.emit('connect', copayerId);
};

Network.prototype._onData = function(encStr, isInbound, peerId) {
  var sig, payload;

  try {
    var data = this._decrypt(encStr);
    payload=  JSON.parse(data);
  } catch (e) {
    this._deletePeer(peerId);
    return;
  }

  if(isInbound && payload.type === 'hello') {
    var payloadStr = JSON.stringify(payload);

    if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) {
      this._deletePeer(peerId);
      return;
    }

    this._addConnectedCopayer(payload.copayerId, isInbound);
    this._setInboundPeerAuth(peerId, true);
    return;
  }

  if ( !this.copayerForPeer[peerId] || (isInbound && !this.isInboundPeerAuth[peerId]) ) {
    this._deletePeer(peerId);
    return;
  }

  var self=this;
  switch(payload.type) {
    case 'disconnect':
      this._onClose(peerId);
      break;
    default:
      this.emit('data', self.copayerForPeer[peerId], payload, isInbound);
  }
};

Network.prototype._checkAnyPeer = function(msg) {
  if (this.connectedPeers.length === 1) {
    this.emit('onlyYou');
  }
};

Network.prototype._setupConnectionHandlers = function(dataConn, toCopayerId) {
  var self = this;

  var isInbound = toCopayerId ? false : true;

  dataConn.on('open', function() {
    if (!Network._inArray(dataConn.peer, self.connectedPeers) &&
        !self.connections[dataConn.peer]) {

      self.connections[dataConn.peer] = dataConn;

      // The connecting peer send hello
      if(toCopayerId) {
        self.emit('connected');
        self._sendHello(toCopayerId);
        self._addConnectedCopayer(toCopayerId);
      }
    }
  });

  dataConn.on('data', function(data) {
    self._onData(data, isInbound, dataConn.peer);
  });

  dataConn.on('error', function(e) {
    self._onClose(dataConn.peer);
    self._checkAnyPeer();
    self.emit('dataError');
  });

  dataConn.on('close', function() {
    if (self.closing) return;

    self._onClose(dataConn.peer);
    self._checkAnyPeer();
  });
};

Network.prototype._setupPeerHandlers = function(openCallback) {
  var self = this;
  var p = this.peer;

  p.on('open', function() {
    self.connectedPeers = [self.peerId];
    self.copayerForPeer[self.peerId]= self.copayerId;
    return openCallback();
  });

  p.on('error', function(err) {
    console.log('RECV ERROR: ', err); //TODO
    if (!err.message.match(/Could\snot\sconnect\sto peer/) ) {
      self.criticalError=err.message;
    }
  });

  p.on('connection', function(dataConn) {
    if (self.connectedPeers.length >= self.maxPeers) {
      dataConn.on('open', function() {
        dataConn.close();
      });
    } else {
      self._setInboundPeerAuth(dataConn.peer, false);
      self._setupConnectionHandlers(dataConn);
    }
  });
};

Network.prototype._addCopayerMap = function(peerId, copayerId) {
  if (!this.copayerForPeer[peerId]) {
    if(Object.keys(this.copayerForPeer).length < this.maxPeers) {
      this.copayerForPeer[peerId]=copayerId;
    }
    else {
    }
  }
};

Network.prototype._setInboundPeerAuth = function(peerId, isAuthenticated) {
  this.isInboundPeerAuth[peerId] = isAuthenticated;
};

Network.prototype.setCopayerId = function(copayerId) {
  if (this.started) {
    throw new Error('network already started: can not change peerId')
  }
  this.copayerId = copayerId;
  this.copayerIdBuf = new Buffer(copayerId,'hex');
  this.peerId = this.peerFromCopayer(this.copayerId);
  this._addCopayerMap(this.peerId,copayerId);
};

// TODO cache this.
Network.prototype.peerFromCopayer = function(hex) {
  var SIN = bitcore.SIN;
  return new SIN(new Buffer(hex,'hex')).toString();
};

Network.prototype.start = function(opts, openCallback) {
  opts = opts || {};

  if (this.started) return openCallback();

  this.netKey = opts.netKey;
  this.maxPeers = opts.maxPeers || this.maxPeers;

  if (opts.token)
    this.opts.token = opts.token;

  if (!this.copayerId)
    this.setCopayerId(opts.copayerId);

  var self = this;
  var setupPeer = function () {
    if (self.connectedPeers.length > 0) return; // Already connected!
    if (self.peer) {
      self.peer.destroy();
      self.peer.removeAllListeners();
    }

    if (!self.criticalError && self.tries < self.reconnectAttempts) {
      self.tries++;
      self.peer = new Peer(self.peerId, self.opts);
      self.started = true;
      self._setupPeerHandlers(openCallback);
      setTimeout(setupPeer, 3000); // Schedule retry
      return;
    }
    if (self.criticalError && self.criticalError.match(/taken/)) {
      self.criticalError=' Looks like you are already connected to this wallet please close all other Copay Wallets '
    }
    self.emit('serverError', self.criticalError);
    self.cleanUp();
  }

  this.tries = 0;
  setupPeer();
};

Network.prototype.getOnlinePeerIDs = function() {
  return this.connectedPeers;
};

Network.prototype.getPeer = function() {
  return this.peer;
};

Network.prototype._encrypt = function(payloadStr) {
  var plainText = sjcl.codec.utf8String.toBits(payloadStr);
  var p = this.sjclParams;
  ct = sjcl.encrypt(this.netKey, plainText, p);//,p, rp);
  var c = JSON.parse(ct);
  var toSend = {
    iv: c.iv,
    ct: c.ct,
  };
  return JSON.stringify(toSend);
};

Network.prototype._decrypt = function(encStr) {
  var i = JSON.parse(encStr);
  for (var k in this.sjclParams) {
    i[k] = this.sjclParams[k];
  }
  var str= JSON.stringify(i);
  var pt = sjcl.decrypt(this.netKey, str);
  return pt;
};

Network.prototype._sendToOne = function(copayerId, payload, sig, cb) {
  var peerId = this.peerFromCopayer(copayerId);
  if (peerId !== this.peerId) {
    var dataConn = this.connections[peerId];
    if (dataConn) {
      dataConn.send(payload);
    }
  }
  if (typeof cb === 'function') cb();
};

Network.prototype.send = function(copayerIds, payload, cb) {
  if (!payload || !this.netKey) return cb();

  var self=this;
  if (!copayerIds) {
    copayerIds = this.connectedCopayers();
    payload.isBroadcast = 1;
  }

  var sig;
  var payloadStr = JSON.stringify(payload);
  var encPayload = this._encrypt(payloadStr);
  if (Array.isArray(copayerIds)) {
    var l = copayerIds.length;
    var i = 0;
    copayerIds.forEach(function(copayerId) {
      self._sendToOne(copayerId, encPayload, sig, function () {
        if (++i === l && typeof cb === 'function') cb();
      });
    });
  }
  else if (typeof copayerIds === 'string')
    self._sendToOne(copayerIds, encPayload, sig, cb);
};

Network.prototype.isOnline = function() {
  return !!this.peer;
};

Network.prototype.connectTo = function(copayerId) {
  var self = this;

  var peerId = this.peerFromCopayer(copayerId);
  var dataConn = this.peer.connect(peerId, {
    serialization: 'none',
    reliable: true,
  });
  self._setupConnectionHandlers(dataConn, copayerId);
};

Network.prototype.lockIncommingConnections = function(allowedCopayerIdsArray) {
  this.allowedCopayerIds={};
  for(var i in allowedCopayerIdsArray) {
    this.allowedCopayerIds[ allowedCopayerIdsArray[i] ] = true;
  }
};

Network.prototype.disconnect = function(cb, forced) {
  var self = this;
  self.closing = 1;
  self.send(null, { type: 'disconnect' }, function(){
    self.cleanUp();
    if (typeof cb === 'function') cb();
  });
};

module.exports = require('soop')(Network);
<|endoftext|>


<commit_before>import * as types from '../../mutation-types'
import lazyLoader from './lazyLoader'

const state = {
  items: [
    {
      name: 'Home',
      path: '/',
      meta: {
        label: ['主頁面'],
        link: 'Home.vue',
        icon: ['fas', 'home'],
        permission: 99
      },
      components: lazyLoader('Home')
    },
    {
      name: 'Server Status',
      path: '/status',
      meta: {
        label: ['伺服器'],
        link: 'Server.vue',
        icon: ['fas', 'server'],
        permission: 99
      },
      components: lazyLoader('Server')
    },
    {
      name: 'Files',
      path: '/files',
      meta: {
        label: ['檔案庫'],
        link: 'File.vue',
        icon: ['fas', 'database'],
        permission: 1
      },
      components: lazyLoader('File')
    },
    {
      name: 'Monit',
      path: '/monit',
      meta: {
        label: ['監視器'],
        link: 'Monit.vue',
        icon: ['fas', 'video'],
        permission: 1
      },
      components: lazyLoader('Monit')
    },
    {
      name: 'Todo List',
      path: '/todos',
      meta: {
        label: ['待辦事項'],
        link: 'Todos.vue',
        icon: ['fas', 'calendar-check'],
        permission: 1
      },
      components: lazyLoader('Todos')
    },
    {
      name: 'Post',
      path: '/post',
      meta: {
        label: ['新增文章'],
        link: 'Post.vue',
        icon: ['fas', 'file'],
        permission: 3
      },
      components: lazyLoader('Post/Add')
    },
    {
      name: 'Post List',
      path: '/post-list',
      meta: {
        label: ['文章列表'],
        link: 'Post.vue',
        icon: ['fas', 'list-alt'],
        permission: 3
      },
      components: lazyLoader('Post/List')
    },
    {
      name: 'News',
      path: '/news',
      meta: {
        label: ['新增新聞'],
        link: 'News.vue',
        icon: ['fas', 'plus'],
        permission: 2
      },
      components: lazyLoader('News/Add')
    },
    {
      name: 'News List',
      path: '/news-list',
      meta: {
        label: ['新聞列表'],
        link: 'News.vue',
        icon: ['fas', 'newspaper'],
        permission: 2
      },
      components: lazyLoader('News/List')
    }
  ]
}

const mutations = {
  [types.EXPAND_MENU] (state, menuItem) {
    if (menuItem.index > -1) {
      if (state.items[menuItem.index] && state.items[menuItem.index].meta) {
        state.items[menuItem.index].meta.expanded = menuItem.expanded
      }
    } else if (menuItem.item && 'expanded' in menuItem.item.meta) {
      menuItem.item.meta.expanded = menuItem.expanded
    }
  }
}

export default {
  state,
  mutations
}
<commit_msg>Security: Modify permission<commit_after>import * as types from '../../mutation-types'
import lazyLoader from './lazyLoader'

const state = {
  items: [
    {
      name: 'Home',
      path: '/',
      meta: {
        label: ['主頁面'],
        link: 'Home.vue',
        icon: ['fas', 'home'],
        permission: 99
      },
      components: lazyLoader('Home')
    },
    {
      name: 'Server Status',
      path: '/status',
      meta: {
        label: ['伺服器'],
        link: 'Server.vue',
        icon: ['fas', 'server'],
        permission: 99
      },
      components: lazyLoader('Server')
    },
    {
      name: 'Files',
      path: '/files',
      meta: {
        label: ['檔案庫'],
        link: 'File.vue',
        icon: ['fas', 'database'],
        permission: 0
      },
      components: lazyLoader('File')
    },
    {
      name: 'Monit',
      path: '/monit',
      meta: {
        label: ['監視器'],
        link: 'Monit.vue',
        icon: ['fas', 'video'],
        permission: 1
      },
      components: lazyLoader('Monit')
    },
    {
      name: 'Todo List',
      path: '/todos',
      meta: {
        label: ['待辦事項'],
        link: 'Todos.vue',
        icon: ['fas', 'calendar-check'],
        permission: 2
      },
      components: lazyLoader('Todos')
    },
    {
      name: 'Post',
      path: '/post',
      meta: {
        label: ['新增文章'],
        link: 'Post.vue',
        icon: ['fas', 'file'],
        permission: 3
      },
      components: lazyLoader('Post/Add')
    },
    {
      name: 'Post List',
      path: '/post-list',
      meta: {
        label: ['文章列表'],
        link: 'Post.vue',
        icon: ['fas', 'list-alt'],
        permission: 3
      },
      components: lazyLoader('Post/List')
    },
    {
      name: 'News',
      path: '/news',
      meta: {
        label: ['新增新聞'],
        link: 'News.vue',
        icon: ['fas', 'plus'],
        permission: 2
      },
      components: lazyLoader('News/Add')
    },
    {
      name: 'News List',
      path: '/news-list',
      meta: {
        label: ['新聞列表'],
        link: 'News.vue',
        icon: ['fas', 'newspaper'],
        permission: 2
      },
      components: lazyLoader('News/List')
    }
  ]
}

const mutations = {
  [types.EXPAND_MENU] (state, menuItem) {
    if (menuItem.index > -1) {
      if (state.items[menuItem.index] && state.items[menuItem.index].meta) {
        state.items[menuItem.index].meta.expanded = menuItem.expanded
      }
    } else if (menuItem.item && 'expanded' in menuItem.item.meta) {
      menuItem.item.meta.expanded = menuItem.expanded
    }
  }
}

export default {
  state,
  mutations
}
<|endoftext|>


<commit_before>var assert = require('assert');
var expect = require('chai').expect;
var waitForOncoprint = require('./../specUtils').waitForOncoprint;
var setOncoprintMutationsMenuOpen = require('./../specUtils').setOncoprintMutationsMenuOpen;
var goToUrlAndSetLocalStorage = require('./../specUtils').goToUrlAndSetLocalStorage;
var waitForNetworkQuiet = require('./../specUtils').waitForNetworkQuiet;
var sessionServiceIsEnabled = require('./../specUtils').sessionServiceIsEnabled;
var assertScreenShotMatch = require('../../lib/testUtils').assertScreenShotMatch;

const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, "");

describe("coexpression tab screenshot tests", function() {

    this.retries(2);

    before(function() {
        var url = `${CBIOPORTAL_URL}/results/coexpression?tab_index=tab_visualize&cancer_study_list=coadread_tcga_pub&cancer_study_id=coadread_tcga_pub&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic&Z_SCORE_THRESHOLD=2.0&case_set_id=coadread_tcga_pub_nonhypermut&case_ids=&gene_list=KRAS+NRAS+BRAF&gene_set_choice=user-defined-list&Action=Submit`;
        goToUrlAndSetLocalStorage(url);
    });
    it('coexpression tab coadread_tcga_pub initial load', function() {
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub log scale x and y mutations on', function() {
        browser.click('div[data-test="coExpressionTabDiv"] input[data-test="logScale"]');
        browser.moveToObject("body",0,0);
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub log scale x and y with regression line', function() {
        browser.click('input[data-test="ShowRegressionLine"]');
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub loc scale x and y mutations off', function() {
        browser.click('input[data-test="ShowRegressionLine"]');
        browser.click('div[data-test="coExpressionTabDiv"] input[data-test="ShowMutations"]');
        browser.moveToObject("body",0,0);
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub switch tabs', function() {
        browser.click('#coexpressionTabGeneTabs>ul>li:nth-child(2)>a');// click on NRAS
        browser.moveToObject("body",0,0);
        browser.pause(100); // give time to start loading
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub switch profiles', function() {
        browser.execute(function() { resultsViewCoExpressionTab.onSelectProfileX({ value: "coadread_tcga_pub_mrna"}); });
        browser.execute(function() { resultsViewCoExpressionTab.onSelectProfileY({ value: "coadread_tcga_pub_mrna"}); });
        browser.pause(100); // give time to start loading
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub switch profiles + regression line', function() {
        browser.click('input[data-test="ShowRegressionLine"]');
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub with a lot of genes', function() {
        goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}/results/coexpression?cancer_study_id=coadread_tcga_pub&Z_SCORE_THRESHOLD=2&RPPA_SCORE_THRESHOLD=2&data_priority=0&case_set_id=coadread_tcga_pub_nonhypermut&gene_list=AKR1C3%2520AR%2520CYB5A%2520CYP11A1%2520CYP11B1%2520CYP11B2%2520CYP17A1%2520CYP19A1%2520CYP21A2%2520HSD17B1%2520HSD17B10%2520HSD17B11%2520HSD17B12%2520HSD17B13%2520HSD17B14%2520HSD17B2%2520HSD17B3%2520HSD17B4%2520HSD17B6%2520HSD17B7%2520HSD17B8%2520HSD3B1%2520HSD3B2%2520HSD3B7%2520RDH5%2520SHBG%2520SRD5A1%2520SRD5A2%2520SRD5A3%2520STAR&geneset_list=+&tab_index=tab_visualize&Action=Submit&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic`);
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
});
<commit_msg>Add screenshot test coexpression tab<commit_after>var assert = require('assert');
var expect = require('chai').expect;
var waitForOncoprint = require('./../specUtils').waitForOncoprint;
var setOncoprintMutationsMenuOpen = require('./../specUtils').setOncoprintMutationsMenuOpen;
var goToUrlAndSetLocalStorage = require('./../specUtils').goToUrlAndSetLocalStorage;
var waitForNetworkQuiet = require('./../specUtils').waitForNetworkQuiet;
var sessionServiceIsEnabled = require('./../specUtils').sessionServiceIsEnabled;
var assertScreenShotMatch = require('../../lib/testUtils').assertScreenShotMatch;

const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, "");

describe("coexpression tab screenshot tests", function() {

    this.retries(2);

    before(function() {
        var url = `${CBIOPORTAL_URL}/results/coexpression?tab_index=tab_visualize&cancer_study_list=coadread_tcga_pub&cancer_study_id=coadread_tcga_pub&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic&Z_SCORE_THRESHOLD=2.0&case_set_id=coadread_tcga_pub_nonhypermut&case_ids=&gene_list=KRAS+NRAS+BRAF&gene_set_choice=user-defined-list&Action=Submit`;
        goToUrlAndSetLocalStorage(url);
    });
    it('coexpression tab coadread_tcga_pub initial load', function() {
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub log scale x and y mutations on', function() {
        browser.click('div[data-test="coExpressionTabDiv"] input[data-test="logScale"]');
        browser.moveToObject("body",0,0);
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub log scale x and y with regression line', function() {
        browser.click('input[data-test="ShowRegressionLine"]');
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub loc scale x and y mutations off', function() {
        browser.click('input[data-test="ShowRegressionLine"]');
        browser.click('div[data-test="coExpressionTabDiv"] input[data-test="ShowMutations"]');
        browser.moveToObject("body",0,0);
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub switch tabs', function() {
        browser.click('#coexpressionTabGeneTabs>ul>li:nth-child(2)>a');// click on NRAS
        browser.moveToObject("body",0,0);
        browser.pause(100); // give time to start loading
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub switch profiles', function() {
        browser.execute(function() { resultsViewCoExpressionTab.onSelectProfileX({ value: "coadread_tcga_pub_mrna"}); });
        browser.execute(function() { resultsViewCoExpressionTab.onSelectProfileY({ value: "coadread_tcga_pub_mrna"}); });
        browser.pause(100); // give time to start loading
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub switch profiles + regression line', function() {
        browser.click('input[data-test="ShowRegressionLine"]');
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
    it('coexpression tab coadread_tcga_pub with a lot of genes', function() {
        goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}/results/coexpression?cancer_study_id=coadread_tcga_pub&Z_SCORE_THRESHOLD=2&RPPA_SCORE_THRESHOLD=2&data_priority=0&case_set_id=coadread_tcga_pub_nonhypermut&gene_list=AKR1C3%2520AR%2520CYB5A%2520CYP11A1%2520CYP11B1%2520CYP11B2%2520CYP17A1%2520CYP19A1%2520CYP21A2%2520HSD17B1%2520HSD17B10%2520HSD17B11%2520HSD17B12%2520HSD17B13%2520HSD17B14%2520HSD17B2%2520HSD17B3%2520HSD17B4%2520HSD17B6%2520HSD17B7%2520HSD17B8%2520HSD3B1%2520HSD3B2%2520HSD3B7%2520RDH5%2520SHBG%2520SRD5A1%2520SRD5A2%2520SRD5A3%2520STAR&geneset_list=+&tab_index=tab_visualize&Action=Submit&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic`);
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });

    it('coexpression tab coadread_tcga_pub with user defined case list', function() {
        goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}/results/coexpression?Action=Submit&cancer_study_list=coadread_tcga_pub&case_ids=coadread_tcga_pub%3ATCGA-A6-2672-01%2Bcoadread_tcga_pub%3ATCGA-A6-2678-01%2Bcoadread_tcga_pub%3ATCGA-A6-3809-01%2Bcoadread_tcga_pub%3ATCGA-AA-3502-01%2Bcoadread_tcga_pub%3ATCGA-AA-3510-01%2Bcoadread_tcga_pub%3ATCGA-AA-3672-01%2Bcoadread_tcga_pub%3ATCGA-AA-3673-01%2Bcoadread_tcga_pub%3ATCGA-AA-3850-01%2Bcoadread_tcga_pub%3ATCGA-AA-3852-01%2Bcoadread_tcga_pub%3ATCGA-AA-3862-01%2Bcoadread_tcga_pub%3ATCGA-AA-3877-01%2Bcoadread_tcga_pub%3ATCGA-AA-3986-01%2Bcoadread_tcga_pub%3ATCGA-AA-3989-01%2Bcoadread_tcga_pub%3ATCGA-AA-3994-01%2Bcoadread_tcga_pub%3ATCGA-AA-A00L-01%2Bcoadread_tcga_pub%3ATCGA-AA-A010-01%2Bcoadread_tcga_pub%3ATCGA-AA-A02O-01%2Bcoadread_tcga_pub%3ATCGA-CM-4748-01&case_set_id=-1&clinicallist=PROFILED_IN_coadread_tcga_pub_mutations%2CPROFILED_IN_coadread_tcga_pub_gistic&gene_list=KRAS%0AAPC&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&show_samples=false&tab_index=tab_visualize`);
        browser.waitForExist('div[data-test="CoExpressionPlot"]', 60000); // wait for plot to show up
        var res = browser.checkElement('div[data-test="coExpressionTabDiv"]');
        assertScreenShotMatch(res);
    });
});
<|endoftext|>


<commit_before>import forOwn from 'lodash/object/forOwn';
import isArray from 'lodash/lang/isArray';

import Session from './Session';
import Backend from './Backend';
import QuerySet from './QuerySet';
import {
    ManyToMany,
    ForeignKey,
    OneToOne,
} from './fields';
import {CREATE, UPDATE, DELETE, ORDER} from './constants';
import {
    match,
    normalizeEntity,
    arrayDiffActions,
} from './utils';

/**
 * The heart of an ORM, the data model.
 * The static class methods manages the updates
 * passed to this. The class itself is connected to a session,
 * and because of this you can only have a single session at a time
 * for a {@link Model} class.
 *
 * An instance of {@link Model} represents an object in the database.
 *
 * To create data models in your schema, subclass {@link Model}. To define
 * information about the data model, override static class methods. Define instance
 * logic by defining prototype methods (without `static` keyword).
 */
const Model = class Model {
    /**
     * Creates a Model instance.
     * @param  {Object} props - the properties to instantiate with
     */
    constructor(props) {
        this._initFields(props);
    }

    _initFields(props) {
        const ModelClass = this.getClass();

        this._fieldNames = [];
        this._fields = props;
        const idAttribute = ModelClass.idAttribute;

        forOwn(props, (fieldValue, fieldName) => {
            this._fields[fieldName] = fieldValue;
            this._fieldNames.push(fieldName);

            // If the field has not already been defined on the
            // prototype for a relation.
            if (!ModelClass.definedProperties[fieldName]) {
                Object.defineProperty(this, fieldName, {
                    get: () => fieldValue,
                    set: (value) => {
                        ModelClass.addUpdate({
                            type: UPDATE,
                            payload: {
                                [idAttribute]: this.getId(),
                                [fieldName]: value,
                            },
                        });
                    },
                });
            }
        });
    }

    /**
     * Returns the raw state for this {@link Model} in the current {@link Session}.
     * @return {Object} The state for this {@link Model} in the current {@link Session}.
     */
    static get state() {
        return this.session.getState(this.modelName);
    }

    static toString() {
        return `ModelClass: ${this.modelName}`;
    }

    /**
     * Returns the options object passed to the {@link Backend} class constructor.
     *
     * @return {Object} the options object used to instantiate a {@link Backend} class.
     */
    static backend() {
        return {
            branchName: this.modelName,
        };
    }

    static _getBackendOpts() {
        if (typeof this.backend === 'function') {
            return this.backend();
        }
        if (typeof this.backend === 'undefined') {
            throw new Error(`You must declare either a 'backend' class method or
                            a 'backend' class variable in your Model Class`);
        }
        return this.backend;
    }

    /**
     * Returns the {@link Backend} class used to instantiate
     * the {@link Backend} instance for this {@link Model}.
     *
     * Override this if you want to use a custom {@link Backend} class.
     * @return {Backend} The {@link Backend} class or subclass to use for this {@link Model}.
     */
    static getBackendClass() {
        return Backend;
    }

    static get _sessionCache() {
        if (!this.hasOwnProperty('__sessionCache')) {
            this.__sessionCache = {};
        }
        return this.__sessionCache;
    }

    static clearSessionCache() {
        this.__sessionCache = {};
    }

    /**
     * Gets the {@link Backend} instance linked to this {@link Model}.
     * @return {Backend} The {@link Backend} instance linked to this {@link Model}.
     */
    static getBackend() {
        if (!this._sessionCache.backend) {
            const BackendClass = this.getBackendClass();

            const opts = this._getBackendOpts();
            if (this._session && this._session.withMutations) {
                opts.withMutations = true;
            }

            this._sessionCache.backend = new BackendClass(opts);
        }
        return this._sessionCache.backend;
    }

    /**
     * Gets the Model's next state by applying the recorded
     * updates.
     * @return {Object} The next state.
     */
    static getNextState() {
        if (typeof this.state === 'undefined') {
            return this.getDefaultState();
        }

        const updates = this.session.getUpdatesFor(this);

        return updates.reduce(this.updateReducer.bind(this), this.state);
    }

    static updateReducer(state, action) {
        const backend = this.getBackend();

        switch (action.type) {
        case CREATE:
            return backend.insert(state, action.payload);
        case UPDATE:
            return backend.update(state, action.payload.idArr, action.payload.updater);
        case ORDER:
            return backend.order(state, action.payload);
        case DELETE:
            return backend.delete(state, action.payload);
        default:
            return state;
        }
    }

    /**
     * The default reducer implementation.
     * If the user doesn't define a reducer, this is used.
     *
     * @param {Object} state - the current state
     * @param {Object} action - the dispatched action
     * @param {Model} model - the concrete model class being used
     * @param {Session} session - the current {@link Session} instance
     */
    static reducer(state, action, model, session) {
        return model.getNextState();
    }

    /**
     * Gets the default, empty state of the branch.
     * Delegates to a {@link Backend} instance.
     * @return {Object} The default state.
     */
    static getDefaultState() {
        return this.getBackend().getDefaultState();
    }

    static markAccessed() {
        this.session.markAccessed(this);
    }

    /**
     * Returns the id attribute of this {@link Model}.
     * Delegates to the related {@link Backend} instance.
     *
     * @return {string} The id attribute of this {@link Model}.
     */
    static get idAttribute() {
        return this.getBackend().idAttribute;
    }

    /**
     * A convenience method to call {@link Backend#accessId} from
     * the {@link Model} class.
     *
     * @param  {Number} id - the object id to access
     * @return {Object} a reference to the object in the database.
     */
    static accessId(id) {
        this.markAccessed();
        return this.getBackend().accessId(this.state, id);
    }

    /**
     * A convenience method to call {@link Backend#accessIdList} from
     * the {@link Model} class with the current state.
     */
    static accessIds() {
        this.markAccessed();
        return this.getBackend().accessIdList(this.state);
    }

    static accessList() {
        this.markAccessed();
        return this.getBackend().accessList(this.state);
    }

    static iterator() {
        this.markAccessed();
        return this.getBackend().iterator(this.state);
    }

    /**
     * Connect the model class to a {@link Session}. Invalidates
     * the session-specific cache.
     *
     * @param  {Session} session - The session to connect to.
     */
    static connect(session) {
        if (!session instanceof Session) {
            throw Error('A model can only connect to a Session instance.');
        }
        this._session = session;
        this.clearSessionCache();
    }

    /**
     * Get the current {@link Session} instance.
     *
     * @return {Session} The current {@link Session} instance.
     */
    static get session() {
        return this._session;
    }

    /**
     * A convenience method that delegates to the current {@link Session} instane.
     * Adds the required backenddata about this {@link Model} to the update object.
     * @param {Object} update - the update to add.
     */
    static addUpdate(update) {
        update.meta = {name: this.modelName};
        this.session.addUpdate(update);
    }

    /**
     * Returns the id to be assigned to a new entity.
     * You may override this to suit your needs.
     * @return {*} the id value for a new entity.
     */
    static nextId() {
        if (typeof this._sessionCache.nextId === 'undefined') {
            const idArr = this.accessIds();
            if (idArr.length === 0) {
                this._sessionCache.nextId = 0;
            } else {
                this._sessionCache.nextId = Math.max(...idArr) + 1;
            }
        }
        return this._sessionCache.nextId;
    }

    static getQuerySet() {
        return this.getQuerySetFromIds(this.accessIds());
    }

    static getQuerySetFromIds(ids) {
        const QuerySetClass = this.querySetClass;
        return new QuerySetClass(this, ids);
    }

    static invalidateClassCache() {
        this.isSetUp = undefined;
        this.definedProperties = {};
        this.virtualFields = {};
    }

    static get query() {
        if (!this._sessionCache.queryset) {
            this._sessionCache.queryset = this.getQuerySet();
        }
        return this._sessionCache.queryset;
    }

    /**
     * Returns a {@link QuerySet} containing all {@link Model} instances.
     * @return {QuerySet} a QuerySet containing all {@link Model} instances
     */
    static all() {
        return this.getQuerySet();
    }

    /**
     * Records the addition of a new {@link Model} instance and returns it.
     *
     * @param  {props} props - the new {@link Model}'s properties.
     * @return {Model} a new {@link Model} instance.
     */
    static create(userProps) {
        const idAttribute = this.idAttribute;
        const props = Object.assign({}, userProps);

        if (!props.hasOwnProperty(idAttribute)) {
            const nextId = this.nextId();
            props[idAttribute] = nextId;
            this._sessionCache.nextId++;
        } else {
            const id = props[idAttribute];
            if (id > this.nextId()) {
                this._sessionCache.nextId = id + 1;
            }
        }

        const m2mVals = {};

        forOwn(userProps, (value, key) => {
            props[key] = normalizeEntity(value);

            // If a value is supplied for a ManyToMany field,
            // discard them from props and save for later processing.
            if (isArray(value)) {
                if (this.fields.hasOwnProperty(key) && this.fields[key] instanceof ManyToMany) {
                    m2mVals[key] = value;
                    delete props[key];
                }
            }
        });

        this.addUpdate({
            type: CREATE,
            payload: props,
        });
        const ModelClass = this;
        const instance = new ModelClass(props);

        forOwn(m2mVals, (value, key) => {
            const ids = value.map(normalizeEntity);
            instance[key].add(...ids);
        });

        return instance;
    }

    static withId(id) {
        const ModelClass = this;
        return new ModelClass(this.accessId(id));
    }

    /**
     * Gets the {@link Model} instance that matches properties in `lookupObj`.
     * Throws an error if {@link Model} is not found.
     *
     * @param  {Object} lookupObj - the properties used to match a single entity.
     * @return {Model} a {@link Model} instance that matches `lookupObj` properties.
     */
    static get(lookupObj) {
        if (!this.accessIds().length) {
            throw new Error(`No entities found for model ${this.modelName}`);
        }
        const ModelClass = this;

        // We treat `idAttribute` as unique, so if it's
        // in `lookupObj` we search with that attribute only.
        if (lookupObj.hasOwnProperty(this.idAttribute)) {
            const props = this.accessId(lookupObj[this.idAttribute]);
            if (typeof props !== 'undefined') {
                return new ModelClass(props);
            }

            throw new Error('Model instance not found when calling get method');
        }

        const iterator = this.iterator();

        let done = false;
        while (!done) {
            const curr = iterator.next();
            if (match(lookupObj, curr.value)) {
                return new ModelClass(curr.value);
            }
            done = curr.done;
        }

        throw new Error('Model instance not found when calling get method');
    }

    /**
     * Records an ordering update for the objects.
     * Note that if you create or update any objects after
     * calling this, they won't be in order.
     *
     * @param {function|string|string[]} orderArg - A function, an attribute name or a list of attribute
     *                                              names to order the objects by. If you supply a function,
     *                                              it must return a value user to order the entities.
     * @return {undefined}
     */
    static setOrder(orderArg) {
        this.addUpdate({
            type: ORDER,
            payload: orderArg,
        });
    }

    /**
     * Gets the {@link Model} class or subclass constructor (the class that
     * instantiated this instance).
     *
     * @return {Model} The {@link Model} class or subclass constructor used to instantiate
     *                 this instance.
     */
    getClass() {
        return this.constructor;
    }

    /**
     * Gets the id value of the current instance.
     * @return {*} The id value of the current instance.
     */
    getId() {
        return this._fields[this.getClass().idAttribute];
    }

    /**
     * Returns a string representation of the {@link Model} instance.
     * @return {string} A string representation of this {@link Model} instance.
     */
    toString() {
        const className = this.getClass().modelName;
        const fields = this._fieldNames.map(fieldName => {
            const val = this._fields[fieldName];
            return `${fieldName}: ${val}`;
        }).join(', ');
        return `${className}: {${fields}}`;
    }

    equals(otherModel) {
        return this.getClass() === otherModel.getClass() && this.getId() === otherModel.getId();
    }

    /**
     * Returns a plain JavaScript object representation
     * of the {@link Model} instance.
     * @return {Object} a plain JavaScript object representing the {@link Model}
     */
    toPlain() {
        const obj = {};
        this._fieldNames.forEach((fieldName) => {
            obj[fieldName] = this._fields[fieldName];
        });
        return obj;
    }

    /**
     * Records a update to the {@link Model} instance for a single
     * field value assignment.
     * @param {string} propertyName - name of the property to set
     * @param {*} value - value assigned to the property
     * @return {undefined}
     */
    set(propertyName, value) {
        this.update({[propertyName]: value});
    }

    /**
     * Records a update to the {@link Model} instance for multiple field value assignments.
     * @param  {Object} userMergeObj - an object that will be merged with this instance.
     * @return {undefined}
     */
    update(userMergeObj) {
        const relFields = this.getClass().fields;
        const mergeObj = Object.assign({}, userMergeObj);

        // If an array of entities or id's is supplied for a
        // many-to-many related field, clear the old relations
        // and add the new ones.
        for (const mergeKey in mergeObj) {
            if (relFields.hasOwnProperty(mergeKey)) {
                const field = relFields[mergeKey];
                if (field instanceof ManyToMany) {
                    const currentIds = this[mergeKey].idArr;

                    // TODO: It could be better to check this stuff in Backend.
                    const diffActions = arrayDiffActions(currentIds, mergeObj[mergeKey]);
                    if (diffActions) {
                        const idsToDelete = diffActions.delete;
                        const idsToAdd = diffActions.add;

                        if (idsToDelete.length > 0) {
                            this[mergeKey].filter(item => idsToDelete.indexOf(item.getId()) !== -1)
                                          .delete();
                        }
                        if (idsToAdd.length > 0) {
                            this[mergeKey].add(...idsToAdd);
                        }
                    }
                    delete mergeObj[mergeKey];
                }
            }
        }

        this.getClass().addUpdate({
            type: UPDATE,
            payload: {
                idArr: [this.getId()],
                updater: mergeObj,
            },
        });
    }

    /**
     * Records the {@link Model} to be deleted.
     * @return {undefined}
     */
    delete() {
        this.getClass().addUpdate({
            type: DELETE,
            payload: [this.getId()],
        });
        this._onDelete();
    }

    _onDelete() {
        const virtualFields = this.getClass().virtualFields;
        for (const key in virtualFields) { // eslint-disable-line
            const field = virtualFields[key];
            if (field instanceof ManyToMany) {
                // Delete any many-to-many rows the entity is included in.
                this[key].clear();
            } else if (field instanceof ForeignKey) {
                const relatedQs = this[key];
                if (relatedQs.exists()) {
                    relatedQs.update({[field.relatedName]: null});
                }
            } else if (field instanceof OneToOne) {
                // Set null to any foreign keys or one to ones pointed to
                // this instance.
                if (this[key] !== null ) {
                    this[key][field.relatedName] = null;
                }
            }
        }
    }
};

Model.fields = {};
Model.definedProperties = {};
Model.virtualFields = {};
Model.querySetClass = QuerySet;

export default Model;
<commit_msg>Use the correct .remove method for through QuerySet instead of .delete. Normalize any Model instances passed to Model.update instance method to their ids.<commit_after>import forOwn from 'lodash/object/forOwn';
import isArray from 'lodash/lang/isArray';

import Session from './Session';
import Backend from './Backend';
import QuerySet from './QuerySet';
import {
    ManyToMany,
    ForeignKey,
    OneToOne,
} from './fields';
import {CREATE, UPDATE, DELETE, ORDER} from './constants';
import {
    match,
    normalizeEntity,
    arrayDiffActions,
} from './utils';

/**
 * The heart of an ORM, the data model.
 * The static class methods manages the updates
 * passed to this. The class itself is connected to a session,
 * and because of this you can only have a single session at a time
 * for a {@link Model} class.
 *
 * An instance of {@link Model} represents an object in the database.
 *
 * To create data models in your schema, subclass {@link Model}. To define
 * information about the data model, override static class methods. Define instance
 * logic by defining prototype methods (without `static` keyword).
 */
const Model = class Model {
    /**
     * Creates a Model instance.
     * @param  {Object} props - the properties to instantiate with
     */
    constructor(props) {
        this._initFields(props);
    }

    _initFields(props) {
        const ModelClass = this.getClass();

        this._fieldNames = [];
        this._fields = props;
        const idAttribute = ModelClass.idAttribute;

        forOwn(props, (fieldValue, fieldName) => {
            this._fields[fieldName] = fieldValue;
            this._fieldNames.push(fieldName);

            // If the field has not already been defined on the
            // prototype for a relation.
            if (!ModelClass.definedProperties[fieldName]) {
                Object.defineProperty(this, fieldName, {
                    get: () => fieldValue,
                    set: (value) => {
                        ModelClass.addUpdate({
                            type: UPDATE,
                            payload: {
                                [idAttribute]: this.getId(),
                                [fieldName]: value,
                            },
                        });
                    },
                });
            }
        });
    }

    /**
     * Returns the raw state for this {@link Model} in the current {@link Session}.
     * @return {Object} The state for this {@link Model} in the current {@link Session}.
     */
    static get state() {
        return this.session.getState(this.modelName);
    }

    static toString() {
        return `ModelClass: ${this.modelName}`;
    }

    /**
     * Returns the options object passed to the {@link Backend} class constructor.
     *
     * @return {Object} the options object used to instantiate a {@link Backend} class.
     */
    static backend() {
        return {
            branchName: this.modelName,
        };
    }

    static _getBackendOpts() {
        if (typeof this.backend === 'function') {
            return this.backend();
        }
        if (typeof this.backend === 'undefined') {
            throw new Error(`You must declare either a 'backend' class method or
                            a 'backend' class variable in your Model Class`);
        }
        return this.backend;
    }

    /**
     * Returns the {@link Backend} class used to instantiate
     * the {@link Backend} instance for this {@link Model}.
     *
     * Override this if you want to use a custom {@link Backend} class.
     * @return {Backend} The {@link Backend} class or subclass to use for this {@link Model}.
     */
    static getBackendClass() {
        return Backend;
    }

    static get _sessionCache() {
        if (!this.hasOwnProperty('__sessionCache')) {
            this.__sessionCache = {};
        }
        return this.__sessionCache;
    }

    static clearSessionCache() {
        this.__sessionCache = {};
    }

    /**
     * Gets the {@link Backend} instance linked to this {@link Model}.
     * @return {Backend} The {@link Backend} instance linked to this {@link Model}.
     */
    static getBackend() {
        if (!this._sessionCache.backend) {
            const BackendClass = this.getBackendClass();

            const opts = this._getBackendOpts();
            if (this._session && this._session.withMutations) {
                opts.withMutations = true;
            }

            this._sessionCache.backend = new BackendClass(opts);
        }
        return this._sessionCache.backend;
    }

    /**
     * Gets the Model's next state by applying the recorded
     * updates.
     * @return {Object} The next state.
     */
    static getNextState() {
        if (typeof this.state === 'undefined') {
            return this.getDefaultState();
        }

        const updates = this.session.getUpdatesFor(this);

        return updates.reduce(this.updateReducer.bind(this), this.state);
    }

    static updateReducer(state, action) {
        const backend = this.getBackend();

        switch (action.type) {
        case CREATE:
            return backend.insert(state, action.payload);
        case UPDATE:
            return backend.update(state, action.payload.idArr, action.payload.updater);
        case ORDER:
            return backend.order(state, action.payload);
        case DELETE:
            return backend.delete(state, action.payload);
        default:
            return state;
        }
    }

    /**
     * The default reducer implementation.
     * If the user doesn't define a reducer, this is used.
     *
     * @param {Object} state - the current state
     * @param {Object} action - the dispatched action
     * @param {Model} model - the concrete model class being used
     * @param {Session} session - the current {@link Session} instance
     */
    static reducer(state, action, model, session) {
        return model.getNextState();
    }

    /**
     * Gets the default, empty state of the branch.
     * Delegates to a {@link Backend} instance.
     * @return {Object} The default state.
     */
    static getDefaultState() {
        return this.getBackend().getDefaultState();
    }

    static markAccessed() {
        this.session.markAccessed(this);
    }

    /**
     * Returns the id attribute of this {@link Model}.
     * Delegates to the related {@link Backend} instance.
     *
     * @return {string} The id attribute of this {@link Model}.
     */
    static get idAttribute() {
        return this.getBackend().idAttribute;
    }

    /**
     * A convenience method to call {@link Backend#accessId} from
     * the {@link Model} class.
     *
     * @param  {Number} id - the object id to access
     * @return {Object} a reference to the object in the database.
     */
    static accessId(id) {
        this.markAccessed();
        return this.getBackend().accessId(this.state, id);
    }

    /**
     * A convenience method to call {@link Backend#accessIdList} from
     * the {@link Model} class with the current state.
     */
    static accessIds() {
        this.markAccessed();
        return this.getBackend().accessIdList(this.state);
    }

    static accessList() {
        this.markAccessed();
        return this.getBackend().accessList(this.state);
    }

    static iterator() {
        this.markAccessed();
        return this.getBackend().iterator(this.state);
    }

    /**
     * Connect the model class to a {@link Session}. Invalidates
     * the session-specific cache.
     *
     * @param  {Session} session - The session to connect to.
     */
    static connect(session) {
        if (!session instanceof Session) {
            throw Error('A model can only connect to a Session instance.');
        }
        this._session = session;
        this.clearSessionCache();
    }

    /**
     * Get the current {@link Session} instance.
     *
     * @return {Session} The current {@link Session} instance.
     */
    static get session() {
        return this._session;
    }

    /**
     * A convenience method that delegates to the current {@link Session} instane.
     * Adds the required backenddata about this {@link Model} to the update object.
     * @param {Object} update - the update to add.
     */
    static addUpdate(update) {
        update.meta = {name: this.modelName};
        this.session.addUpdate(update);
    }

    /**
     * Returns the id to be assigned to a new entity.
     * You may override this to suit your needs.
     * @return {*} the id value for a new entity.
     */
    static nextId() {
        if (typeof this._sessionCache.nextId === 'undefined') {
            const idArr = this.accessIds();
            if (idArr.length === 0) {
                this._sessionCache.nextId = 0;
            } else {
                this._sessionCache.nextId = Math.max(...idArr) + 1;
            }
        }
        return this._sessionCache.nextId;
    }

    static getQuerySet() {
        return this.getQuerySetFromIds(this.accessIds());
    }

    static getQuerySetFromIds(ids) {
        const QuerySetClass = this.querySetClass;
        return new QuerySetClass(this, ids);
    }

    static invalidateClassCache() {
        this.isSetUp = undefined;
        this.definedProperties = {};
        this.virtualFields = {};
    }

    static get query() {
        if (!this._sessionCache.queryset) {
            this._sessionCache.queryset = this.getQuerySet();
        }
        return this._sessionCache.queryset;
    }

    /**
     * Returns a {@link QuerySet} containing all {@link Model} instances.
     * @return {QuerySet} a QuerySet containing all {@link Model} instances
     */
    static all() {
        return this.getQuerySet();
    }

    /**
     * Records the addition of a new {@link Model} instance and returns it.
     *
     * @param  {props} props - the new {@link Model}'s properties.
     * @return {Model} a new {@link Model} instance.
     */
    static create(userProps) {
        const idAttribute = this.idAttribute;
        const props = Object.assign({}, userProps);

        if (!props.hasOwnProperty(idAttribute)) {
            const nextId = this.nextId();
            props[idAttribute] = nextId;
            this._sessionCache.nextId++;
        } else {
            const id = props[idAttribute];
            if (id > this.nextId()) {
                this._sessionCache.nextId = id + 1;
            }
        }

        const m2mVals = {};

        forOwn(userProps, (value, key) => {
            props[key] = normalizeEntity(value);

            // If a value is supplied for a ManyToMany field,
            // discard them from props and save for later processing.
            if (isArray(value)) {
                if (this.fields.hasOwnProperty(key) && this.fields[key] instanceof ManyToMany) {
                    m2mVals[key] = value;
                    delete props[key];
                }
            }
        });

        this.addUpdate({
            type: CREATE,
            payload: props,
        });
        const ModelClass = this;
        const instance = new ModelClass(props);

        forOwn(m2mVals, (value, key) => {
            const ids = value.map(normalizeEntity);
            instance[key].add(...ids);
        });

        return instance;
    }

    static withId(id) {
        const ModelClass = this;
        return new ModelClass(this.accessId(id));
    }

    /**
     * Gets the {@link Model} instance that matches properties in `lookupObj`.
     * Throws an error if {@link Model} is not found.
     *
     * @param  {Object} lookupObj - the properties used to match a single entity.
     * @return {Model} a {@link Model} instance that matches `lookupObj` properties.
     */
    static get(lookupObj) {
        if (!this.accessIds().length) {
            throw new Error(`No entities found for model ${this.modelName}`);
        }
        const ModelClass = this;

        // We treat `idAttribute` as unique, so if it's
        // in `lookupObj` we search with that attribute only.
        if (lookupObj.hasOwnProperty(this.idAttribute)) {
            const props = this.accessId(lookupObj[this.idAttribute]);
            if (typeof props !== 'undefined') {
                return new ModelClass(props);
            }

            throw new Error('Model instance not found when calling get method');
        }

        const iterator = this.iterator();

        let done = false;
        while (!done) {
            const curr = iterator.next();
            if (match(lookupObj, curr.value)) {
                return new ModelClass(curr.value);
            }
            done = curr.done;
        }

        throw new Error('Model instance not found when calling get method');
    }

    /**
     * Records an ordering update for the objects.
     * Note that if you create or update any objects after
     * calling this, they won't be in order.
     *
     * @param {function|string|string[]} orderArg - A function, an attribute name or a list of attribute
     *                                              names to order the objects by. If you supply a function,
     *                                              it must return a value user to order the entities.
     * @return {undefined}
     */
    static setOrder(orderArg) {
        this.addUpdate({
            type: ORDER,
            payload: orderArg,
        });
    }

    /**
     * Gets the {@link Model} class or subclass constructor (the class that
     * instantiated this instance).
     *
     * @return {Model} The {@link Model} class or subclass constructor used to instantiate
     *                 this instance.
     */
    getClass() {
        return this.constructor;
    }

    /**
     * Gets the id value of the current instance.
     * @return {*} The id value of the current instance.
     */
    getId() {
        return this._fields[this.getClass().idAttribute];
    }

    /**
     * Returns a string representation of the {@link Model} instance.
     * @return {string} A string representation of this {@link Model} instance.
     */
    toString() {
        const className = this.getClass().modelName;
        const fields = this._fieldNames.map(fieldName => {
            const val = this._fields[fieldName];
            return `${fieldName}: ${val}`;
        }).join(', ');
        return `${className}: {${fields}}`;
    }

    equals(otherModel) {
        return this.getClass() === otherModel.getClass() && this.getId() === otherModel.getId();
    }

    /**
     * Returns a plain JavaScript object representation
     * of the {@link Model} instance.
     * @return {Object} a plain JavaScript object representing the {@link Model}
     */
    toPlain() {
        const obj = {};
        this._fieldNames.forEach((fieldName) => {
            obj[fieldName] = this._fields[fieldName];
        });
        return obj;
    }

    /**
     * Records a update to the {@link Model} instance for a single
     * field value assignment.
     * @param {string} propertyName - name of the property to set
     * @param {*} value - value assigned to the property
     * @return {undefined}
     */
    set(propertyName, value) {
        this.update({[propertyName]: value});
    }

    /**
     * Records a update to the {@link Model} instance for multiple field value assignments.
     * @param  {Object} userMergeObj - an object that will be merged with this instance.
     * @return {undefined}
     */
    update(userMergeObj) {
        const relFields = this.getClass().fields;
        const mergeObj = Object.assign({}, userMergeObj);

        // If an array of entities or id's is supplied for a
        // many-to-many related field, clear the old relations
        // and add the new ones.
        for (const mergeKey in mergeObj) {
            if (relFields.hasOwnProperty(mergeKey)) {
                const field = relFields[mergeKey];
                if (field instanceof ManyToMany) {
                    const currentIds = this[mergeKey].idArr;

                    // TODO: It could be better to check this stuff in Backend.
                    const diffActions = arrayDiffActions(currentIds, mergeObj[mergeKey]);
                    if (diffActions) {
                        const idsToDelete = diffActions.delete;
                        const idsToAdd = diffActions.add;

                        if (idsToDelete.length > 0) {
                            this[mergeKey].remove(idsToDelete);
                        }
                        if (idsToAdd.length > 0) {
                            this[mergeKey].add(...idsToAdd);
                        }
                    }
                    delete mergeObj[mergeKey];
                } else if (field instanceof ForeignKey || field instanceof OneToOne) {
                    mergeObj[mergeKey] = normalizeEntity(mergeObj[mergeKey]);
                }
            }
        }

        this.getClass().addUpdate({
            type: UPDATE,
            payload: {
                idArr: [this.getId()],
                updater: mergeObj,
            },
        });
    }

    /**
     * Records the {@link Model} to be deleted.
     * @return {undefined}
     */
    delete() {
        this.getClass().addUpdate({
            type: DELETE,
            payload: [this.getId()],
        });
        this._onDelete();
    }

    _onDelete() {
        const virtualFields = this.getClass().virtualFields;
        for (const key in virtualFields) { // eslint-disable-line
            const field = virtualFields[key];
            if (field instanceof ManyToMany) {
                // Delete any many-to-many rows the entity is included in.
                this[key].clear();
            } else if (field instanceof ForeignKey) {
                const relatedQs = this[key];
                if (relatedQs.exists()) {
                    relatedQs.update({[field.relatedName]: null});
                }
            } else if (field instanceof OneToOne) {
                // Set null to any foreign keys or one to ones pointed to
                // this instance.
                if (this[key] !== null ) {
                    this[key][field.relatedName] = null;
                }
            }
        }
    }
};

Model.fields = {};
Model.definedProperties = {};
Model.virtualFields = {};
Model.querySetClass = QuerySet;

export default Model;
<|endoftext|>