<commit_before>// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

function readOnlyTransaction(instanceId, databaseId, projectId) {
  // [START spanner_read_only_transaction]
  // Imports the Google Cloud client library
  const {Spanner} = require('@google-cloud/spanner');

  /**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const projectId = 'my-project-id';
  // const instanceId = 'my-instance';
  // const databaseId = 'my-database';

  // Creates a client
  const spanner = new Spanner({
    projectId: projectId,
  });

  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  // Gets a transaction object that captures the database state
  // at a specific point in time
  database.getSnapshot(async (err, transaction) => {
    if (err) {
      console.error(err);
      return;
    }
    const queryOne = 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums';

    try {
      // Read #1, using SQL
      const [qOneRows] = await transaction.run(queryOne);

      qOneRows.forEach(row => {
        const json = row.toJSON();
        console.log(
          `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
        );
      });

      const queryTwo = {
        columns: ['SingerId', 'AlbumId', 'AlbumTitle'],
      };

      // Read #2, using the `read` method. Even if changes occur
      // in-between the reads, the transaction ensures that both
      // return the same data.
      const [qTwoRows] = await transaction.read('Albums', queryTwo);

      qTwoRows.forEach(row => {
        const json = row.toJSON();
        console.log(
          `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
        );
      });

      console.log('Successfully executed read-only transaction.');
    } catch (err) {
      console.error('ERROR:', err);
    } finally {
      transaction.end();
      // Close the database when finished.
      await database.close();
    }
  });
  // [END spanner_read_only_transaction]
}

function readWriteTransaction(instanceId, databaseId, projectId) {
  // [START spanner_read_write_transaction]
  // This sample transfers 200,000 from the MarketingBudget field
  // of the second Album to the first Album, as long as the second
  // Album has enough money in its budget. Make sure to run the
  // addColumn and updateData samples first (in that order).

  // Imports the Google Cloud client library
  const {Spanner} = require('@google-cloud/spanner');

  /**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const projectId = 'my-project-id';
  // const instanceId = 'my-instance';
  // const databaseId = 'my-database';

  // Creates a client
  const spanner = new Spanner({
    projectId: projectId,
  });

  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const transferAmount = 200000;

  database.runTransaction(async (err, transaction) => {
    if (err) {
      console.error(err);
      return;
    }
    let firstBudget, secondBudget;
    const queryOne = {
      columns: [`MarketingBudget`],
      keys: [[2, 2]], // SingerId: 2, AlbumId: 2
    };

    const queryTwo = {
      columns: ['MarketingBudget'],
      keys: [[1, 1]], // SingerId: 1, AlbumId: 1
    };

    Promise.all([
      // Reads the second album's budget
      transaction.read('Albums', queryOne).then(results => {
        // Gets second album's budget
        const rows = results[0].map(row => row.toJSON());
        secondBudget = rows[0].MarketingBudget;
        console.log(`The second album's marketing budget: ${secondBudget}`);

        // Makes sure the second album's budget is large enough
        if (secondBudget < transferAmount) {
          throw new Error(
            `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`
          );
        }
      }),

      // Reads the first album's budget
      transaction.read('Albums', queryTwo).then(results => {
        // Gets first album's budget
        const rows = results[0].map(row => row.toJSON());
        firstBudget = rows[0].MarketingBudget;
        console.log(`The first album's marketing budget: ${firstBudget}`);
      }),
    ])
      .then(() => {
        console.log(firstBudget, secondBudget);
        // Transfers the budgets between the albums
        firstBudget += transferAmount;
        secondBudget -= transferAmount;

        console.log(firstBudget, secondBudget);

        // Updates the database
        // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
        // must be converted (back) to strings before being inserted as INT64s.
        transaction.update('Albums', [
          {
            SingerId: '1',
            AlbumId: '1',
            MarketingBudget: firstBudget.toString(),
          },
          {
            SingerId: '2',
            AlbumId: '2',
            MarketingBudget: secondBudget.toString(),
          },
        ]);
      })
      .then(() => {
        // Commits the transaction and send the changes to the database
        return transaction.commit();
      })
      .then(() => {
        console.log(
          `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`
        );
      })
      .catch(err => {
        console.error('ERROR:', err);
      })
      .then(() => {
        // Closes the database when finished
        return database.close();
      });
  });
  // [END spanner_read_write_transaction]
}

require(`yargs`)
  .demand(1)
  .command(
    `readOnly <instanceName> <databaseName> <projectId>`,
    `Execute a read-only transaction on an example Cloud Spanner table.`,
    {},
    opts =>
      readOnlyTransaction(opts.instanceName, opts.databaseName, opts.projectId)
  )
  .command(
    `readWrite <instanceName> <databaseName> <projectId>`,
    `Execute a read-write transaction on an example Cloud Spanner table.`,
    {},
    opts =>
      readWriteTransaction(opts.instanceName, opts.databaseName, opts.projectId)
  )
  .example(`node $0 readOnly "my-instance" "my-database" "my-project-id"`)
  .example(`node $0 readWrite "my-instance" "my-database" "my-project-id"`)
  .wrap(120)
  .recommendCommands()
  .epilogue(`For more information, see https://cloud.google.com/spanner/docs`)
  .strict()
  .help().argv;
<commit_msg>fix: end readWrite transaction in sample (#766)<commit_after>// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

function readOnlyTransaction(instanceId, databaseId, projectId) {
  // [START spanner_read_only_transaction]
  // Imports the Google Cloud client library
  const {Spanner} = require('@google-cloud/spanner');

  /**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const projectId = 'my-project-id';
  // const instanceId = 'my-instance';
  // const databaseId = 'my-database';

  // Creates a client
  const spanner = new Spanner({
    projectId: projectId,
  });

  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  // Gets a transaction object that captures the database state
  // at a specific point in time
  database.getSnapshot(async (err, transaction) => {
    if (err) {
      console.error(err);
      return;
    }
    const queryOne = 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums';

    try {
      // Read #1, using SQL
      const [qOneRows] = await transaction.run(queryOne);

      qOneRows.forEach(row => {
        const json = row.toJSON();
        console.log(
          `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
        );
      });

      const queryTwo = {
        columns: ['SingerId', 'AlbumId', 'AlbumTitle'],
      };

      // Read #2, using the `read` method. Even if changes occur
      // in-between the reads, the transaction ensures that both
      // return the same data.
      const [qTwoRows] = await transaction.read('Albums', queryTwo);

      qTwoRows.forEach(row => {
        const json = row.toJSON();
        console.log(
          `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
        );
      });

      console.log('Successfully executed read-only transaction.');
    } catch (err) {
      console.error('ERROR:', err);
    } finally {
      transaction.end();
      // Close the database when finished.
      await database.close();
    }
  });
  // [END spanner_read_only_transaction]
}

function readWriteTransaction(instanceId, databaseId, projectId) {
  // [START spanner_read_write_transaction]
  // This sample transfers 200,000 from the MarketingBudget field
  // of the second Album to the first Album, as long as the second
  // Album has enough money in its budget. Make sure to run the
  // addColumn and updateData samples first (in that order).

  // Imports the Google Cloud client library
  const {Spanner} = require('@google-cloud/spanner');

  /**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const projectId = 'my-project-id';
  // const instanceId = 'my-instance';
  // const databaseId = 'my-database';

  // Creates a client
  const spanner = new Spanner({
    projectId: projectId,
  });

  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const transferAmount = 200000;

  database.runTransaction(async (err, transaction) => {
    if (err) {
      console.error(err);
      return;
    }
    let firstBudget, secondBudget;
    const queryOne = {
      columns: [`MarketingBudget`],
      keys: [[2, 2]], // SingerId: 2, AlbumId: 2
    };

    const queryTwo = {
      columns: ['MarketingBudget'],
      keys: [[1, 1]], // SingerId: 1, AlbumId: 1
    };

    Promise.all([
      // Reads the second album's budget
      transaction.read('Albums', queryOne).then(results => {
        // Gets second album's budget
        const rows = results[0].map(row => row.toJSON());
        secondBudget = rows[0].MarketingBudget;
        console.log(`The second album's marketing budget: ${secondBudget}`);

        // Makes sure the second album's budget is large enough
        if (secondBudget < transferAmount) {
          throw new Error(
            `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`
          );
        }
      }),

      // Reads the first album's budget
      transaction.read('Albums', queryTwo).then(results => {
        // Gets first album's budget
        const rows = results[0].map(row => row.toJSON());
        firstBudget = rows[0].MarketingBudget;
        console.log(`The first album's marketing budget: ${firstBudget}`);
      }),
    ])
      .then(() => {
        console.log(firstBudget, secondBudget);
        // Transfers the budgets between the albums
        firstBudget += transferAmount;
        secondBudget -= transferAmount;

        console.log(firstBudget, secondBudget);

        // Updates the database
        // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
        // must be converted (back) to strings before being inserted as INT64s.
        transaction.update('Albums', [
          {
            SingerId: '1',
            AlbumId: '1',
            MarketingBudget: firstBudget.toString(),
          },
          {
            SingerId: '2',
            AlbumId: '2',
            MarketingBudget: secondBudget.toString(),
          },
        ]);
      })
      .then(() => {
        // Commits the transaction and send the changes to the database
        return transaction.commit();
      })
      .then(() => {
        console.log(
          `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`
        );
      })
      .catch(err => {
        console.error('ERROR:', err);
      })
      .then(() => {
        transaction.end();
        // Closes the database when finished
        return database.close();
      });
  });
  // [END spanner_read_write_transaction]
}

require(`yargs`)
  .demand(1)
  .command(
    `readOnly <instanceName> <databaseName> <projectId>`,
    `Execute a read-only transaction on an example Cloud Spanner table.`,
    {},
    opts =>
      readOnlyTransaction(opts.instanceName, opts.databaseName, opts.projectId)
  )
  .command(
    `readWrite <instanceName> <databaseName> <projectId>`,
    `Execute a read-write transaction on an example Cloud Spanner table.`,
    {},
    opts =>
      readWriteTransaction(opts.instanceName, opts.databaseName, opts.projectId)
  )
  .example(`node $0 readOnly "my-instance" "my-database" "my-project-id"`)
  .example(`node $0 readWrite "my-instance" "my-database" "my-project-id"`)
  .wrap(120)
  .recommendCommands()
  .epilogue(`For more information, see https://cloud.google.com/spanner/docs`)
  .strict()
  .help().argv;
<|endoftext|>


<commit_before>var events = require('events'),
	spawn = require("child_process").spawn;

function passEvents(source, dest, events, prefix) {
	prefix = prefix || '';
	events.forEach(function(event) {
		source.on(event, function() {
			var args = Array.prototype.slice.call(arguments, 0);
			args.unshift(prefix + event);
			dest.emit.apply(dest, args);
		});
	});
};

var Instance = function (cmd, args, settings) {
	var self = this,
		process = spawn(cmd, args, settings || {}),
		readableStreamEvents = ['data', 'end', 'error', 'close'];
	this.id = process.pid;
	passEvents(process.stdout, this, readableStreamEvents, 'stdout.');
	passEvents(process.stderr, this, readableStreamEvents, 'stderr.');
	passEvents(process, this, ['exit', 'close', 'disconnect', 'message'], 'process.');
	this.on('process.exit', function(code, signal) {
		self.emit('stop', {
			code : code,
			signal : signal
		});
	});
	this.process = process;
};

Instance.prototype = Object.create(events.EventEmitter.prototype);

Instance.prototype.stop = function (callback) {
	this.once('stop', function (msg) {
		callback(null, msg);
	});
	// send SIGHUP to process
	this.process.kill('SIGHUP');
}

exports.Instance = Instance;

/**
 * Starts a new process.
 *
 * @param cmd The process command line
 * @param args The process arugments
 * @param settings The process environment settings
 * @param callback function(error, instance) Callback after the instance is started
 * @see http://nodejs.org/api/child_process.html#child_process_child_process
 */
exports.start = function (cmd, args, settings) {
	return new Instance(cmd, args, settings);
}
<commit_msg>No empty callbacks<commit_after>var events = require('events'),
	spawn = require("child_process").spawn;

function passEvents(source, dest, events, prefix) {
	prefix = prefix || '';
	events.forEach(function(event) {
		source.on(event, function() {
			var args = Array.prototype.slice.call(arguments, 0);
			args.unshift(prefix + event);
			dest.emit.apply(dest, args);
		});
	});
};

var Instance = function (cmd, args, settings) {
	var self = this,
		process = spawn(cmd, args, settings || {}),
		readableStreamEvents = ['data', 'end', 'error', 'close'];
	this.id = process.pid;
	passEvents(process.stdout, this, readableStreamEvents, 'stdout.');
	passEvents(process.stderr, this, readableStreamEvents, 'stderr.');
	passEvents(process, this, ['exit', 'close', 'disconnect', 'message'], 'process.');
	this.on('process.exit', function(code, signal) {
		self.emit('stop', {
			code : code,
			signal : signal
		});
	});
	this.process = process;
};

Instance.prototype = Object.create(events.EventEmitter.prototype);

Instance.prototype.stop = function (callback) {
	if(callback) {
		this.once('stop', function (msg) {
			callback(null, msg);
		});
	}
	// send SIGHUP to process
	this.process.kill('SIGHUP');
}

exports.Instance = Instance;

/**
 * Starts a new process.
 *
 * @param cmd The process command line
 * @param args The process arugments
 * @param settings The process environment settings
 * @param callback function(error, instance) Callback after the instance is started
 * @see http://nodejs.org/api/child_process.html#child_process_child_process
 */
exports.start = function (cmd, args, settings) {
	return new Instance(cmd, args, settings);
}
<|endoftext|>


<commit_before>'use strict';

const path = require('path');
const EOL = require('os').EOL;
const ADB = require('macaca-adb');
const UnlockApk = require('unlock-apk');
const DriverBase = require('driver-base');
const UIAutomatorWD = require('uiautomatorwd');
const ChromeDriver = require('macaca-chromedriver');

const _ = require('./helper');
const logger = require('./logger');
const controllers = require('./controllers');

const reuseStatus = {};

reuseStatus.noReuse = 0;
reuseStatus.reuseEmu = 1;
reuseStatus.reuseEmuApp = 2;
reuseStatus.reuseEmuAppState = 3;

class Android extends DriverBase {
  constructor() {
    super();
    this.adb = null;
    this.apkInfo = null;
    this.args = null;
    this.chromedriver = null;
    this.chromeDriverPort = null;
    this.proxy = null;
    this.udid = null;
    this.uiautomator = null;
    this.isChrome = null;
    this.isVirtual = true;
    this.contexts = [];
    this.context = null;
    this.isWaitActivity = false;
    this.isActivityReady = false;
  }
}

Android.prototype.startDevice = function * (caps) {
  this.args = _.clone(caps);
  this.isChrome = this.args.browserName && this.args.browserName.toLowerCase() === 'chrome';
  this.initReuse();
  this.initAdb();
  yield this.initDevice();
  yield this.initUiautomator();
  yield this.getApkInfo();
  yield this.unlock();
  yield this.skipPermission();
  yield this.launchApk();

  this.autoAcceptAlerts = Boolean(caps.autoAcceptAlerts);
  this.autoDismissAlerts = Boolean(caps.autoDismissAlerts);
  this.isWaitActivity = Boolean(caps.isWaitActivity);

  if (this.isWaitActivity) {
    yield this.waitActivityReady();
  }

  if (this.isChrome) {
    yield this.getWebviews();
  }
};

Android.prototype.stopDevice = function * () {
  this.chromedriver && this.chromedriver.stop();
  if (this.isVirtual && this.args.reuse === reuseStatus.noReuse) {
    return ADB
      .emuKill()
      .catch(e => {
        logger.warn(e);
      });
  }

  return Promise.resolve();
};

Android.prototype.isProxy = function() {
  return !!this.proxy;
};

Android.prototype.whiteList = function(context) {
  var basename = path.basename(context.url);
  const whiteList = [
    'context',
    'contexts',
    'screenshot',
    'back',
    'tap',
    'source',
    'keys',
    'url',
    'title'
  ];
  return !!~whiteList.indexOf(basename);
};

Android.prototype.proxyCommand = function * (url, method, body) {

  if (this.autoAcceptAlerts) {
    const acceptUrl = '/wd/hub/session/:sessionId/accept_alert';
    yield this.proxy.sendCommand(acceptUrl, 'POST', {});
  } else if (this.autoDismissAlerts) {
    const dismissUrl = '/wd/hub/session/:sessionId/dismiss_alert';
    yield this.proxy.sendCommand(dismissUrl, 'POST', {});
  }

  url = url.replace('property', 'attribute');
  return yield this.proxy.sendCommand(url, method, body);
};

Android.prototype.skipPermission = function *() {
  // execute and let continue install target app, consider make this an external file
  const skipPermissionUrl = `/wd/hub/permission/skip`;
  return yield this.proxy.sendCommand(skipPermissionUrl, 'POST', {
    'value':['继续安装', '下一步', '好', '允许', '确定', '我知道了'],
    'repeat':5});
};

Android.prototype.waitActivityReady = function *() {

  yield _.sleep(1000);

  try {
    this.isActivityReady = yield this.adb.isActivityReady(this.apkInfo.package, this.apkInfo.activity);
  } catch (e) {
    logger.info(`waiting for activity: \`${this.apkInfo.activity}\` ready`);
  }

  if (!this.isActivityReady) {
    yield this.waitActivityReady();
  }
};

Android.prototype.initAdb = function() {
  this.adb = new ADB();
};

Android.prototype.initReuse = function() {
  let reuse = parseInt(this.args.reuse, 10);
  if (!reuse && reuse !== reuseStatus.noReuse) {
    reuse = reuseStatus.reuseEmu;
  }
  this.args.reuse = reuse;
};

Android.prototype.initUiautomator = function * () {
  this.uiautomator = new UIAutomatorWD();
  this.proxy = this.uiautomator;
  yield this.uiautomator.init(this.adb);
};

Android.prototype.initDevice = function * () {

  if (this.args.udid) {
    this.udid = this.args.udid;
    this.adb.setDeviceId(this.udid);
    return;
  }
  var devices = yield ADB.getDevices();
  var device = devices[0];

  if (device) {
    this.adb.setDeviceId(device.udid);
    this.udid = device.udid;
  } else {
    logger.info('no device, now create one from avd');
    var env = global.process.env;
    var emulatorCommand = path.resolve(env.ANDROID_HOME, 'tools', 'emulator');
    var androidCommand = path.resolve(env.ANDROID_HOME, 'tools', 'android');

    var data = yield _.exec(`${androidCommand} list avd`);
    data = data.split(EOL);
    data.shift();

    if (data.length === 0) {
      throw new Error('no avd created! Please create one avd first');
    } else {
      var avdArr = data.filter(avd => {
        return /Name:/.test(avd);
      }).map(avd => {
        return _.trim(avd.split(':')[1]);
      });

      _.exec(`${emulatorCommand} -avd ${avdArr[0]}`);

      var checkEmulator = () => {
        return new Promise((resolve, reject) => {
          ADB.getBootStatus().then(data => {
            resolve(data === 'stopped');
          }).catch(() => {
            reject('check emulator failed');
          });
        });
      };
      yield _.waitForCondition(checkEmulator, 60 * 1000, 2 * 1000);

      devices = yield ADB.getDevices();
      device = devices[0];

      if (device) {
        this.adb.setDeviceId(device.udid);
        this.udid = device.udid;
      } else {
        throw new Error('emulator start failed or too slow!');
      }
    }
  }
  this.isVirtual = device.type === 'virtual';
};

Android.prototype.getApkInfo = function * () {

  if (this.isChrome) {
    this.apkInfo = {
      package: 'com.android.browser',
      activity: '.BrowserActivity'
    };
    return;
  }
  const pkg = this.args.package;
  const activity = this.args.activity;
  const app = this.args.app;
  const androidProcess = this.args.androidProcess;

  if (pkg) {
    this.apkInfo = {
      package: pkg,
      activity: activity,
      androidProcess: androidProcess
    };
  } else if (app) {
    this.apkInfo = yield ADB.getApkMainifest(app);
  } else {
    throw new Error('Either app path or package name should be provided!');
  }
};

Android.prototype.unlock = function * () {

  if (!_.isExistedFile(UnlockApk.apkPath)) {
    logger.warn(`unlock apk not found in: ${UnlockApk.apkPath}`);
    return;
  }

  const isInstalled = yield this.adb.isInstalled(UnlockApk.package);

  if (isInstalled) {
    yield this.checkApkVersion(UnlockApk.apkPath, UnlockApk.package);
  } else {
    yield this.adb.install(UnlockApk.apkPath);
  }

  var isScreenLocked = yield this.adb.isScreenLocked();

  if (isScreenLocked) {
    yield this.adb.startApp(UnlockApk);
    yield _.sleep(5000);
    yield this.unlock();
  }
};

Android.prototype.checkApkVersion = function * (app, pkg) {
  var newVersion = yield ADB.getApkVersion(app);
  var oldVersion = yield this.adb.getInstalledApkVersion(pkg);
  if (newVersion > oldVersion) {
    yield this.adb.install(app);
  }
};

Android.prototype.launchApk = function * () {

  if (!this.isChrome) {
    const reuse = this.args.reuse;
    const app = this.args.app;
    const pkg = this.apkInfo.package;
    const isInstalled = yield this.adb.isInstalled(pkg);
    if (!isInstalled && !app) {
      throw new Error('App is neither installed, nor provided!');
    }
    if (isInstalled) {
      switch (reuse) {
      case reuseStatus.noReuse:
      case reuseStatus.reuseEmu:
        if (app) {
          yield this.adb.unInstall(pkg);
          yield this.adb.install(app);
        } else {
          yield this.adb.clear(pkg);
        }
        break;
      case reuseStatus.reuseEmuApp:
        if (app) {
          yield this.adb.install(app);
        }
        break;
      case reuseStatus.reuseEmuAppState:
        // Keep app state, don't change to main activity.
        this.apkInfo.activity = '';
      }
    } else {
      yield this.adb.install(app);
    }
  }

  logger.debug(`start app with: ${JSON.stringify(this.apkInfo)}`);
  yield this.adb.startApp(this.apkInfo);
  yield _.sleep(5 * 1000);
};

Android.prototype.getWebviews = function * () {
  if (!this.chromedriver) {
    var webviewVersion = null;
    try {
      webviewVersion = yield this.adb.getWebviewVersion();
    } catch (error) {
      logger.info('No webview version found from adb shell!');
      webviewVersion = null;
    }
    try {
      if (webviewVersion) {
        yield this.initChromeDriver({
          webviewVersion: webviewVersion
        });
      } else {
        yield this.initChromeDriver({});
      }
    } catch (error) {
      logger.error('initChromeDriver failed!' + error);
      this.chromedriver = null;
      return [];
    }
  }
  this.proxy = this.chromedriver;

  var webviews = [];

  const result = yield this.proxy.sendCommand('/wd/hub/session/:sessionId/window_handles', 'GET', {});
  webviews = _.parseWebDriverResult(result);

  return webviews;
};

Android.prototype.initChromeDriver = function(options) {
  return new Promise((resolve, reject) => {
    this.chromedriver = new ChromeDriver(options);
    if (this.chromedriver.binPathReady) {
      logger.info('starting chromedriver service!');
      this.chromedriver.start({
        chromeOptions: {
          androidPackage: this.apkInfo.package,
          androidUseRunningApp: true,
          androidDeviceSerial: this.udid,
          androidProcess: this.apkInfo.androidProcess
        }
      });
    }
    this.chromedriver.on(ChromeDriver.BIN_READY, data => {
      logger.info(`chromedriver bin file ready: ${data}`);
    });
    this.chromedriver.on(ChromeDriver.EVENT_READY, data => {
      logger.info(`chromedriver ready with: ${JSON.stringify(data)}`);
      resolve('');
    });
    this.chromedriver.on(ChromeDriver.EVENT_ERROR, data => {
      logger.error(`chromedriver error with: ${data}`);
      reject(data);
    });

    logger.info('starting chromedriver service!');

    this.chromedriver.start({
      chromeOptions: {
        androidPackage: this.apkInfo.package,
        androidUseRunningApp: true,
        androidDeviceSerial: this.udid,
        androidProcess: this.apkInfo.androidProcess
      }
    });
  });
};

_.extend(Android.prototype, controllers);

module.exports = Android;
<commit_msg>update android<commit_after>'use strict';

const path = require('path');
const EOL = require('os').EOL;
const ADB = require('macaca-adb');
const UnlockApk = require('unlock-apk');
const DriverBase = require('driver-base');
const UIAutomatorWD = require('uiautomatorwd');
const ChromeDriver = require('macaca-chromedriver');

const _ = require('./helper');
const logger = require('./logger');
const controllers = require('./controllers');

const reuseStatus = {};

reuseStatus.noReuse = 0;
reuseStatus.reuseEmu = 1;
reuseStatus.reuseEmuApp = 2;
reuseStatus.reuseEmuAppState = 3;

class Android extends DriverBase {
  constructor() {
    super();
    this.adb = null;
    this.apkInfo = null;
    this.args = null;
    this.chromedriver = null;
    this.chromeDriverPort = null;
    this.proxy = null;
    this.udid = null;
    this.uiautomator = null;
    this.isChrome = null;
    this.isVirtual = true;
    this.contexts = [];
    this.context = null;
    this.isWaitActivity = false;
    this.isActivityReady = false;
  }
}

Android.prototype.startDevice = function * (caps) {
  this.args = _.clone(caps);
  this.isChrome = this.args.browserName && this.args.browserName.toLowerCase() === 'chrome';
  this.initReuse();
  this.initAdb();
  yield this.initDevice();
  yield this.initUiautomator(caps.permissionPatterns);
  yield this.getApkInfo();
  yield this.unlock();
  yield this.launchApk();

  this.autoAcceptAlerts = Boolean(caps.autoAcceptAlerts);
  this.autoDismissAlerts = Boolean(caps.autoDismissAlerts);
  this.isWaitActivity = Boolean(caps.isWaitActivity);

  if (this.isWaitActivity) {
    yield this.waitActivityReady();
  }

  if (this.isChrome) {
    yield this.getWebviews();
  }
};

Android.prototype.stopDevice = function * () {
  this.chromedriver && this.chromedriver.stop();
  if (this.isVirtual && this.args.reuse === reuseStatus.noReuse) {
    return ADB
      .emuKill()
      .catch(e => {
        logger.warn(e);
      });
  }

  return Promise.resolve();
};

Android.prototype.isProxy = function() {
  return !!this.proxy;
};

Android.prototype.whiteList = function(context) {
  var basename = path.basename(context.url);
  const whiteList = [
    'context',
    'contexts',
    'screenshot',
    'back',
    'tap',
    'source',
    'keys',
    'url',
    'title'
  ];
  return !!~whiteList.indexOf(basename);
};

Android.prototype.proxyCommand = function * (url, method, body) {

  if (this.autoAcceptAlerts) {
    const acceptUrl = '/wd/hub/session/:sessionId/accept_alert';
    yield this.proxy.sendCommand(acceptUrl, 'POST', {});
  } else if (this.autoDismissAlerts) {
    const dismissUrl = '/wd/hub/session/:sessionId/dismiss_alert';
    yield this.proxy.sendCommand(dismissUrl, 'POST', {});
  }

  url = url.replace('property', 'attribute');
  return yield this.proxy.sendCommand(url, method, body);
};

Android.prototype.waitActivityReady = function * () {

  yield _.sleep(1000);

  try {
    this.isActivityReady = yield this.adb.isActivityReady(this.apkInfo.package, this.apkInfo.activity);
  } catch (e) {
    logger.info(`waiting for activity: \`${this.apkInfo.activity}\` ready`);
  }

  if (!this.isActivityReady) {
    yield this.waitActivityReady();
  }
};

Android.prototype.initAdb = function() {
  this.adb = new ADB();
};

Android.prototype.initReuse = function() {
  let reuse = parseInt(this.args.reuse, 10);
  if (!reuse && reuse !== reuseStatus.noReuse) {
    reuse = reuseStatus.reuseEmu;
  }
  this.args.reuse = reuse;
};

Android.prototype.initUiautomator = function * (permissionPatterns) {
  this.uiautomator = new UIAutomatorWD();
  this.proxy = this.uiautomator;
  permissionPatterns = permissionPatterns || [
    '\"继续安装\"',
    '\"下一步\"',
    '\"好\"',
    '\"允许\"',
    '\"确定\"',
    '\"我知道\"'
  ];

  yield this.uiautomator.init(this.adb, permissionPatterns);
};

Android.prototype.initDevice = function * () {

  if (this.args.udid) {
    this.udid = this.args.udid;
    this.adb.setDeviceId(this.udid);
    return;
  }
  var devices = yield ADB.getDevices();
  var device = devices[0];

  if (device) {
    this.adb.setDeviceId(device.udid);
    this.udid = device.udid;
  } else {
    logger.info('no device, now create one from avd');
    var env = global.process.env;
    var emulatorCommand = path.resolve(env.ANDROID_HOME, 'tools', 'emulator');
    var androidCommand = path.resolve(env.ANDROID_HOME, 'tools', 'android');

    var data = yield _.exec(`${androidCommand} list avd`);
    data = data.split(EOL);
    data.shift();

    if (data.length === 0) {
      throw new Error('no avd created! Please create one avd first');
    } else {
      var avdArr = data.filter(avd => {
        return /Name:/.test(avd);
      }).map(avd => {
        return _.trim(avd.split(':')[1]);
      });

      _.exec(`${emulatorCommand} -avd ${avdArr[0]}`);

      var checkEmulator = () => {
        return new Promise((resolve, reject) => {
          ADB.getBootStatus().then(data => {
            resolve(data === 'stopped');
          }).catch(() => {
            reject('check emulator failed');
          });
        });
      };
      yield _.waitForCondition(checkEmulator, 60 * 1000, 2 * 1000);

      devices = yield ADB.getDevices();
      device = devices[0];

      if (device) {
        this.adb.setDeviceId(device.udid);
        this.udid = device.udid;
      } else {
        throw new Error('emulator start failed or too slow!');
      }
    }
  }
  this.isVirtual = device.type === 'virtual';
};

Android.prototype.getApkInfo = function * () {

  if (this.isChrome) {
    this.apkInfo = {
      package: 'com.android.browser',
      activity: '.BrowserActivity'
    };
    return;
  }
  const pkg = this.args.package;
  const activity = this.args.activity;
  const app = this.args.app;
  const androidProcess = this.args.androidProcess;

  if (pkg) {
    this.apkInfo = {
      package: pkg,
      activity: activity,
      androidProcess: androidProcess
    };
  } else if (app) {
    this.apkInfo = yield ADB.getApkMainifest(app);
  } else {
    throw new Error('Either app path or package name should be provided!');
  }
};

Android.prototype.unlock = function * () {

  if (!_.isExistedFile(UnlockApk.apkPath)) {
    logger.warn(`unlock apk not found in: ${UnlockApk.apkPath}`);
    return;
  }

  const isInstalled = yield this.adb.isInstalled(UnlockApk.package);

  if (isInstalled) {
    yield this.checkApkVersion(UnlockApk.apkPath, UnlockApk.package);
  } else {
    yield this.adb.install(UnlockApk.apkPath);
  }

  var isScreenLocked = yield this.adb.isScreenLocked();

  if (isScreenLocked) {
    yield this.adb.startApp(UnlockApk);
    yield _.sleep(5000);
    yield this.unlock();
  }
};

Android.prototype.checkApkVersion = function * (app, pkg) {
  var newVersion = yield ADB.getApkVersion(app);
  var oldVersion = yield this.adb.getInstalledApkVersion(pkg);
  if (newVersion > oldVersion) {
    yield this.adb.install(app);
  }
};

Android.prototype.launchApk = function * () {

  if (!this.isChrome) {
    const reuse = this.args.reuse;
    const app = this.args.app;
    const pkg = this.apkInfo.package;
    const isInstalled = yield this.adb.isInstalled(pkg);
    if (!isInstalled && !app) {
      throw new Error('App is neither installed, nor provided!');
    }
    if (isInstalled) {
      switch (reuse) {
      case reuseStatus.noReuse:
      case reuseStatus.reuseEmu:
        if (app) {
          yield this.adb.unInstall(pkg);
          yield this.adb.install(app);
        } else {
          yield this.adb.clear(pkg);
        }
        break;
      case reuseStatus.reuseEmuApp:
        if (app) {
          yield this.adb.install(app);
        }
        break;
      case reuseStatus.reuseEmuAppState:
        // Keep app state, don't change to main activity.
        this.apkInfo.activity = '';
      }
    } else {
      yield this.adb.install(app);
    }
  }

  logger.debug(`start app with: ${JSON.stringify(this.apkInfo)}`);
  yield this.adb.startApp(this.apkInfo);
  yield _.sleep(5 * 1000);
};

Android.prototype.getWebviews = function * () {
  if (!this.chromedriver) {
    var webviewVersion = null;
    try {
      webviewVersion = yield this.adb.getWebviewVersion();
    } catch (error) {
      logger.info('No webview version found from adb shell!');
      webviewVersion = null;
    }
    try {
      if (webviewVersion) {
        yield this.initChromeDriver({
          webviewVersion: webviewVersion
        });
      } else {
        yield this.initChromeDriver({});
      }
    } catch (error) {
      logger.error('initChromeDriver failed!' + error);
      this.chromedriver = null;
      return [];
    }
  }
  this.proxy = this.chromedriver;

  var webviews = [];

  const result = yield this.proxy.sendCommand('/wd/hub/session/:sessionId/window_handles', 'GET', {});
  webviews = _.parseWebDriverResult(result);

  return webviews;
};

Android.prototype.initChromeDriver = function(options) {
  return new Promise((resolve, reject) => {
    this.chromedriver = new ChromeDriver(options);
    if (this.chromedriver.binPathReady) {
      logger.info('starting chromedriver service!');
      this.chromedriver.start({
        chromeOptions: {
          androidPackage: this.apkInfo.package,
          androidUseRunningApp: true,
          androidDeviceSerial: this.udid,
          androidProcess: this.apkInfo.androidProcess
        }
      });
    }
    this.chromedriver.on(ChromeDriver.BIN_READY, data => {
      logger.info(`chromedriver bin file ready: ${data}`);
    });
    this.chromedriver.on(ChromeDriver.EVENT_READY, data => {
      logger.info(`chromedriver ready with: ${JSON.stringify(data)}`);
      resolve('');
    });
    this.chromedriver.on(ChromeDriver.EVENT_ERROR, data => {
      logger.error(`chromedriver error with: ${data}`);
      reject(data);
    });

    logger.info('starting chromedriver service!');

    this.chromedriver.start({
      chromeOptions: {
        androidPackage: this.apkInfo.package,
        androidUseRunningApp: true,
        androidDeviceSerial: this.udid,
        androidProcess: this.apkInfo.androidProcess
      }
    });
  });
};

_.extend(Android.prototype, controllers);

module.exports = Android;
<|endoftext|>


<commit_before>const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const axios = require('axios')
const uuid = require('uuid')

const db = require('./db')
const pairing = require('./pairing')
const { checkPings, checkStuckScreen } = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
const notifierUtils = require('./notifier/utils')
const notifierQueries = require('./notifier/queries')

const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
const stuckStatus = { label: 'Stuck', type: 'error' }

function getMachines () {
  return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
    .then(rr => rr.map(r => ({
      deviceId: r.device_id,
      cashbox: r.cashbox,
      cassette1: r.cassette1,
      cassette2: r.cassette2,
      version: r.version,
      model: r.model,
      pairedAt: new Date(r.created),
      lastPing: new Date(r.last_online),
      name: r.name,
      // TODO: we shall start using this JSON field at some point
      // location: r.location,
      paired: r.paired
    })))
}

function getConfig (defaultConfig) {
  if (defaultConfig) return Promise.resolve(defaultConfig)

  return settingsLoader.loadLatest().config
}

const getStatus = (ping, stuck) => {
  if (ping && ping.age) return unresponsiveStatus

  if (stuck && stuck.age) return stuckStatus

  return fullyFunctionalStatus
}

function addName (pings, events, config) {
  return machine => {
    const cashOutConfig = configManager.getCashOut(machine.deviceId, config)

    const cashOut = !!cashOutConfig.active

    const statuses = [
      getStatus(
        _.first(pings[machine.deviceId]),
        _.first(checkStuckScreen(events, machine.name))
        )
      ]

    return _.assign(machine, { cashOut, statuses })
  }
}

function getMachineNames (config) {
  return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
    .then(([rawMachines, config, heartbeat, performance]) => Promise.all(
      [rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
    ))
    .then(([rawMachines, pings, events, config, heartbeat, performance]) => {
      const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
      const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)

      const getStatus = (ping, stuck) => {
        if (ping && ping.age) return unresponsiveStatus

        if (stuck && stuck.age) return stuckStatus

        return fullyFunctionalStatus
      }

      const addName = r => {
        const cashOutConfig = configManager.getCashOut(r.deviceId, config)

        const cashOut = !!cashOutConfig.active

        const statuses = [
          getStatus(
            _.first(pings[r.deviceId]),
            _.first(checkStuckScreen(events, r.name))
          )
        ]

        return _.assign(r, { cashOut, statuses })
      }
      return _.map(addName, machines)
    })
}

/**
 * Given the machine id, get the machine name
 *
 * @name getMachineName
 * @function
 * @async
 *
 * @param {string} machineId machine id
 * @returns {string} machine name
 */
function getMachineName (machineId) {
  const sql = 'SELECT * FROM devices WHERE device_id=$1'
  return db.oneOrNone(sql, [machineId])
    .then(it => it.name)
}

function getMachine (machineId, config) {
  const sql = 'SELECT * FROM devices WHERE device_id=$1'
  const queryMachine = db.oneOrNone(sql, [machineId]).then(r => ({
    deviceId: r.device_id,
    cashbox: r.cashbox,
    cassette1: r.cassette1,
    cassette2: r.cassette2,
    version: r.version,
    model: r.model,
    pairedAt: new Date(r.created),
    lastPing: new Date(r.last_online),
    name: r.name,
    paired: r.paired
  }))

  return Promise.all([queryMachine, dbm.machineEvents(), config])
      .then(([machine, events, config]) => {
        const pings = checkPings([machine])

      return [machine].map(addName(pings, events, config))[0]
    })
}

function renameMachine (rec) {
  const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
  return db.none(sql, [rec.newName, rec.deviceId])
}

function resetCashOutBills (rec) {
  const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
  const sql = `UPDATE devices SET cassette1=$1, cassette2=$2 WHERE device_id=$3;`
  return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
}

function emptyCashInBills (rec) {
  const sql = 'UPDATE devices SET cashbox=0 WHERE device_id=$1'
  return db.none(sql, [rec.deviceId])
}

function setCassetteBills (rec) {
  const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3 where device_id=$4'
  return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.deviceId])
}

function unpair (rec) {
  return pairing.unpair(rec.deviceId)
}

function reboot (rec) {
  return axios.post(`http://localhost:3030/reboot?device_id=${rec.deviceId}`)
}

function shutdown (rec) {
  return axios.post(`http://localhost:3030/shutdown?device_id=${rec.deviceId}`)
}

function restartServices (rec) {
  return axios.post(`http://localhost:3030/restartServices?device_id=${rec.deviceId}`)
}

function setMachine (rec) {
  switch (rec.action) {
    case 'rename': return renameMachine(rec)
    case 'emptyCashInBills': return emptyCashInBills(rec)
    case 'resetCashOutBills': return resetCashOutBills(rec)
    case 'setCassetteBills': return setCassetteBills(rec)
    case 'unpair': return unpair(rec)
    case 'reboot': return reboot(rec)
    case 'shutdown': return shutdown(rec)
    case 'restartServices': return restartServices(rec)
    default: throw new Error('No such action: ' + rec.action)
  }
}

function updateNetworkPerformance (deviceId, data) {
  const downloadSpeed = _.head(data)
  const dbData = {
    device_id: deviceId,
    download_speed: downloadSpeed.speed,
    created: new Date()
  }
  const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'],
    { table: 'machine_network_performance' })
  const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' +
    cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
  const upsert = pgp.helpers.insert(dbData, cs) + onConflict
  return db.none(upsert)
}

function updateNetworkHeartbeat (deviceId, data) {
  const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
  const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
  const dbData = {
    id: uuid.v4(),
    device_id: deviceId,
    average_response_time: avgResponseTime,
    average_packet_loss: avgPacketLoss
  }
  const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
  return db.none(sql)
}

function getNetworkPerformance () {
  const sql = `SELECT device_id, download_speed FROM machine_network_performance`
  return db.manyOrNone(sql)
    .then(res => _.map(_.mapKeys(_.camelCase))(res))
}

function getNetworkHeartbeat () {
  const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
    FROM machine_network_heartbeat
    GROUP BY device_id`
  return db.manyOrNone(sql)
    .then(res => _.map(_.mapKeys(_.camelCase))(res))
}

module.exports = {
  getMachineName,
  getMachines,
  getMachine,
  getMachineNames,
  setMachine,
  updateNetworkPerformance,
  updateNetworkHeartbeat,
  getNetworkPerformance,
  getNetworkHeartbeat
}
<commit_msg>feat: ping time internet quality measurement<commit_after>const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const axios = require('axios')
const uuid = require('uuid')

const db = require('./db')
const pairing = require('./pairing')
const { checkPings, checkStuckScreen } = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
const notifierUtils = require('./notifier/utils')
const notifierQueries = require('./notifier/queries')

const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
const stuckStatus = { label: 'Stuck', type: 'error' }

function getMachines () {
  return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
    .then(rr => rr.map(r => ({
      deviceId: r.device_id,
      cashbox: r.cashbox,
      cassette1: r.cassette1,
      cassette2: r.cassette2,
      version: r.version,
      model: r.model,
      pairedAt: new Date(r.created),
      lastPing: new Date(r.last_online),
      name: r.name,
      // TODO: we shall start using this JSON field at some point
      // location: r.location,
      paired: r.paired
    })))
}

function getConfig (defaultConfig) {
  if (defaultConfig) return Promise.resolve(defaultConfig)

  return settingsLoader.loadLatest().config
}

const getStatus = (ping, stuck) => {
  if (ping && ping.age) return unresponsiveStatus

  if (stuck && stuck.age) return stuckStatus

  return fullyFunctionalStatus
}

function addName (pings, events, config) {
  return machine => {
    const cashOutConfig = configManager.getCashOut(machine.deviceId, config)

    const cashOut = !!cashOutConfig.active

    const statuses = [
      getStatus(
        _.first(pings[machine.deviceId]),
        _.first(checkStuckScreen(events, machine.name))
      )
    ]

    return _.assign(machine, { cashOut, statuses })
  }
}

function getMachineNames (config) {
  return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
    .then(([rawMachines, config, heartbeat, performance]) => Promise.all(
      [rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
    ))
    .then(([rawMachines, pings, events, config, heartbeat, performance]) => {
      const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
      const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)

      const getStatus = (ping, stuck) => {
        if (ping && ping.age) return unresponsiveStatus

        if (stuck && stuck.age) return stuckStatus

        return fullyFunctionalStatus
      }

      const addName = r => {
        const cashOutConfig = configManager.getCashOut(r.deviceId, config)

        const cashOut = !!cashOutConfig.active

        const statuses = [
          getStatus(
            _.first(pings[r.deviceId]),
            _.first(checkStuckScreen(events, r.name))
          )
        ]

        return _.assign(r, { cashOut, statuses })
      }
      return _.map(addName, machines)
    })
}

/**
 * Given the machine id, get the machine name
 *
 * @name getMachineName
 * @function
 * @async
 *
 * @param {string} machineId machine id
 * @returns {string} machine name
 */
function getMachineName (machineId) {
  const sql = 'SELECT * FROM devices WHERE device_id=$1'
  return db.oneOrNone(sql, [machineId])
    .then(it => it.name)
}

function getMachine (machineId, config) {
  const sql = 'SELECT * FROM devices WHERE device_id=$1'
  const queryMachine = db.oneOrNone(sql, [machineId]).then(r => ({
    deviceId: r.device_id,
    cashbox: r.cashbox,
    cassette1: r.cassette1,
    cassette2: r.cassette2,
    version: r.version,
    model: r.model,
    pairedAt: new Date(r.created),
    lastPing: new Date(r.last_online),
    name: r.name,
    paired: r.paired
  }))

  return Promise.all([queryMachine, dbm.machineEvents(), config])
    .then(([machine, events, config]) => {
      const pings = checkPings([machine])

      return [machine].map(addName(pings, events, config))[0]
    })
}

function renameMachine (rec) {
  const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
  return db.none(sql, [rec.newName, rec.deviceId])
}

function resetCashOutBills (rec) {
  const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
  const sql = `UPDATE devices SET cassette1=$1, cassette2=$2 WHERE device_id=$3;`
  return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
}

function emptyCashInBills (rec) {
  const sql = 'UPDATE devices SET cashbox=0 WHERE device_id=$1'
  return db.none(sql, [rec.deviceId])
}

function setCassetteBills (rec) {
  const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3 where device_id=$4'
  return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.deviceId])
}

function unpair (rec) {
  return pairing.unpair(rec.deviceId)
}

function reboot (rec) {
  return axios.post(`http://localhost:3030/reboot?device_id=${rec.deviceId}`)
}

function shutdown (rec) {
  return axios.post(`http://localhost:3030/shutdown?device_id=${rec.deviceId}`)
}

function restartServices (rec) {
  return axios.post(`http://localhost:3030/restartServices?device_id=${rec.deviceId}`)
}

function setMachine (rec) {
  switch (rec.action) {
    case 'rename': return renameMachine(rec)
    case 'emptyCashInBills': return emptyCashInBills(rec)
    case 'resetCashOutBills': return resetCashOutBills(rec)
    case 'setCassetteBills': return setCassetteBills(rec)
    case 'unpair': return unpair(rec)
    case 'reboot': return reboot(rec)
    case 'shutdown': return shutdown(rec)
    case 'restartServices': return restartServices(rec)
    default: throw new Error('No such action: ' + rec.action)
  }
}

function updateNetworkPerformance (deviceId, data) {
  const downloadSpeed = _.head(data)
  const dbData = {
    device_id: deviceId,
    download_speed: downloadSpeed.speed,
    created: new Date()
  }
  const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'],
    { table: 'machine_network_performance' })
  const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' +
    cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
  const upsert = pgp.helpers.insert(dbData, cs) + onConflict
  return db.none(upsert)
}

function updateNetworkHeartbeat (deviceId, data) {
  const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
  const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
  const dbData = {
    id: uuid.v4(),
    device_id: deviceId,
    average_response_time: avgResponseTime,
    average_packet_loss: avgPacketLoss
  }
  const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
  return db.none(sql)
}

function getNetworkPerformance () {
  const sql = `SELECT device_id, download_speed FROM machine_network_performance`
  return db.manyOrNone(sql)
    .then(res => _.map(_.mapKeys(_.camelCase))(res))
}

function getNetworkHeartbeat () {
  const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
    FROM machine_network_heartbeat
    GROUP BY device_id`
  return db.manyOrNone(sql)
    .then(res => _.map(_.mapKeys(_.camelCase))(res))
}

module.exports = {
  getMachineName,
  getMachines,
  getMachine,
  getMachineNames,
  setMachine,
  updateNetworkPerformance,
  updateNetworkHeartbeat,
  getNetworkPerformance,
  getNetworkHeartbeat
}
<|endoftext|>


<commit_before>export default class Nativetable {

  /**
   * Data sources
   * Getter
   *
   * @return {Object[]}
   */
  get sources() {
    return this.data.sources
  }

  /**
   * Data sources
   * Setter
   *
   * @return {Object[]}
   */
  set sources(rows) {
    this.options.reloading.filtered = true
    this.options.reloading.sorted = true
    this.options.reloading.paginated = true
    this.data.sources = rows
  }

  /**
   * Filters rules
   * Getter
   *
   * @return {Object}
   */
  get filters() {
    return this.options.filters
  }

  /**
   * Filters rules
   * Setter
   *
   * @return {Object}
   */
  set filters(filters) {
    this.options.reloading.filtered = true
    this.options.filters = filters
  }

  /**
   * Data sources filtered by filters
   * Getter
   *
   * @return {Object[]}
   */
  get filtered() {
    const filters = this.options.filters

    if ( // pagination cached
      this.data.filtered &&
      this.data.filtered.length > 0 &&
      this.options.reloading.filtered === false
    ) {
      return this.data.filtered
    }

    if (Object.keys(filters).length === 0) {
      this.data.filtered = this.sources

      this.options.reloading.filtered = false
      return this.data.filtered
    }

    /**
     * Closure
     * AND logical condition
     *
     * @param {Object[]} items - array of boolean value
     * @returns {boolean}
     */
    let $and = (items) => {
      let condition = true
      for (let item of items) {
        condition = condition && item
      }

      return condition
    }

    /**
     * Closure
     * OR logical condition
     *
     * @param {Object[]} items - array of boolean value
     * @returns {boolean}
     */
    let $or = (items) => {
      let condition = false
      for (let item of items) {
        condition = condition || item
      }

      return condition
    }

    /**
     * Closure
     * Calculate conditionnal result for one columns
     *
     * @param {Object} item - item to test
     * @param {function|any[]} - array of value to match or a closure for custom condition
     *
     * @return {boolean}
     */
    let $resultForColumn = (item, condition) => {
      if (typeof condition === 'function') { // custom condition
        let closure = condition
        let result = closure(item)
        if (typeof result === 'boolean') {
          return result
        }

        return false // bad return type of closure
      }

      let column = []
      for (let value of condition) { // normal condition
        column.push(item === value)
      }

      return $or(column)
    }

    this.data.filtered = this.sources.filter((item) => {
      let result = []
      if ('$and' in filters) {
        let keys = Object.keys(filters.$and)
        let conditions = filters['$and']
        let resultAND = []

        for (let key of keys) {
          resultAND.push($resultForColumn(item[key], conditions[key]))
        }

        result.push($and(resultAND))
      }

      if ('$or' in filters) {
        let keys = Object.keys(filters.$or)
        let conditions = filters['$or']
        let resultOR = []

        for (let key of keys) {
          resultOR.push($resultForColumn(item[key], conditions[key]))
        }

        result.push($or(resultOR))
      }

      return $and(result)
    })

    this.options.reloading.filtered = false
    return this.data.filtered
  }

  /**
   * Data sources sorted
   * Getter
   *
   * @return {Object[]}
   */
  get sorted() {
    if ( // pagination cached
      this.data.sorted &&
      this.data.sorted.length > 0 &&
      this.options.reloading.sorted === false
    ) {
      return this.data.sorted
    }

    const sources = this.filtered
    const { column, order = 'none', activated: isSortingActivated } = this.options.sorting

    if (
      sources.length <= 0 ||
      isSortingActivated === false ||
      typeof column === 'undefined'
    ) { // no sources or no sorting
      this.data.sorted = sources

      this.options.reloading.sorted = false
      return this.data.sorted
    }

    let tmpArray = sources.map((e, i) => { // create temporary array that is easier to sort
      let el = e[column]

      if (typeof e[column] === 'string') {
        el = e[column].toLowerCase()
      }

      if (typeof e[column] === 'undefined') {
        el = ''
      }

      return {
        index: i,
        value: el
      }
    })

    tmpArray.sort((a, b) => {
      if (a.value > b.value) {
        return order === 'asc' ? 1 : -1
      }

      if (a.value  { // rebuild sources in data.sorted
      return sources[e.index]
    })

    this.options.reloading.sorted = false
    return this.data.sorted
  }

  /**
   * Data sources paginated
   * Getter
   *
   * @return {Object[]}
   */
  get paginated() {
    if ( // pagination cached
      this.data.paginated &&
      this.data.paginated.length > 0 &&
      this.options.reloading.paginated === false
    ) {
      return this.data.paginated
    }

    const sources = this.sorted
    const maxLength = this.options.pagination.maxLength

    if (sources.length <= 0) { // no sources
      this.data.paginated = []

      this.options.reloading.paginated = false
      return this.data.paginated
    }

    if (maxLength === -1) { // no pagination
      this.data.paginated = [ sources ]
      return this.data.paginated
    }

    let pages = []
    let page = []

    for (let row of sources) {
      if (page.length >= maxLength) {
        pages.push(page)
        page = []
      }

      page.push(row)
    }
    pages.push(page)

    this.data.paginated = pages
    this.options.reloading.paginated = false
    return this.data.paginated
  }

  /**
   * Colum's nouns getter
   *
   * @return {Object[]} - column
   */
  get columns() {
    let headers = this.options.columns || []

    if (headers.length > 0) {
      return headers
    }

    this.filtered.forEach((el) => {
      let tmpHeaders = Object.keys(el)

      for (let col of tmpHeaders) {
        if (headers.indexOf(col) !== -1) {
          continue
        }

        headers.push(col)
      }
    })

    this.options.columns = headers.map((el) => {
      return {
        key: el,
        title: el
      }
    })
    return this.options.columns
  }

  /**
   * Colum's nouns setter
   *
   * @param {string[]|Object[]} value - column
   */
  set columns(value) {
    this.options.columns = []

    if (value == null) {
      return
    }

    for (let columns of value) {
      let { key, title } = columns

      if (typeof columns === 'string') {
        key = title = columns
      }

      if (typeof key === 'undefined' || typeof title === 'undefined') {
        continue
      }

      this.options.columns.push({
        key,
        title
      })
    }
  }

  /**
   * Setting pagination
   * Setter
   *
   * @param {number} maxLength - number max of elements per page
   */
  set pagination({ maxLength = -1 }) {
    this.options.reloading.paginated = true
    this.options.pagination.maxLength = maxLength
  }

  /**
   * Setting pagination
   * Getter
   *
   * @return {Object} current pagination settings
   */
  get pagination() {
    return {
      currentPage: this.options.pagination.currentPage,
      maxLength: this.options.pagination.maxLength
    }
  }

  /**
   * @param {string}    id                             - identifiant of
 tag targeted
   * @param {Object}    [options]                      - options of Nativetable
   * @param {Object[]}  [options.sources]              - array of source values
   * @param {Object}    [options.filters]              - filters to applies
   * @param {string[]}  [options.columns]              - column's nouns
   * @param {Object}    [options.pagination]           - options for pagination
   * @param {Object}    [options.pagination.maxLength] - number max of elements per page
   * @param {boolean}   [options.sorting]              - a flag that activates sorting
   *
   * @throws {TypeError} when the id parameter is invalid
   *
   * @return {Nativetable} - an instance of Nativetable
   */
  constructor(id, { sources = [], filters = {}, columns = [], pagination: { maxLength = -1 } = {}, sorting = false } = {}) {
    this.options = {}
    this.data = {}

    if (typeof id !== 'string' || document.getElementById(id) == null) {
      throw new TypeError('First parameter of Nativetable need to be a valid tag ID.')
    }

    this.options.id = id
    this.options.box = document.getElementById(id)
    this.options.reloading = {}
    this.options.filters = filters
    this.options.sorting = {
      activated: sorting
    }
    this.options.pagination = {
      currentPage: 0,
      maxLength
    }

    this.columns = columns
    this.sources = sources

    this.draw()
  }

  /**
   * Reload data before drawing table
   *
   * @param {Object[]} [rows] - objects to reload
   */
  reload(rows = []) {
    this.options.pagination.currentPage = 0
    this.sources = rows.length > 0 ? rows : this.sources

    this.options.reloading.filtered = true
    this.options.reloading.paginated = true

    this.draw()
  }

  /**
   * Draw HTML table.
   */
  draw() {
    const sources = this.paginated
    const isPaginated = sources.length !== 1

    if (sources.length <= 0) {
      return
    }

    let currentPage = this.options.pagination.currentPage
    currentPage = currentPage < sources.length ? currentPage : 0

    let tableTag, theadTag, tbodyTag, paginationTag

    if ( // Delete pagination if exist
      this.options.box.children[1] &&
      this.options.box.children[1].classList.contains('nt-pagination')
    ) {
      paginationTag = this.options.box.children[1]
      this.options.box.removeChild(paginationTag)
    }

    if (isPaginated) { // Rebuild pagination if data is paginated
      paginationTag = this.buildPagination(sources, currentPage)
      this.options.box.appendChild(paginationTag)
    }

    if ( // Doesn't recreate the full table if already exist
      this.options.box.children[0] &&
      this.options.box.children[0].classList.contains('nt-table')
    ) {
      tableTag = this.options.box.children[0]
      theadTag = tableTag.children[0]
      tbodyTag = tableTag.children[1]
      tableTag.removeChild(tbodyTag)

      if (this.options.reloading.headers) {
        tableTag.removeChild(theadTag)
        theadTag = this.buildTableHeader(this.columns)
        tableTag.appendChild(theadTag)
      }

      tbodyTag = this.buildTableBody(sources[currentPage], this.columns)
      tableTag.appendChild(tbodyTag)

      return
    }

    // Creation
    tableTag = document.createElement('table')
    theadTag = this.buildTableHeader(this.columns)
    tbodyTag = this.buildTableBody(sources[currentPage], this.columns)

    tableTag.classList.add('nt-table')

    tableTag.appendChild(theadTag)
    tableTag.appendChild(tbodyTag)

    this.options.box.insertBefore(tableTag, paginationTag)
  }

  /**
   * Builder for table header
   *
   * @param {Object[]} cols - Array of columns
   *
   * @return {HTMLElement} A thead HTML tag
   */
  buildTableHeader(cols) {
    const columns = cols

    let theadTag = document.createElement('thead')
    let trTag = document.createElement('tr')

    theadTag.classList.add('nt-head')

    for (let { key, title } of columns) {
      let tdTag = document.createElement('td')
      tdTag.textContent = title
      tdTag.dataset.ntColumnName = key

      if (this.options.sorting.activated) {
        const glyphList = {
          asc: '',
          desc: '',
          none: ''
        }
        let order = this.options.sorting.column === key ? this.options.sorting.order : 'none'
        let glyph = glyphList[order]
        let aTag = document.createElement('a')

        aTag.href = '#'
        aTag.addEventListener('click', this.onSortingClick.bind(this))
        aTag.innerHTML = `${title} ${glyph}`

        tdTag.textContent = ''
        tdTag.appendChild(aTag)
      }

      trTag.appendChild(tdTag)
    }

    theadTag.appendChild(trTag)
    return theadTag
  }

  /**
   * Builder for table body
   *
   * @param {Object[]} rows - All rows to display
   * @param {string[]} cols - Array of columns
   *
   * @return {HTMLElement} A tbody HTML tag with all rows inside
   */
  buildTableBody(rows, cols) {
    const sources = rows
    const columns = cols

    let tbodyTag = document.createElement('tbody')
    tbodyTag.classList.add('nt-body')

    for (let row of sources) {
      let trTag = document.createElement('tr')

      trTag.classList.add('nt-row')
      trTag.dataset.ntObject = this.objectSignature(row)

      for (let name of columns) {
        let tdTag = document.createElement('td')
        tdTag.textContent = typeof row[name] === 'undefined' ? '' : row[name]
        trTag.appendChild(tdTag)
      }

      tbodyTag.appendChild(trTag)
    }

    return tbodyTag
  }

  /**
   * Builder for pagination ul
   *
   * @param {array[]} pages - Paginated rows
   * @param {number} [current] - Number of current page
   *
   * @return {HTMLElement} A ul HTML tag for pagination
   */
  buildPagination(pages, current = 0) {
    const sources = pages

    let ulTag = document.createElement('ul')
    for (let index in sources) {
      let liTag = document.createElement('li')
      let aTag = document.createElement('a')

      index = +index // casting to number

      aTag.href = '#'
      aTag.textContent = index + 1
      aTag.addEventListener('click', this.onPaginationClick.bind(this))

      liTag.classList.add('nt-pagination-item')
      liTag.dataset.ntPaginationIndex = index
      liTag.appendChild(aTag)

      if (index === current) {
        liTag.classList.add('nt-pagination-item-active')
      }

      ulTag.appendChild(liTag)
    }

    ulTag.classList.add('nt-pagination')
    return ulTag
  }

  /**
   * Event handler. Called when user click on pagination links
   *
   * @param {Event} event - the event
   */
  onPaginationClick(event) {
    event.preventDefault()

    let item = event.target.parentNode
    this.options.pagination.currentPage = parseInt(item.dataset.ntPaginationIndex)

    this.draw()
  }

  /**
   * Event handler.
   * Called when user click on header to sort corresponding column
   *
   * @param {Event} event - the event
   */
  onSortingClick(event) {
    event.preventDefault()

    let item = event.target.parentNode

    if (this.options.sorting.column !== item.dataset.ntColumnName) {
      this.options.sorting.column = item.dataset.ntColumnName
      this.options.sorting.order = 'none'
    }
    this.options.sorting.order = this.options.sorting.order === 'asc' ? 'desc' : 'asc'

    this.options.reloading.sorted = true
    this.options.reloading.headers = true
    this.draw()
  }

  /**
   * Return object signature in b64
   *
   * @param {Object} obj - object to sign
   *
   * @return {string} - object signature
   */
  objectSignature(obj) {
    return btoa(JSON.stringify(obj, this.columns))
  }
}
<commit_msg>fix bug on reload for header<commit_after>export default class Nativetable {

  /**
   * Data sources
   * Getter
   *
   * @return {Object[]}
   */
  get sources() {
    return this.data.sources
  }

  /**
   * Data sources
   * Setter
   *
   * @return {Object[]}
   */
  set sources(rows) {
    this.options.reloading.filtered = true
    this.options.reloading.sorted = true
    this.options.reloading.paginated = true
    this.data.sources = rows
  }

  /**
   * Filters rules
   * Getter
   *
   * @return {Object}
   */
  get filters() {
    return this.options.filters
  }

  /**
   * Filters rules
   * Setter
   *
   * @return {Object}
   */
  set filters(filters) {
    this.options.reloading.filtered = true
    this.options.filters = filters
  }

  /**
   * Data sources filtered by filters
   * Getter
   *
   * @return {Object[]}
   */
  get filtered() {
    const filters = this.options.filters

    if ( // pagination cached
      this.data.filtered &&
      this.data.filtered.length > 0 &&
      this.options.reloading.filtered === false
    ) {
      return this.data.filtered
    }

    if (Object.keys(filters).length === 0) {
      this.data.filtered = this.sources

      this.options.reloading.filtered = false
      return this.data.filtered
    }

    /**
     * Closure
     * AND logical condition
     *
     * @param {Object[]} items - array of boolean value
     * @returns {boolean}
     */
    let $and = (items) => {
      let condition = true
      for (let item of items) {
        condition = condition && item
      }

      return condition
    }

    /**
     * Closure
     * OR logical condition
     *
     * @param {Object[]} items - array of boolean value
     * @returns {boolean}
     */
    let $or = (items) => {
      let condition = false
      for (let item of items) {
        condition = condition || item
      }

      return condition
    }

    /**
     * Closure
     * Calculate conditionnal result for one columns
     *
     * @param {Object} item - item to test
     * @param {function|any[]} - array of value to match or a closure for custom condition
     *
     * @return {boolean}
     */
    let $resultForColumn = (item, condition) => {
      if (typeof condition === 'function') { // custom condition
        let closure = condition
        let result = closure(item)
        if (typeof result === 'boolean') {
          return result
        }

        return false // bad return type of closure
      }

      let column = []
      for (let value of condition) { // normal condition
        column.push(item === value)
      }

      return $or(column)
    }

    this.data.filtered = this.sources.filter((item) => {
      let result = []
      if ('$and' in filters) {
        let keys = Object.keys(filters.$and)
        let conditions = filters['$and']
        let resultAND = []

        for (let key of keys) {
          resultAND.push($resultForColumn(item[key], conditions[key]))
        }

        result.push($and(resultAND))
      }

      if ('$or' in filters) {
        let keys = Object.keys(filters.$or)
        let conditions = filters['$or']
        let resultOR = []

        for (let key of keys) {
          resultOR.push($resultForColumn(item[key], conditions[key]))
        }

        result.push($or(resultOR))
      }

      return $and(result)
    })

    this.options.reloading.filtered = false
    return this.data.filtered
  }

  /**
   * Data sources sorted
   * Getter
   *
   * @return {Object[]}
   */
  get sorted() {
    if ( // pagination cached
      this.data.sorted &&
      this.data.sorted.length > 0 &&
      this.options.reloading.sorted === false
    ) {
      return this.data.sorted
    }

    const sources = this.filtered
    const { column, order = 'none', activated: isSortingActivated } = this.options.sorting

    if (
      sources.length <= 0 ||
      isSortingActivated === false ||
      typeof column === 'undefined'
    ) { // no sources or no sorting
      this.data.sorted = sources

      this.options.reloading.sorted = false
      return this.data.sorted
    }

    let tmpArray = sources.map((e, i) => { // create temporary array that is easier to sort
      let el = e[column]

      if (typeof e[column] === 'string') {
        el = e[column].toLowerCase()
      }

      if (typeof e[column] === 'undefined') {
        el = ''
      }

      return {
        index: i,
        value: el
      }
    })

    tmpArray.sort((a, b) => {
      if (a.value > b.value) {
        return order === 'asc' ? 1 : -1
      }

      if (a.value  { // rebuild sources in data.sorted
      return sources[e.index]
    })

    this.options.reloading.sorted = false
    return this.data.sorted
  }

  /**
   * Data sources paginated
   * Getter
   *
   * @return {Object[]}
   */
  get paginated() {
    if ( // pagination cached
      this.data.paginated &&
      this.data.paginated.length > 0 &&
      this.options.reloading.paginated === false
    ) {
      return this.data.paginated
    }

    const sources = this.sorted
    const maxLength = this.options.pagination.maxLength

    if (sources.length <= 0) { // no sources
      this.data.paginated = []

      this.options.reloading.paginated = false
      return this.data.paginated
    }

    if (maxLength === -1) { // no pagination
      this.data.paginated = [ sources ]
      return this.data.paginated
    }

    let pages = []
    let page = []

    for (let row of sources) {
      if (page.length >= maxLength) {
        pages.push(page)
        page = []
      }

      page.push(row)
    }
    pages.push(page)

    this.data.paginated = pages
    this.options.reloading.paginated = false
    return this.data.paginated
  }

  /**
   * Colum's nouns getter
   *
   * @return {Object[]} - column
   */
  get columns() {
    let headers = this.options.columns || []

    if (headers.length > 0) {
      return headers
    }

    this.filtered.forEach((el) => {
      let tmpHeaders = Object.keys(el)

      for (let col of tmpHeaders) {
        if (headers.indexOf(col) !== -1) {
          continue
        }

        headers.push(col)
      }
    })

    this.options.columns = headers.map((el) => {
      return {
        key: el,
        title: el
      }
    })
    return this.options.columns
  }

  /**
   * Colum's nouns setter
   *
   * @param {string[]|Object[]} value - column
   */
  set columns(value) {
    this.options.columns = []

    if (value == null) {
      return
    }

    for (let columns of value) {
      let { key, title } = columns

      if (typeof columns === 'string') {
        key = title = columns
      }

      if (typeof key === 'undefined' || typeof title === 'undefined') {
        continue
      }

      this.options.columns.push({
        key,
        title
      })
    }

    this.options.reloading.headers = true
  }

  /**
   * Setting pagination
   * Setter
   *
   * @param {number} maxLength - number max of elements per page
   */
  set pagination({ maxLength = -1 }) {
    this.options.reloading.paginated = true
    this.options.pagination.maxLength = maxLength
  }

  /**
   * Setting pagination
   * Getter
   *
   * @return {Object} current pagination settings
   */
  get pagination() {
    return {
      currentPage: this.options.pagination.currentPage,
      maxLength: this.options.pagination.maxLength
    }
  }

  /**
   * @param {string}    id                             - identifiant of
 tag targeted
   * @param {Object}    [options]                      - options of Nativetable
   * @param {Object[]}  [options.sources]              - array of source values
   * @param {Object}    [options.filters]              - filters to applies
   * @param {string[]}  [options.columns]              - column's nouns
   * @param {Object}    [options.pagination]           - options for pagination
   * @param {Object}    [options.pagination.maxLength] - number max of elements per page
   * @param {boolean}   [options.sorting]              - a flag that activates sorting
   *
   * @throws {TypeError} when the id parameter is invalid
   *
   * @return {Nativetable} - an instance of Nativetable
   */
  constructor(id, { sources = [], filters = {}, columns = [], pagination: { maxLength = -1 } = {}, sorting = false } = {}) {
    this.options = {}
    this.data = {}

    if (typeof id !== 'string' || document.getElementById(id) == null) {
      throw new TypeError('First parameter of Nativetable need to be a valid tag ID.')
    }

    this.options.id = id
    this.options.box = document.getElementById(id)
    this.options.reloading = {}
    this.options.filters = filters
    this.options.sorting = {
      activated: sorting
    }
    this.options.pagination = {
      currentPage: 0,
      maxLength
    }

    this.columns = columns
    this.sources = sources

    this.draw()
  }

  /**
   * Reload data before drawing table
   *
   * @param {Object[]} [rows] - objects to reload
   */
  reload(rows = []) {
    this.options.pagination.currentPage = 0
    this.sources = rows.length > 0 ? rows : this.sources

    this.options.reloading.filtered = true
    this.options.reloading.paginated = true

    this.draw()
  }

  /**
   * Draw HTML table.
   */
  draw() {
    const sources = this.paginated
    const isPaginated = sources.length !== 1

    if (sources.length <= 0) {
      return
    }

    let currentPage = this.options.pagination.currentPage
    currentPage = currentPage < sources.length ? currentPage : 0

    let tableTag, theadTag, tbodyTag, paginationTag

    if ( // Delete pagination if exist
      this.options.box.children[1] &&
      this.options.box.children[1].classList.contains('nt-pagination')
    ) {
      paginationTag = this.options.box.children[1]
      this.options.box.removeChild(paginationTag)
    }

    if (isPaginated) { // Rebuild pagination if data is paginated
      paginationTag = this.buildPagination(sources, currentPage)
      this.options.box.appendChild(paginationTag)
    }

    if ( // Doesn't recreate the full table if already exist
      this.options.box.children[0] &&
      this.options.box.children[0].classList.contains('nt-table')
    ) {
      tableTag = this.options.box.children[0]
      theadTag = tableTag.children[0]
      tbodyTag = tableTag.children[1]
      tableTag.removeChild(tbodyTag)

      if (this.options.reloading.headers) {
        tableTag.removeChild(theadTag)
        theadTag = this.buildTableHeader(this.columns)
        tableTag.appendChild(theadTag)
      }

      tbodyTag = this.buildTableBody(sources[currentPage], this.columns)
      tableTag.appendChild(tbodyTag)

      return
    }

    // Creation
    tableTag = document.createElement('table')
    theadTag = this.buildTableHeader(this.columns)
    tbodyTag = this.buildTableBody(sources[currentPage], this.columns)

    tableTag.classList.add('nt-table')

    tableTag.appendChild(theadTag)
    tableTag.appendChild(tbodyTag)

    this.options.box.insertBefore(tableTag, paginationTag)
  }

  /**
   * Builder for table header
   *
   * @param {Object[]} cols - Array of columns
   *
   * @return {HTMLElement} A thead HTML tag
   */
  buildTableHeader(cols) {
    const columns = cols

    let theadTag = document.createElement('thead')
    let trTag = document.createElement('tr')

    theadTag.classList.add('nt-head')

    for (let { key, title } of columns) {
      let tdTag = document.createElement('td')
      tdTag.textContent = title
      tdTag.dataset.ntColumnName = key

      if (this.options.sorting.activated) {
        const glyphList = {
          asc: '',
          desc: '',
          none: ''
        }
        let order = this.options.sorting.column === key ? this.options.sorting.order : 'none'
        let glyph = glyphList[order]
        let aTag = document.createElement('a')

        aTag.href = '#'
        aTag.addEventListener('click', this.onSortingClick.bind(this))
        aTag.innerHTML = `${title} ${glyph}`

        tdTag.textContent = ''
        tdTag.appendChild(aTag)
      }

      trTag.appendChild(tdTag)
    }

    theadTag.appendChild(trTag)
    return theadTag
  }

  /**
   * Builder for table body
   *
   * @param {Object[]} rows - All rows to display
   * @param {string[]} cols - Array of columns
   *
   * @return {HTMLElement} A tbody HTML tag with all rows inside
   */
  buildTableBody(rows, cols) {
    const sources = rows
    const columns = cols

    let tbodyTag = document.createElement('tbody')
    tbodyTag.classList.add('nt-body')

    for (let row of sources) {
      let trTag = document.createElement('tr')

      trTag.classList.add('nt-row')
      trTag.dataset.ntObject = this.objectSignature(row)

      for (let name of columns) {
        let tdTag = document.createElement('td')
        tdTag.textContent = typeof row[name] === 'undefined' ? '' : row[name]
        trTag.appendChild(tdTag)
      }

      tbodyTag.appendChild(trTag)
    }

    return tbodyTag
  }

  /**
   * Builder for pagination ul
   *
   * @param {array[]} pages - Paginated rows
   * @param {number} [current] - Number of current page
   *
   * @return {HTMLElement} A ul HTML tag for pagination
   */
  buildPagination(pages, current = 0) {
    const sources = pages

    let ulTag = document.createElement('ul')
    for (let index in sources) {
      let liTag = document.createElement('li')
      let aTag = document.createElement('a')

      index = +index // casting to number

      aTag.href = '#'
      aTag.textContent = index + 1
      aTag.addEventListener('click', this.onPaginationClick.bind(this))

      liTag.classList.add('nt-pagination-item')
      liTag.dataset.ntPaginationIndex = index
      liTag.appendChild(aTag)

      if (index === current) {
        liTag.classList.add('nt-pagination-item-active')
      }

      ulTag.appendChild(liTag)
    }

    ulTag.classList.add('nt-pagination')
    return ulTag
  }

  /**
   * Event handler. Called when user click on pagination links
   *
   * @param {Event} event - the event
   */
  onPaginationClick(event) {
    event.preventDefault()

    let item = event.target.parentNode
    this.options.pagination.currentPage = parseInt(item.dataset.ntPaginationIndex)

    this.draw()
  }

  /**
   * Event handler.
   * Called when user click on header to sort corresponding column
   *
   * @param {Event} event - the event
   */
  onSortingClick(event) {
    event.preventDefault()

    let item = event.target.parentNode

    if (this.options.sorting.column !== item.dataset.ntColumnName) {
      this.options.sorting.column = item.dataset.ntColumnName
      this.options.sorting.order = 'none'
    }
    this.options.sorting.order = this.options.sorting.order === 'asc' ? 'desc' : 'asc'

    this.options.reloading.sorted = true
    this.options.reloading.headers = true
    this.draw()
  }

  /**
   * Return object signature in b64
   *
   * @param {Object} obj - object to sign
   *
   * @return {string} - object signature
   */
  objectSignature(obj) {
    return btoa(JSON.stringify(obj, this.columns))
  }
}
<|endoftext|>


<commit_before>import { Base64 } from 'js-base64';
import   mozilla  from 'source-map';
import   path     from 'path';

export default class {

    constructor(stringify, root, opts) {
        this.stringify = stringify;
        this.mapOpts   = opts.map || { };
        this.root      = root;
        this.opts      = opts;
    }

    isMap() {
        if ( typeof this.opts.map !== 'undefined' ) {
            return !!this.opts.map;
        } else {
            return this.previous().length > 0;
        }
    }

    previous() {
        if ( !this.previousMaps ) {
            this.previousMaps = [];
            this.root.walk( node => {
                if ( node.source && node.source.input.map ) {
                    let map = node.source.input.map;
                    if ( this.previousMaps.indexOf(map) === -1 ) {
                        this.previousMaps.push(map);
                    }
                }
            });
        }

        return this.previousMaps;
    }

    isInline() {
        if ( typeof this.mapOpts.inline !== 'undefined' ) {
            return this.mapOpts.inline;
        }

        let annotation = this.mapOpts.annotation;
        if ( typeof annotation !== 'undefined' && annotation !== true ) {
            return false;
        }

        if ( this.previous().length ) {
            return this.previous().some( i => i.inline );
        } else {
            return true;
        }
    }

    isSourcesContent() {
        if ( typeof this.mapOpts.sourcesContent !== 'undefined' ) {
            return this.mapOpts.sourcesContent;
        }
        if ( this.previous().length ) {
            return this.previous().some( i => i.withContent() );
        } else {
            return true;
        }
    }

    clearAnnotation() {
        if ( this.mapOpts.annotation === false ) return;

        let node;
        for ( let i = this.root.nodes.length - 1; i >= 0; i-- ) {
            node = this.root.nodes[i];
            if ( node.type !== 'comment' ) continue;
            if ( node.text.indexOf('# sourceMappingURL=') === 0 ) {
                this.root.removeChild(i);
            }
        }
    }

    setSourcesContent() {
        let already = { };
        this.root.walk( node => {
            if ( node.source ) {
                let from = node.source.input.from;
                if ( from && !already[from] ) {
                    already[from] = true;
                    let relative = this.relative(from);
                    this.map.setSourceContent(relative, node.source.input.css);
                }
            }
        });
    }

    applyPrevMaps() {
        for ( let prev of this.previous() ) {
            let from = this.relative(prev.file);
            let root = prev.root || path.dirname(prev.file);
            let map;

            if ( this.mapOpts.sourcesContent === false ) {
                map = new mozilla.SourceMapConsumer(prev.text);
                if ( map.sourcesContent ) {
                    map.sourcesContent = map.sourcesContent.map( () => null );
                }
            } else {
                map = prev.consumer();
            }

            this.map.applySourceMap(map, from, this.relative(root));
        }
    }

    isAnnotation() {
        if ( this.isInline() ) {
            return true;
        } else if ( typeof this.mapOpts.annotation !== 'undefined' ) {
            return this.mapOpts.annotation;
        } else if ( this.previous().length ) {
            return this.previous().some( i => i.annotation );
        } else {
            return true;
        }
    }

    addAnnotation() {
        let content;

        if ( this.isInline() ) {
            content = 'data:application/json;base64,' +
                       Base64.encode( this.map.toString() );

        } else if ( typeof this.mapOpts.annotation === 'string' ) {
            content = this.mapOpts.annotation;

        } else {
            content = this.outputFile() + '.map';
        }

        let eol   = '\n';
        if ( this.css.indexOf('\r\n') !== -1 ) eol = '\r\n';

        this.css += eol + '/*# sourceMappingURL=' + content + ' */';
    }

    outputFile() {
        if ( this.opts.to ) {
            return this.relative(this.opts.to);
        } else if ( this.opts.from ) {
            return this.relative(this.opts.from);
        } else {
            return 'to.css';
        }
    }

    generateMap() {
        this.generateString();
        if ( this.isSourcesContent() )    this.setSourcesContent();
        if ( this.previous().length > 0 ) this.applyPrevMaps();
        if ( this.isAnnotation() )        this.addAnnotation();

        if ( this.isInline() ) {
            return [this.css];
        } else {
            return [this.css, this.map];
        }
    }

    relative(file) {
        if ( /^\w+:\/\//.test(file) ) return file;

        let from = this.opts.to ? path.dirname(this.opts.to) : '.';

        if ( typeof this.mapOpts.annotation === 'string' ) {
            from = path.dirname( path.resolve(from, this.mapOpts.annotation) );
        }

        file = path.relative(from, file);
        if ( path.sep === '\\' ) {
            return file.replace(/\\/g, '/');
        } else {
            return file;
        }
    }

    sourcePath(node) {
        if ( this.mapOpts.from ) {
            return this.mapOpts.from;
        } else {
            return this.relative(node.source.input.from);
        }
    }

    generateString() {
        this.css = '';
        this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() });

        let line   = 1;
        let column = 1;

        let lines, last;
        this.stringify(this.root, (str, node, type) => {
            this.css += str;

            if ( node && type !== 'end' ) {
                if ( node.source && node.source.start ) {
                    this.map.addMapping({
                        source:    this.sourcePath(node),
                        generated: { line, column: column - 1 },
                        original:  {
                            line:   node.source.start.line,
                            column: node.source.start.column - 1
                        }
                    });
                } else {
                    this.map.addMapping({
                        source:    '<no source>',
                        original:  { line: 1, column: 0 },
                        generated: { line, column: column - 1 }
                    });
                }
            }

            lines = str.match(/\n/g);
            if ( lines ) {
                line  += lines.length;
                last   = str.lastIndexOf('\n');
                column = str.length - last;
            } else {
                column += str.length;
            }

            if ( node && type !== 'start' ) {
                if ( node.source && node.source.end ) {
                    this.map.addMapping({
                        source:    this.sourcePath(node),
                        generated: { line, column: column - 1 },
                        original:  {
                            line:   node.source.end.line,
                            column: node.source.end.column
                        }
                    });
                } else {
                    this.map.addMapping({
                        source:    '<no source>',
                        original:  { line: 1, column: 0 },
                        generated: { line, column: column - 1 }
                    });
                }
            }
        });
    }

    generate() {
        this.clearAnnotation();

        if ( this.isMap() ) {
            return this.generateMap();
        } else {
            let result = '';
            this.stringify(this.root, i => {
                result += i;
            });
            return [result];
        }
    }

}
<commit_msg>Add class name for MapGenerator<commit_after>import { Base64 } from 'js-base64';
import   mozilla  from 'source-map';
import   path     from 'path';

export default class MapGenerator {

    constructor(stringify, root, opts) {
        this.stringify = stringify;
        this.mapOpts   = opts.map || { };
        this.root      = root;
        this.opts      = opts;
    }

    isMap() {
        if ( typeof this.opts.map !== 'undefined' ) {
            return !!this.opts.map;
        } else {
            return this.previous().length > 0;
        }
    }

    previous() {
        if ( !this.previousMaps ) {
            this.previousMaps = [];
            this.root.walk( node => {
                if ( node.source && node.source.input.map ) {
                    let map = node.source.input.map;
                    if ( this.previousMaps.indexOf(map) === -1 ) {
                        this.previousMaps.push(map);
                    }
                }
            });
        }

        return this.previousMaps;
    }

    isInline() {
        if ( typeof this.mapOpts.inline !== 'undefined' ) {
            return this.mapOpts.inline;
        }

        let annotation = this.mapOpts.annotation;
        if ( typeof annotation !== 'undefined' && annotation !== true ) {
            return false;
        }

        if ( this.previous().length ) {
            return this.previous().some( i => i.inline );
        } else {
            return true;
        }
    }

    isSourcesContent() {
        if ( typeof this.mapOpts.sourcesContent !== 'undefined' ) {
            return this.mapOpts.sourcesContent;
        }
        if ( this.previous().length ) {
            return this.previous().some( i => i.withContent() );
        } else {
            return true;
        }
    }

    clearAnnotation() {
        if ( this.mapOpts.annotation === false ) return;

        let node;
        for ( let i = this.root.nodes.length - 1; i >= 0; i-- ) {
            node = this.root.nodes[i];
            if ( node.type !== 'comment' ) continue;
            if ( node.text.indexOf('# sourceMappingURL=') === 0 ) {
                this.root.removeChild(i);
            }
        }
    }

    setSourcesContent() {
        let already = { };
        this.root.walk( node => {
            if ( node.source ) {
                let from = node.source.input.from;
                if ( from && !already[from] ) {
                    already[from] = true;
                    let relative = this.relative(from);
                    this.map.setSourceContent(relative, node.source.input.css);
                }
            }
        });
    }

    applyPrevMaps() {
        for ( let prev of this.previous() ) {
            let from = this.relative(prev.file);
            let root = prev.root || path.dirname(prev.file);
            let map;

            if ( this.mapOpts.sourcesContent === false ) {
                map = new mozilla.SourceMapConsumer(prev.text);
                if ( map.sourcesContent ) {
                    map.sourcesContent = map.sourcesContent.map( () => null );
                }
            } else {
                map = prev.consumer();
            }

            this.map.applySourceMap(map, from, this.relative(root));
        }
    }

    isAnnotation() {
        if ( this.isInline() ) {
            return true;
        } else if ( typeof this.mapOpts.annotation !== 'undefined' ) {
            return this.mapOpts.annotation;
        } else if ( this.previous().length ) {
            return this.previous().some( i => i.annotation );
        } else {
            return true;
        }
    }

    addAnnotation() {
        let content;

        if ( this.isInline() ) {
            content = 'data:application/json;base64,' +
                       Base64.encode( this.map.toString() );

        } else if ( typeof this.mapOpts.annotation === 'string' ) {
            content = this.mapOpts.annotation;

        } else {
            content = this.outputFile() + '.map';
        }

        let eol   = '\n';
        if ( this.css.indexOf('\r\n') !== -1 ) eol = '\r\n';

        this.css += eol + '/*# sourceMappingURL=' + content + ' */';
    }

    outputFile() {
        if ( this.opts.to ) {
            return this.relative(this.opts.to);
        } else if ( this.opts.from ) {
            return this.relative(this.opts.from);
        } else {
            return 'to.css';
        }
    }

    generateMap() {
        this.generateString();
        if ( this.isSourcesContent() )    this.setSourcesContent();
        if ( this.previous().length > 0 ) this.applyPrevMaps();
        if ( this.isAnnotation() )        this.addAnnotation();

        if ( this.isInline() ) {
            return [this.css];
        } else {
            return [this.css, this.map];
        }
    }

    relative(file) {
        if ( /^\w+:\/\//.test(file) ) return file;

        let from = this.opts.to ? path.dirname(this.opts.to) : '.';

        if ( typeof this.mapOpts.annotation === 'string' ) {
            from = path.dirname( path.resolve(from, this.mapOpts.annotation) );
        }

        file = path.relative(from, file);
        if ( path.sep === '\\' ) {
            return file.replace(/\\/g, '/');
        } else {
            return file;
        }
    }

    sourcePath(node) {
        if ( this.mapOpts.from ) {
            return this.mapOpts.from;
        } else {
            return this.relative(node.source.input.from);
        }
    }

    generateString() {
        this.css = '';
        this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() });

        let line   = 1;
        let column = 1;

        let lines, last;
        this.stringify(this.root, (str, node, type) => {
            this.css += str;

            if ( node && type !== 'end' ) {
                if ( node.source && node.source.start ) {
                    this.map.addMapping({
                        source:    this.sourcePath(node),
                        generated: { line, column: column - 1 },
                        original:  {
                            line:   node.source.start.line,
                            column: node.source.start.column - 1
                        }
                    });
                } else {
                    this.map.addMapping({
                        source:    '<no source>',
                        original:  { line: 1, column: 0 },
                        generated: { line, column: column - 1 }
                    });
                }
            }

            lines = str.match(/\n/g);
            if ( lines ) {
                line  += lines.length;
                last   = str.lastIndexOf('\n');
                column = str.length - last;
            } else {
                column += str.length;
            }

            if ( node && type !== 'start' ) {
                if ( node.source && node.source.end ) {
                    this.map.addMapping({
                        source:    this.sourcePath(node),
                        generated: { line, column: column - 1 },
                        original:  {
                            line:   node.source.end.line,
                            column: node.source.end.column
                        }
                    });
                } else {
                    this.map.addMapping({
                        source:    '<no source>',
                        original:  { line: 1, column: 0 },
                        generated: { line, column: column - 1 }
                    });
                }
            }
        });
    }

    generate() {
        this.clearAnnotation();

        if ( this.isMap() ) {
            return this.generateMap();
        } else {
            let result = '';
            this.stringify(this.root, i => {
                result += i;
            });
            return [result];
        }
    }

}
<|endoftext|>