<commit_before>var Node = require('roole-node');
var Transformer = require('tree-transformer');

module.exports = Compiler;

function Compiler(options) {
	if (!options) options = {};
	if (!options.indent) options.indent = '\t';
	if (!options.precision) options.precision = 3;

	this.options = options;
	this.level = 0;
}

Compiler.prototype = new Transformer();

Compiler.prototype.compile = function(node) {
	return this.visit(node);
};

Compiler.prototype.indent = function(offset) {
	if (offset === undefined) offset = 0;
	return new Array(this.level + offset + 1).join(this.options.indent);
};

Compiler.prototype.comments = function(node) {
	var comments = node.comments;
	if (!comments) return '';

	comments = comments.map(function (comment) {
		return comment.replace(/\n/g, '\n' + this.indent());
	}, this).join('\n' + this.indent());

	if (comments) return this.indent() + comments + '\n';
	return comments;
};

Compiler.prototype.visit_node = function (node) {
	return this.visit(node.children).join('');
};

Compiler.prototype.visit_stylesheet = function (stylesheet) {
	var self = this;
	var comments = this.comments(stylesheet);
	var rules = stylesheet.children.reduce(function (css, child, i) {
		var str = self.visit(child);
		if (!child.level && i) css += '\n';
		return css + str + '\n';
	}, '');

	return comments + rules;
};

Compiler.prototype.visit_ruleset = function(ruleset) {
	var level = this.level;
	this.level += ruleset.level || 0;

	var indent = this.indent();
	var comments = this.comments(ruleset);
	var selList = this.visit(ruleset.children[0]);
	var ruleList = this.visit(ruleset.children[1]);

	this.level = level;

	return comments + indent + selList + ' ' + ruleList;
};

Compiler.prototype.visit_selectorList =
Compiler.prototype.visit_mediaQueryList =
Compiler.prototype.visit_keyframeSelectorList = function(node) {
	return this.visit(node.children).join(',\n' + this.indent());
};

Compiler.prototype.visit_classSelector = function(sel) {
	return '.' + this.visit(sel.children[0]);
};

Compiler.prototype.visit_hashSelector = function(sel) {
	return '#' + this.visit(sel.children[0]);
};

Compiler.prototype.visit_attributeSelector = function(sel) {
	var attr = this.visit(sel.children).join(sel.operator);
	return '[' + attr + ']';
};

Compiler.prototype.visit_pseudoSelector = function(sel) {
	var colon = sel.doubleColon ? '::' : ':';
	var name = this.visit(sel.children[0]);
	var args = this.visit(sel.children[1]) || '';
	if (args) args = '(' + args + ')';
	return colon + name + args;
};

Compiler.prototype.visit_negationSelector = function(sel) {
	return ':not(' + this.visit(sel.children[0]) + ')';
};

Compiler.prototype.visit_universalSelector = function() {
	return '*';
};

Compiler.prototype.visit_combinator = function(comb) {
	var value = comb.children[0];
	if (value !== ' ') value = ' ' + value + ' ';
	return value;
};

Compiler.prototype.visit_ruleList = function(ruleList) {
	++this.level;
	var rules = this.visit(ruleList.children).join('\n');
	--this.level;
	if (!rules) return '{}';
	return '{\n' + rules + '\n' + this.indent() + '}';
};

Compiler.prototype.visit_property = function(prop) {
	var name = this.visit(prop.children[0]);
	var value = this.visit(prop.children[1]);
	var priority = prop.priority || '';
	if (priority) priority = ' ' + priority;
	var indent = this.indent();
	var comments = this.comments(prop);
	return comments + indent + name + ': ' +  value + priority + ';';
};

Compiler.prototype.visit_number = function(num) {
	num = +num.children[0].toFixed(this.options.precision);
	return num.toString();
};

Compiler.prototype.visit_percentage = function(percent) {
	var num = +percent.children[0].toFixed(this.options.precision);
	return num + '%';
};

Compiler.prototype.visit_dimension = function(dimen) {
	var num = +dimen.children[0].toFixed(this.options.precision);
	var unit = dimen.children[1];
	return num + unit;
};

Compiler.prototype.visit_string = function(str) {
	return str.quote + str.children[0] + str.quote;
};

Compiler.prototype.visit_color = function(color) {
	return '#' + color.children[0];
};

Compiler.prototype.visit_null = function() {
	return 'null';
};

Compiler.prototype.visit_separator = function(sep) {
	sep = sep.children[0];
	if (sep === ',') sep += ' ';
	return sep;
};

Compiler.prototype.visit_url = function(url) {
	url = this.visit(url.children[0]);
	return 'url(' + url + ')';
};

Compiler.prototype.visit_range = function(range) {
	return this.visit(Node.toListNode(range));
};

Compiler.prototype.visit_call = function(call) {
	var name = this.visit(call.children[0]);
	var args = this.visit(call.children[1]);
	return name + '(' + args + ')';
};

Compiler.prototype.visit_argumentList = function (argList) {
	return this.visit(argList.children).join(', ');
};

Compiler.prototype.visit_media = function(media) {
	var level = this.level;
	this.level += media.level || 0;

	var comments = this.comments(media);
	var indent = this.indent();
	var mqList = media.children[0];
	var length = mqList.children.length;
	mqList = this.visit(mqList);
	mqList = (length === 1 ? ' ' : '\n' + this.indent()) + mqList;
	var ruleList = this.visit(media.children[1]);

	this.level = level;

	return comments + indent + '@media' + mqList + ' ' + ruleList;
};

Compiler.prototype.visit_mediaQuery = function(mq) {
	return this.visit(mq.children).join(' and ');
};

Compiler.prototype.visit_mediaType = function(mt) {
	var modifier = mt.modifier || '';
	if (modifier) modifier += ' ';
	var name = this.visit(mt.children[0]);
	return modifier + name;
};

Compiler.prototype.visit_mediaFeature = function(mf) {
	var name = this.visit(mf.children[0]);
	var value = this.visit(mf.children[1]) || '';
	if (value) value = ': ' + value;
	return '(' + name + value + ')';
};

Compiler.prototype.visit_import = function(importNode) {
	var comments = this.comments(importNode);
	var url = this.visit(importNode.children[0]);
	var mq = this.visit(importNode.children[1]) || '';
	if (mq) mq = ' ' + mq;
	return comments + '@import ' + url + mq + ';';
};

Compiler.prototype.visit_keyframes = function(kfs) {
	var comments = this.comments(kfs);
	var prefix = kfs.prefix || '';
	if (prefix) prefix = '-' + prefix + '-';
	var name = this.visit(kfs.children[0]);
	var ruleList = this.visit(kfs.children[1]);
	return comments + '@' + prefix + 'keyframes ' + name + ' ' + ruleList;
};

Compiler.prototype.visit_keyframe = function(kf) {
	var comments = this.comments(kf);
	var indent = this.indent();
	var selList = this.visit(kf.children[0]);
	var ruleList = this.visit(kf.children[1]);
	return comments + indent + selList + ' ' + ruleList;
};

Compiler.prototype.visit_fontFace = function(ff) {
	var comments = this.comments(ff);
	var ruleList = this.visit(ff.children[0]);
	return comments + '@font-face '+ ruleList;
};

Compiler.prototype.visit_charset = function(charset) {
	var comments = this.comments(charset);
	var value = this.visit(charset.children[0]);
	return comments + '@charset ' + value + ';';
};

Compiler.prototype.visit_page = function(page) {
	var comments = this.comments(page);
	var name = this.visit(page.children[0]) || '';
	if (name) name = ' :' + name;
	var ruleList = this.visit(page.children[1]);
	return comments + '@page' + name + ' ' + ruleList;
};<commit_msg>refactor<commit_after>var Node = require('roole-node');
var Transformer = require('tree-transformer');

module.exports = Compiler;

function Compiler(options) {
	if (!options) options = {};
	if (!options.indent) options.indent = '\t';
	if (!options.precision) options.precision = 3;

	this.options = options;
	this.level = 0;
}

Compiler.prototype = new Transformer();

Compiler.prototype.compile = function(node) {
	return this.visit(node);
};

Compiler.prototype.indent = function(offset) {
	if (offset === undefined) offset = 0;
	return new Array(this.level + offset + 1).join(this.options.indent);
};

Compiler.prototype.comments = function(node) {
	var comments = node.comments;
	if (!comments) return '';

	comments = comments.map(function (comment) {
		return comment.replace(/\n/g, '\n' + this.indent());
	}, this).join('\n' + this.indent());

	if (comments) return this.indent() + comments + '\n';
	return comments;
};

Compiler.prototype.visit_node = function (node) {
	return this.visit(node.children).join('');
};

Compiler.prototype.visit_stylesheet = function (stylesheet) {
	var self = this;
	var comments = this.comments(stylesheet);
	var rules = stylesheet.children.reduce(function (css, child, i) {
		var str = self.visit(child);
		if (!child.level && i) css += '\n';
		return css + str + '\n';
	}, '');

	return comments + rules;
};

Compiler.prototype.visit_ruleset = function(ruleset) {
	var level = this.level;
	this.level += ruleset.level || 0;

	var indent = this.indent();
	var comments = this.comments(ruleset);
	var selList = this.visit(ruleset.children[0]);
	var ruleList = this.visit(ruleset.children[1]);

	this.level = level;

	return comments + indent + selList + ' ' + ruleList;
};

Compiler.prototype.visit_selectorList =
Compiler.prototype.visit_mediaQueryList =
Compiler.prototype.visit_keyframeSelectorList = function(node) {
	return this.visit(node.children).join(',\n' + this.indent());
};

Compiler.prototype.visit_classSelector = function(sel) {
	return '.' + this.visit(sel.children[0]);
};

Compiler.prototype.visit_hashSelector = function(sel) {
	return '#' + this.visit(sel.children[0]);
};

Compiler.prototype.visit_attributeSelector = function(sel) {
	var attr = this.visit(sel.children).join(sel.operator);
	return '[' + attr + ']';
};

Compiler.prototype.visit_pseudoSelector = function(sel) {
	var colon = sel.doubleColon ? '::' : ':';
	var name = this.visit(sel.children[0]);
	var args = this.visit(sel.children[1]) || '';
	if (args) args = '(' + args + ')';
	return colon + name + args;
};

Compiler.prototype.visit_negationSelector = function(sel) {
	return ':not(' + this.visit(sel.children[0]) + ')';
};

Compiler.prototype.visit_universalSelector = function() {
	return '*';
};

Compiler.prototype.visit_combinator = function(comb) {
	var val = comb.children[0];
	if (val !== ' ') val = ' ' + val + ' ';
	return val;
};

Compiler.prototype.visit_ruleList = function(ruleList) {
	++this.level;

	var rules = this.visit(ruleList.children).join('\n');

	--this.level;

	if (!rules) return '{}';
	return '{\n' + rules + '\n' + this.indent() + '}';
};

Compiler.prototype.visit_property = function(prop) {
	var name = this.visit(prop.children[0]);
	var value = this.visit(prop.children[1]);
	var priority = prop.priority;
	if (priority) priority = ' ' + priority;
	var indent = this.indent();
	var comments = this.comments(prop);
	return comments + indent + name + ': ' +  value + priority + ';';
};

Compiler.prototype.visit_number = function(num) {
	num = +num.children[0].toFixed(this.options.precision);
	return num.toString();
};

Compiler.prototype.visit_percentage = function(percent) {
	var num = +percent.children[0].toFixed(this.options.precision);
	return num + '%';
};

Compiler.prototype.visit_dimension = function(dimen) {
	var num = +dimen.children[0].toFixed(this.options.precision);
	var unit = dimen.children[1];
	return num + unit;
};

Compiler.prototype.visit_string = function(str) {
	return str.quote + str.children[0] + str.quote;
};

Compiler.prototype.visit_color = function(color) {
	return '#' + color.children[0];
};

Compiler.prototype.visit_null = function() {
	return 'null';
};

Compiler.prototype.visit_separator = function(sep) {
	sep = sep.children[0];
	if (sep === ',') sep += ' ';
	return sep;
};

Compiler.prototype.visit_url = function(url) {
	url = this.visit(url.children[0]);
	return 'url(' + url + ')';
};

Compiler.prototype.visit_range = function(range) {
	return this.visit(Node.toListNode(range));
};

Compiler.prototype.visit_call = function(call) {
	var name = this.visit(call.children[0]);
	var args = this.visit(call.children[1]);
	return name + '(' + args + ')';
};

Compiler.prototype.visit_argumentList = function (argList) {
	return this.visit(argList.children).join(', ');
};

Compiler.prototype.visit_media = function(media) {
	var level = this.level;
	this.level += media.level || 0;

	var comments = this.comments(media);
	var indent = this.indent();
	var mqList = media.children[0];
	var length = mqList.children.length;
	mqList = this.visit(mqList);
	mqList = (length === 1 ? ' ' : '\n' + indent) + mqList;
	var ruleList = this.visit(media.children[1]);

	this.level = level;

	return comments + indent + '@media' + mqList + ' ' + ruleList;
};

Compiler.prototype.visit_mediaQuery = function(mq) {
	return this.visit(mq.children).join(' and ');
};

Compiler.prototype.visit_mediaType = function(mt) {
	var modifier = mt.modifier;
	if (modifier) modifier += ' ';
	var name = this.visit(mt.children[0]);
	return modifier + name;
};

Compiler.prototype.visit_mediaFeature = function(mf) {
	var name = this.visit(mf.children[0]);
	var value = this.visit(mf.children[1]) || '';
	if (value) value = ': ' + value;
	return '(' + name + value + ')';
};

Compiler.prototype.visit_import = function(importNode) {
	var comments = this.comments(importNode);
	var url = this.visit(importNode.children[0]);
	var mq = this.visit(importNode.children[1]) || '';
	if (mq) mq = ' ' + mq;
	return comments + '@import ' + url + mq + ';';
};

Compiler.prototype.visit_keyframes = function(kfs) {
	var comments = this.comments(kfs);
	var prefix = kfs.prefix;
	if (prefix) prefix = '-' + prefix + '-';
	var name = this.visit(kfs.children[0]);
	var ruleList = this.visit(kfs.children[1]);
	return comments + '@' + prefix + 'keyframes ' + name + ' ' + ruleList;
};

Compiler.prototype.visit_keyframe = function(kf) {
	var comments = this.comments(kf);
	var indent = this.indent();
	var selList = this.visit(kf.children[0]);
	var ruleList = this.visit(kf.children[1]);
	return comments + indent + selList + ' ' + ruleList;
};

Compiler.prototype.visit_fontFace = function(ff) {
	var comments = this.comments(ff);
	var ruleList = this.visit(ff.children[0]);
	return comments + '@font-face '+ ruleList;
};

Compiler.prototype.visit_charset = function(charset) {
	var comments = this.comments(charset);
	var value = this.visit(charset.children[0]);
	return comments + '@charset ' + value + ';';
};

Compiler.prototype.visit_page = function(page) {
	var comments = this.comments(page);
	var name = this.visit(page.children[0]) || '';
	if (name) name = ' :' + name;
	var ruleList = this.visit(page.children[1]);
	return comments + '@page' + name + ' ' + ruleList;
};<|endoftext|>


<commit_before>/* eslint-disable react/forbid-prop-types */
/* eslint-disable import/prefer-default-export */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import { MODAL_KEYS } from '../utilities';
import { usePageReducer } from '../hooks/usePageReducer';
import { recordKeyExtractor, getItemLayout } from './dataTableUtilities';

import { DataTablePageModal } from '../widgets/modals';
import { PageButton, PageInfo, DataTablePageView, SearchBar } from '../widgets';
import { DataTable, DataTableHeaderRow, DataTableRow } from '../widgets/DataTable';

import { gotoStocktakeManagePage } from '../navigation/actions';

import { buttonStrings } from '../localization';
import { newPageStyles } from '../globalStyles';
import { useRecordListener, useNavigationFocus } from '../hooks/index';

const stateInitialiser = pageObject => ({
  pageObject,
  backingData: pageObject.items,
  data: pageObject.items.sorted('item.name').slice(),
  keyExtractor: recordKeyExtractor,
  dataState: new Map(),
  searchTerm: '',
  filterDataKeys: ['item.name'],
  sortBy: 'itemName',
  isAscending: true,
  modalKey: '',
  modalValue: null,
});

/**
 * Renders a mSupply page with a stocktake loaded for editing
 *
 * State:
 * Uses a reducer to manage state with `backingData` being a realm results
 * of items to display. `data` is a plain JS array of realm objects. data is
 * hydrated from the `backingData` for displaying in the interface.
 * i.e: When filtering, data is populated from filtered items of `backingData`.
 *
 * dataState is a simple map of objects corresponding to a row being displayed,
 * holding the state of a given row. Each object has the shape :
 * { isSelected, isFocused, isDisabled },
 *
 * @prop {Object} stocktake The realm transaction object for this invoice.
 * @prop {Func}   runWithLoadingIndicator Callback for displaying a fullscreen spinner.
 * @prop {String} routeName The current route name for the top of the navigation stack.
 * @prop {Func}   dispatch  Redux store dispatch function.
 * @prop {Object} navigation App-wide stack navigator reference
 *
 */
export const StocktakeEditPage = ({
  runWithLoadingIndicator,
  stocktake,
  routeName,
  dispatch: reduxDispatch,
  navigation,
}) => {
  const [state, dispatch, instantDebouncedDispatch] = usePageReducer(
    routeName,
    {},
    stateInitialiser,
    stocktake
  );

  const {
    pageObject,
    data,
    dataState,
    searchTerm,
    sortBy,
    isAscending,
    modalKey,
    modalValue,
    keyExtractor,
    PageActions,
    columns,
    getPageInfoColumns,
  } = state;

  const { isFinalised, comment, program } = pageObject;

  // Listen to the stocktake become the top of the stack or being finalised,
  // as these events are side-effects. Refreshing makes the state consistent again.
  const refreshCallback = () => dispatch(PageActions.refreshData());
  useRecordListener(refreshCallback, pageObject, 'Stocktake');
  useNavigationFocus(refreshCallback, navigation);

  // If the Stocktake is outdated, force a reset of the stocktake on mount.
  useEffect(() => {
    if (stocktake.isOutdated) dispatch(PageActions.openModal(MODAL_KEYS.STOCKTAKE_OUTDATED_ITEM));
  }, []);

  const onFilterData = value => dispatch(PageActions.filterData(value));
  const onEditBatch = rowKey => PageActions.openModal(MODAL_KEYS.EDIT_STOCKTAKE_BATCH, rowKey);
  const onEditReason = rowKey => PageActions.openModal(MODAL_KEYS.STOCKTAKE_REASON, rowKey);
  const onEditComment = value => dispatch(PageActions.editComment(value, 'Stocktake'));
  const onCloseModal = () => dispatch(PageActions.closeModal());
  const onApplyReason = ({ item }) => dispatch(PageActions.applyReason(item));
  const onConfirmBatchEdit = () => dispatch(PageActions.closeAndRefresh());

  const onResetStocktake = () =>
    runWithLoadingIndicator(() => dispatch(PageActions.resetStocktake()));
  const onManageStocktake = () =>
    reduxDispatch(gotoStocktakeManagePage({ stocktake, stocktakeName: stocktake.name }));

  const renderPageInfo = useCallback(
    () => (
      <PageInfo
        columns={getPageInfoColumns(pageObject, dispatch, PageActions)}
        isEditingDisabled={isFinalised}
      />
    ),
    [comment, isFinalised]
  );

  const getAction = (colKey, propName) => {
    switch (colKey) {
      case 'countedTotalQuantity':
        return PageActions.editCountedQuantity;
      case 'batch':
        return onEditBatch;
      case 'remove':
        if (propName === 'onCheckAction') return PageActions.selectRow;
        return PageActions.deselectRow;
      case 'reasonTitle':
        return onEditReason;
      default:
        return null;
    }
  };

  const getModalOnSelect = () => {
    switch (modalKey) {
      case MODAL_KEYS.STOCKTAKE_COMMENT_EDIT:
        return onEditComment;
      case MODAL_KEYS.EDIT_STOCKTAKE_BATCH:
        return onConfirmBatchEdit;
      case MODAL_KEYS.STOCKTAKE_OUTDATED_ITEM:
        return onResetStocktake;
      case MODAL_KEYS.ENFORCE_STOCKTAKE_REASON:
      case MODAL_KEYS.STOCKTAKE_REASON:
        return onApplyReason;
      default:
        return null;
    }
  };

  const renderRow = useCallback(
    listItem => {
      const { item, index } = listItem;
      const rowKey = keyExtractor(item);
      return (
        <DataTableRow
          rowData={data[index]}
          rowState={dataState.get(rowKey)}
          rowKey={rowKey}
          columns={columns}
          isFinalised={isFinalised}
          dispatch={dispatch}
          getAction={getAction}
          rowIndex={index}
        />
      );
    },
    [data, dataState]
  );

  const renderHeader = useCallback(
    () => (
      <DataTableHeaderRow
        columns={columns}
        dispatch={instantDebouncedDispatch}
        sortAction={PageActions.sortData}
        isAscending={isAscending}
        sortBy={sortBy}
      />
    ),
    [sortBy, isAscending]
  );

  const PageButtons = useCallback(() => {
    const ManageStocktake = () => (
      <PageButton
        text={buttonStrings.manage_stocktake}
        onPress={onManageStocktake}
        isDisabled={isFinalised}
      />
    );

    return <View style={newPageTopRightSectionContainer}>{program ? null : ManageStocktake}</View>;
  }, [program]);

  const {
    newPageTopSectionContainer,
    newPageTopLeftSectionContainer,
    newPageTopRightSectionContainer,
  } = newPageStyles;
  return (
    <DataTablePageView>
      <View style={newPageTopSectionContainer}>
        <View style={newPageTopLeftSectionContainer}>
          {renderPageInfo()}
          <SearchBar onChangeText={onFilterData} value={searchTerm} />
        </View>
        <PageButtons />
      </View>
      <DataTable
        data={data}
        extraData={dataState}
        renderRow={renderRow}
        renderHeader={renderHeader}
        keyExtractor={keyExtractor}
        getItemLayout={getItemLayout}
        columns={columns}
      />
      <DataTablePageModal
        fullScreen={false}
        isOpen={!!modalKey}
        modalKey={modalKey}
        onClose={modalKey === MODAL_KEYS.EDIT_STOCKTAKE_BATCH ? onConfirmBatchEdit : onCloseModal}
        onSelect={getModalOnSelect()}
        dispatch={dispatch}
        currentValue={modalValue}
      />
    </DataTablePageView>
  );
};

StocktakeEditPage.propTypes = {
  runWithLoadingIndicator: PropTypes.func.isRequired,
  stocktake: PropTypes.object.isRequired,
  routeName: PropTypes.string.isRequired,
  dispatch: PropTypes.func.isRequired,
  navigation: PropTypes.object.isRequired,
};
<commit_msg>Add fix for StocktakeManageButton<commit_after>/* eslint-disable react/forbid-prop-types */
/* eslint-disable import/prefer-default-export */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import { MODAL_KEYS } from '../utilities';
import { usePageReducer } from '../hooks/usePageReducer';
import { recordKeyExtractor, getItemLayout } from './dataTableUtilities';

import { DataTablePageModal } from '../widgets/modals';
import { PageButton, PageInfo, DataTablePageView, SearchBar } from '../widgets';
import { DataTable, DataTableHeaderRow, DataTableRow } from '../widgets/DataTable';

import { gotoStocktakeManagePage } from '../navigation/actions';

import { buttonStrings } from '../localization';
import { newPageStyles } from '../globalStyles';
import { useRecordListener, useNavigationFocus } from '../hooks/index';

const stateInitialiser = pageObject => ({
  pageObject,
  backingData: pageObject.items,
  data: pageObject.items.sorted('item.name').slice(),
  keyExtractor: recordKeyExtractor,
  dataState: new Map(),
  searchTerm: '',
  filterDataKeys: ['item.name'],
  sortBy: 'itemName',
  isAscending: true,
  modalKey: '',
  modalValue: null,
});

/**
 * Renders a mSupply page with a stocktake loaded for editing
 *
 * State:
 * Uses a reducer to manage state with `backingData` being a realm results
 * of items to display. `data` is a plain JS array of realm objects. data is
 * hydrated from the `backingData` for displaying in the interface.
 * i.e: When filtering, data is populated from filtered items of `backingData`.
 *
 * dataState is a simple map of objects corresponding to a row being displayed,
 * holding the state of a given row. Each object has the shape :
 * { isSelected, isFocused, isDisabled },
 *
 * @prop {Object} stocktake The realm transaction object for this invoice.
 * @prop {Func}   runWithLoadingIndicator Callback for displaying a fullscreen spinner.
 * @prop {String} routeName The current route name for the top of the navigation stack.
 * @prop {Func}   dispatch  Redux store dispatch function.
 * @prop {Object} navigation App-wide stack navigator reference
 *
 */
export const StocktakeEditPage = ({
  runWithLoadingIndicator,
  stocktake,
  routeName,
  dispatch: reduxDispatch,
  navigation,
}) => {
  const [state, dispatch, instantDebouncedDispatch] = usePageReducer(
    routeName,
    {},
    stateInitialiser,
    stocktake
  );

  const {
    pageObject,
    data,
    dataState,
    searchTerm,
    sortBy,
    isAscending,
    modalKey,
    modalValue,
    keyExtractor,
    PageActions,
    columns,
    getPageInfoColumns,
  } = state;

  const { isFinalised, comment, program } = pageObject;

  // Listen to the stocktake become the top of the stack or being finalised,
  // as these events are side-effects. Refreshing makes the state consistent again.
  const refreshCallback = () => dispatch(PageActions.refreshData());
  useRecordListener(refreshCallback, pageObject, 'Stocktake');
  useNavigationFocus(refreshCallback, navigation);

  // If the Stocktake is outdated, force a reset of the stocktake on mount.
  useEffect(() => {
    if (stocktake.isOutdated) dispatch(PageActions.openModal(MODAL_KEYS.STOCKTAKE_OUTDATED_ITEM));
  }, []);

  const onFilterData = value => dispatch(PageActions.filterData(value));
  const onEditBatch = rowKey => PageActions.openModal(MODAL_KEYS.EDIT_STOCKTAKE_BATCH, rowKey);
  const onEditReason = rowKey => PageActions.openModal(MODAL_KEYS.STOCKTAKE_REASON, rowKey);
  const onEditComment = value => dispatch(PageActions.editComment(value, 'Stocktake'));
  const onCloseModal = () => dispatch(PageActions.closeModal());
  const onApplyReason = ({ item }) => dispatch(PageActions.applyReason(item));
  const onConfirmBatchEdit = () => dispatch(PageActions.closeAndRefresh());

  const onResetStocktake = () =>
    runWithLoadingIndicator(() => dispatch(PageActions.resetStocktake()));
  const onManageStocktake = () =>
    reduxDispatch(gotoStocktakeManagePage({ stocktake, stocktakeName: stocktake.name }));

  const renderPageInfo = useCallback(
    () => (
      <PageInfo
        columns={getPageInfoColumns(pageObject, dispatch, PageActions)}
        isEditingDisabled={isFinalised}
      />
    ),
    [comment, isFinalised]
  );

  const getAction = (colKey, propName) => {
    switch (colKey) {
      case 'countedTotalQuantity':
        return PageActions.editCountedQuantity;
      case 'batch':
        return onEditBatch;
      case 'remove':
        if (propName === 'onCheckAction') return PageActions.selectRow;
        return PageActions.deselectRow;
      case 'reasonTitle':
        return onEditReason;
      default:
        return null;
    }
  };

  const getModalOnSelect = () => {
    switch (modalKey) {
      case MODAL_KEYS.STOCKTAKE_COMMENT_EDIT:
        return onEditComment;
      case MODAL_KEYS.EDIT_STOCKTAKE_BATCH:
        return onConfirmBatchEdit;
      case MODAL_KEYS.STOCKTAKE_OUTDATED_ITEM:
        return onResetStocktake;
      case MODAL_KEYS.ENFORCE_STOCKTAKE_REASON:
      case MODAL_KEYS.STOCKTAKE_REASON:
        return onApplyReason;
      default:
        return null;
    }
  };

  const renderRow = useCallback(
    listItem => {
      const { item, index } = listItem;
      const rowKey = keyExtractor(item);
      return (
        <DataTableRow
          rowData={data[index]}
          rowState={dataState.get(rowKey)}
          rowKey={rowKey}
          columns={columns}
          isFinalised={isFinalised}
          dispatch={dispatch}
          getAction={getAction}
          rowIndex={index}
        />
      );
    },
    [data, dataState]
  );

  const renderHeader = useCallback(
    () => (
      <DataTableHeaderRow
        columns={columns}
        dispatch={instantDebouncedDispatch}
        sortAction={PageActions.sortData}
        isAscending={isAscending}
        sortBy={sortBy}
      />
    ),
    [sortBy, isAscending]
  );

  const PageButtons = useCallback(() => {
    const ManageStocktake = (
      <PageButton
        text={buttonStrings.manage_stocktake}
        onPress={onManageStocktake}
        isDisabled={isFinalised}
      />
    );

    return <View style={newPageTopRightSectionContainer}>{program ? null : ManageStocktake}</View>;
  }, [program]);

  const {
    newPageTopSectionContainer,
    newPageTopLeftSectionContainer,
    newPageTopRightSectionContainer,
  } = newPageStyles;
  return (
    <DataTablePageView>
      <View style={newPageTopSectionContainer}>
        <View style={newPageTopLeftSectionContainer}>
          {renderPageInfo()}
          <SearchBar onChangeText={onFilterData} value={searchTerm} />
        </View>
        <PageButtons />
      </View>
      <DataTable
        data={data}
        extraData={dataState}
        renderRow={renderRow}
        renderHeader={renderHeader}
        keyExtractor={keyExtractor}
        getItemLayout={getItemLayout}
        columns={columns}
      />
      <DataTablePageModal
        fullScreen={false}
        isOpen={!!modalKey}
        modalKey={modalKey}
        onClose={modalKey === MODAL_KEYS.EDIT_STOCKTAKE_BATCH ? onConfirmBatchEdit : onCloseModal}
        onSelect={getModalOnSelect()}
        dispatch={dispatch}
        currentValue={modalValue}
      />
    </DataTablePageView>
  );
};

StocktakeEditPage.propTypes = {
  runWithLoadingIndicator: PropTypes.func.isRequired,
  stocktake: PropTypes.object.isRequired,
  routeName: PropTypes.string.isRequired,
  dispatch: PropTypes.func.isRequired,
  navigation: PropTypes.object.isRequired,
};
<|endoftext|>


<commit_before>"use strict";

var m      = require("mithril"),
    moment = require("moment"),
    set    = require("lodash.set"),
    upper  = require("lodash.capitalize"),

    db = require("../../lib/firebase"),

    css = require("./head.css"),

    icons = global.crucible.icons;

module.exports = {
    controller : function(options) {
        var ctrl = this,
            ref  = options.ref,
            user = db.getAuth().uid,
            // TODO: Remove `published` field, it's deprecated
            published = options.data.published_at || options.data.published,

            publish   = published ? moment(published) : null,
            unpublish = options.data.unpublished_at ? moment(options.data.unpublished_at) : null;

        ctrl.schedule   = false;
        ctrl.unschedule = false;

        if(publish) {
            ctrl.start = {
                date   : publish.format("YYYY-MM-DD"),
                time   : publish.format("HH:mm"),
                moment : publish
            };
        } else {
            ctrl.start = {
                date : "",
                time : ""
            };
        }

        if(unpublish) {
            ctrl.end = {
                date   : unpublish.format("YYYY-MM-DD"),
                time   : unpublish.format("HH:mm"),
                moment : unpublish
            };
        } else {
            ctrl.end = {
                date : "",
                time : ""
            };
        }

        // Event handlers
        ctrl.update = function(section, field, value) {
            ctrl[section][field] = value;

            if(ctrl[section].date) {
                ctrl[section].moment = moment(
                    ctrl[section].date + " " + (ctrl[section].time || "00:00"),
                    "YYYY-MM-DD HH:mm"
                );
            } else {
                ctrl[section].moment = false;
            }
        };

        ctrl.toggle = function(force) {
            ctrl.schedule = typeof force !== "undefined" ? Boolean(force) : !ctrl.schedule;
        };

        ctrl.publish = function() {
            var start, end;

            if(ctrl.schedule && ctrl.start.date) {
                start = moment(
                    ctrl.start.date + " " + (ctrl.start.time || "00:00"),
                    "YYYY-MM-DD HH:mm"
                );
            } else {
                start = moment().subtract(10, "seconds");
            }

            if(ctrl.schedule && ctrl.end.date) {
                end = moment(
                    ctrl.end.date + " " + (ctrl.end.time || "00:00"),
                    "YYYY-MM-DD HH:mm"
                );

                if(end.isSameOrBefore(start)) {
                    // TODO: handle
                    return console.error("Invalid end date");
                }
            }

            return ref.update({
                // TODO: Remove `published` field, it's deprecated
                published      : start.valueOf(),
                published_at   : start.valueOf(),
                published_by   : user,
                unpublished_at : end ? end.valueOf() : null,
                unpublished_by : end ? user : null
            });
        };

        ctrl.unpublish = function() {
            ref.update({
                // TODO: Remove `published` field, it's deprecated
                published    : null,
                published_at : null,
                published_by : null,

                unpublished_at : db.TIMESTAMP,
                unpublished_by : user
            });

            // TODO: THIS IS SUPER GROSS
            ctrl.start = {
                date : "",
                time : ""
            };

            ctrl.end = {
                date : "",
                time : ""
            };
        };

        ctrl.save = function() {
            ref.child("fields").set(options.data.fields);
            ref.child("name").set(options.data.name);
        };
    },

    view : function(ctrl, options) {
        var now     = Date.now(),
            status  = "draft",
            publish = options.data.published_at || options.data.published,
            future  = ctrl.start.moment && ctrl.start.moment.valueOf() > now,
            locked  = global.crucible.locked;

        if(publish > now) {
            status = "scheduled";
        }

        if(publish < now) {
            status = "published";
        }

        return m("div", { class : css.head },
            m("div", { class : css.main },
                m("p", { class : css[status] },
                    upper(status)
                ),
                m("div", { class : css.actions },
                    m("button", {
                            // Attrs
                            class    : css.save,
                            title    : "Save your changes",
                            disabled : locked || null,

                            // Events
                            onclick : ctrl.save
                        },
                        m("svg", { class : css.icon },
                            m("use", { href : icons + "#save" })
                        ),
                        "Save"
                    )
                ),
                m("div", { class : css.publishing },
                    m("button", {
                            // Attrs
                            class : css.schedule,
                            title : "Schedule a publish",

                            // Events
                            onclick : ctrl.toggle.bind(null, undefined)
                        },
                        m("svg", { class : css.onlyIcon },
                            m("use", { href : icons + "#schedule" })
                        )
                    ),
                    m("button", {
                            // Attrs
                            class    : css.publish,
                            title    : future ? "Schedule publish" : "Publish now",
                            disabled : locked || null,

                            // Events
                            onclick : ctrl.publish
                        },
                        m("svg", { class : css.icon },
                            m("use", { href : icons + (future ? "#schedule" : "#publish") })
                        ),
                        future ? "Schedule" : "Publish"
                    ),
                    status === "draft" ?
                        null :
                        m("button", {
                                // Attrs
                                class    : css.unpublish,
                                title    : "Unpublish immediately",
                                disabled : locked || null,

                                // Events
                                onclick : ctrl.unpublish
                            },
                            m("svg", { class : css.icon },
                                m("use", { href : icons + "#remove" })
                            ),
                            "Unpublish"
                        )
                )
            ),
            ctrl.schedule ? m("div", { class : css.details },
                m("div", { class : css.start },
                    m("p",
                        m("label", { for : "published_at_date" }, "Publish at")
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "date",
                            id    : "published_at_date",
                            value : ctrl.start.date,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "start", "date"))
                        })
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "time",
                            id    : "published_at_time",
                            value : ctrl.start.time,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "start", "time"))
                        })
                    )
                ),
                m("div", { class : css.end },
                    m("p",
                        m("label", { for : "unpublished_at_date" }, "Until (optional)")
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "date",
                            id    : "unpublished_at_date",
                            value : ctrl.end.date,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "end", "date"))
                        })
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "time",
                            id    : "unpublished_at_time",
                            value : ctrl.end.time,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "end", "time"))
                        })
                    )
                )
            ) : null
        );
    }
};
<commit_msg>Default publish time option<commit_after>"use strict";

var m      = require("mithril"),
    moment = require("moment"),
    get    = require("lodash.get"),
    upper  = require("lodash.capitalize"),

    db = require("../../lib/firebase"),

    css = require("./head.css"),

    icons = global.crucible.icons;

module.exports = {
    controller : function(options) {
        var ctrl = this,
            ref  = options.ref,
            user = db.getAuth().uid,
            // TODO: Remove `published` field, it's deprecated
            published = options.data.published_at || options.data.published,

            publish   = published ? moment(published) : null,
            unpublish = options.data.unpublished_at ? moment(options.data.unpublished_at) : null;

        ctrl.schedule   = false;
        ctrl.unschedule = false;

        if(publish) {
            ctrl.start = {
                date   : publish.format("YYYY-MM-DD"),
                time   : publish.format("HH:mm"),
                moment : publish
            };
        } else {
            ctrl.start = {
                date : "",
                time : get(global.crucible, "defaults.publish_start_time") || ""
            };
        }

        if(unpublish) {
            ctrl.end = {
                date   : unpublish.format("YYYY-MM-DD"),
                time   : unpublish.format("HH:mm"),
                moment : unpublish
            };
        } else {
            ctrl.end = {
                date : "",
                time : ""
            };
        }

        // Event handlers
        ctrl.update = function(section, field, value) {
            ctrl[section][field] = value;

            if(ctrl[section].date) {
                ctrl[section].moment = moment(
                    ctrl[section].date + " " + (ctrl[section].time || "00:00"),
                    "YYYY-MM-DD HH:mm"
                );
            } else {
                ctrl[section].moment = false;
            }
        };

        ctrl.toggle = function(force) {
            ctrl.schedule = typeof force !== "undefined" ? Boolean(force) : !ctrl.schedule;
        };

        ctrl.publish = function() {
            var start, end;

            if(ctrl.schedule && ctrl.start.date) {
                start = moment(
                    ctrl.start.date + " " + (ctrl.start.time || "00:00"),
                    "YYYY-MM-DD HH:mm"
                );
            } else {
                start = moment().subtract(10, "seconds");
            }

            if(ctrl.schedule && ctrl.end.date) {
                end = moment(
                    ctrl.end.date + " " + (ctrl.end.time || "00:00"),
                    "YYYY-MM-DD HH:mm"
                );

                if(end.isSameOrBefore(start)) {
                    // TODO: handle
                    return console.error("Invalid end date");
                }
            }

            return ref.update({
                // TODO: Remove `published` field, it's deprecated
                published      : start.valueOf(),
                published_at   : start.valueOf(),
                published_by   : user,
                unpublished_at : end ? end.valueOf() : null,
                unpublished_by : end ? user : null
            });
        };

        ctrl.unpublish = function() {
            ref.update({
                // TODO: Remove `published` field, it's deprecated
                published    : null,
                published_at : null,
                published_by : null,

                unpublished_at : db.TIMESTAMP,
                unpublished_by : user
            });

            // TODO: THIS IS SUPER GROSS
            ctrl.start = {
                date : "",
                time : ""
            };

            ctrl.end = {
                date : "",
                time : ""
            };
        };

        ctrl.save = function() {
            ref.child("fields").set(options.data.fields);
            ref.child("name").set(options.data.name);
        };
    },

    view : function(ctrl, options) {
        var now     = Date.now(),
            status  = "draft",
            publish = options.data.published_at || options.data.published,
            future  = ctrl.start.moment && ctrl.start.moment.valueOf() > now,
            locked  = global.crucible.locked;

        if(publish > now) {
            status = "scheduled";
        }

        if(publish < now) {
            status = "published";
        }

        return m("div", { class : css.head },
            m("div", { class : css.main },
                m("p", { class : css[status] },
                    upper(status)
                ),
                m("div", { class : css.actions },
                    m("button", {
                            // Attrs
                            class    : css.save,
                            title    : "Save your changes",
                            disabled : locked || null,

                            // Events
                            onclick : ctrl.save
                        },
                        m("svg", { class : css.icon },
                            m("use", { href : icons + "#save" })
                        ),
                        "Save"
                    )
                ),
                m("div", { class : css.publishing },
                    m("button", {
                            // Attrs
                            class : css.schedule,
                            title : "Schedule a publish",

                            // Events
                            onclick : ctrl.toggle.bind(null, undefined)
                        },
                        m("svg", { class : css.onlyIcon },
                            m("use", { href : icons + "#schedule" })
                        )
                    ),
                    m("button", {
                            // Attrs
                            class    : css.publish,
                            title    : future ? "Schedule publish" : "Publish now",
                            disabled : locked || null,

                            // Events
                            onclick : ctrl.publish
                        },
                        m("svg", { class : css.icon },
                            m("use", { href : icons + (future ? "#schedule" : "#publish") })
                        ),
                        future ? "Schedule" : "Publish"
                    ),
                    status === "draft" ?
                        null :
                        m("button", {
                                // Attrs
                                class    : css.unpublish,
                                title    : "Unpublish immediately",
                                disabled : locked || null,

                                // Events
                                onclick : ctrl.unpublish
                            },
                            m("svg", { class : css.icon },
                                m("use", { href : icons + "#remove" })
                            ),
                            "Unpublish"
                        )
                )
            ),
            ctrl.schedule ? m("div", { class : css.details },
                m("div", { class : css.start },
                    m("p",
                        m("label", { for : "published_at_date" }, "Publish at")
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "date",
                            id    : "published_at_date",
                            value : ctrl.start.date,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "start", "date"))
                        })
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "time",
                            id    : "published_at_time",
                            value : ctrl.start.time,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "start", "time"))
                        })
                    )
                ),
                m("div", { class : css.end },
                    m("p",
                        m("label", { for : "unpublished_at_date" }, "Until (optional)")
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "date",
                            id    : "unpublished_at_date",
                            value : ctrl.end.date,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "end", "date"))
                        })
                    ),
                    m("p",
                        m("input", {
                            class : css.date,
                            type  : "time",
                            id    : "unpublished_at_time",
                            value : ctrl.end.time,

                            // Events
                            oninput : m.withAttr("value", ctrl.update.bind(ctrl, "end", "time"))
                        })
                    )
                )
            ) : null
        );
    }
};
<|endoftext|>


<commit_before>/* jslint node: true */
'use strict';

const stringFormat      = require('./string_format.js');
const { Errors }        = require('./enig_error.js');

//  deps
const pty               = require('node-pty');
const decode            = require('iconv-lite').decode;
const createServer      = require('net').createServer;
const paths             = require('path');

module.exports = class Door {
    constructor(client) {
        this.client     = client;
        this.restored   = false;
    }

    prepare(ioType, cb) {
        this.io = ioType;

        //  we currently only have to do any real setup for 'socket'
        if('socket' !== ioType) {
            return cb(null);
        }

        this.sockServer = createServer(conn => {
            conn.once('end', () => {
                return this.restoreIo(conn);
            });

            conn.once('error', err => {
                this.client.log.info( { error : err.message }, 'Door socket server connection');
                return this.restoreIo(conn);
            });

            this.sockServer.getConnections( (err, count) => {
                //  We expect only one connection from our DOOR/emulator/etc.
                if(!err && count <= 1) {
                    this.client.term.output.pipe(conn);
                    conn.on('data', this.doorDataHandler.bind(this));
                }
            });
        });

        this.sockServer.listen(0, () => {
            return cb(null);
        });
    }

    run(exeInfo, cb) {
        this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();

        if('socket' === this.io && !this.sockServer) {
            return cb(Errors.UnexpectedState('Socket server is not running'));
        }

        const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);

        const formatObj = {
            dropFile        : exeInfo.dropFile,
            dropFilePath    : exeInfo.dropFilePath,
            node            : exeInfo.node.toString(),
            srvPort         : this.sockServer ? this.sockServer.address().port.toString() : '-1',
            userId          : this.client.user.userId.toString(),
            userName        : this.client.user.getSanitizedName(),
            userNameRaw     : this.client.user.username,
            cwd             : cwd,
        };

        const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) );

        this.client.log.debug(
            { cmd : exeInfo.cmd, args, io : this.io },
            'Executing door'
        );

        let door;
        try {
            door = pty.spawn(exeInfo.cmd, args, {
                cols        : this.client.term.termWidth,
                rows        : this.client.term.termHeight,
                cwd         : cwd,
                env         : exeInfo.env,
                encoding    : null, //  we want to handle all encoding ourself
            });
        } catch(e) {
            return cb(e);
        }

        if('stdio' === this.io) {
            this.client.log.debug('Using stdio for door I/O');

            this.client.term.output.pipe(door);

            door.on('data', this.doorDataHandler.bind(this));

            door.once('close', () => {
                return this.restoreIo(door);
            });
        } else if('socket' === this.io) {
            this.client.log.debug(
                { srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket },
                'Using temporary socket server for door I/O'
            );
        }

        door.once('exit', exitCode => {
            this.client.log.info( { exitCode : exitCode }, 'Door exited');

            if(this.sockServer) {
                this.sockServer.close();
            }

            //  we may not get a close
            if('stdio' === this.io) {
                this.restoreIo(door);
            }

            door.removeAllListeners();

            return cb(null);
        });
    }

    doorDataHandler(data) {
        this.client.term.write(decode(data, this.encoding));
    }

    restoreIo(piped) {
        if(!this.restored && this.client.term.output) {
            this.client.term.output.unpipe(piped);
            this.client.term.output.resume();
            this.restored = true;
        }
    }
};
<commit_msg>Hopefully a little better handling of external doors/processes<commit_after>/* jslint node: true */
'use strict';

const stringFormat      = require('./string_format.js');
const { Errors }        = require('./enig_error.js');

//  deps
const pty               = require('node-pty');
const decode            = require('iconv-lite').decode;
const createServer      = require('net').createServer;
const paths             = require('path');

module.exports = class Door {
    constructor(client) {
        this.client     = client;
        this.restored   = false;
    }

    prepare(ioType, cb) {
        this.io = ioType;

        //  we currently only have to do any real setup for 'socket'
        if('socket' !== ioType) {
            return cb(null);
        }

        this.sockServer = createServer(conn => {
            conn.once('end', () => {
                return this.restoreIo(conn);
            });

            conn.once('error', err => {
                this.client.log.info( { error : err.message }, 'Door socket server connection');
                return this.restoreIo(conn);
            });

            this.sockServer.getConnections( (err, count) => {
                //  We expect only one connection from our DOOR/emulator/etc.
                if(!err && count <= 1) {
                    this.client.term.output.pipe(conn);
                    conn.on('data', this.doorDataHandler.bind(this));
                }
            });
        });

        this.sockServer.listen(0, () => {
            return cb(null);
        });
    }

    run(exeInfo, cb) {
        this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();

        if('socket' === this.io && !this.sockServer) {
            return cb(Errors.UnexpectedState('Socket server is not running'));
        }

        const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);

        const formatObj = {
            dropFile        : exeInfo.dropFile,
            dropFilePath    : exeInfo.dropFilePath,
            node            : exeInfo.node.toString(),
            srvPort         : this.sockServer ? this.sockServer.address().port.toString() : '-1',
            userId          : this.client.user.userId.toString(),
            userName        : this.client.user.getSanitizedName(),
            userNameRaw     : this.client.user.username,
            cwd             : cwd,
        };

        const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) );

        this.client.log.info(
            { cmd : exeInfo.cmd, args, io : this.io },
            'Executing external door process'
        );

        try {
            this.doorPty = pty.spawn(exeInfo.cmd, args, {
                cols        : this.client.term.termWidth,
                rows        : this.client.term.termHeight,
                cwd         : cwd,
                env         : exeInfo.env,
                encoding    : null, //  we want to handle all encoding ourself
            });
        } catch(e) {
            return cb(e);
        }

        this.client.log.debug(
            { processId : this.doorPty.pid }, 'External door process spawned'
        );

        if('stdio' === this.io) {
            this.client.log.debug('Using stdio for door I/O');

            this.client.term.output.pipe(this.doorPty);

            this.doorPty.on('data', this.doorDataHandler.bind(this));

            this.doorPty.once('close', () => {
                return this.restoreIo(this.doorPty);
            });
        } else if('socket' === this.io) {
            this.client.log.debug(
                { srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket },
                'Using temporary socket server for door I/O'
            );
        }

        this.doorPty.once('exit', exitCode => {
            this.client.log.info( { exitCode : exitCode }, 'Door exited');

            if(this.sockServer) {
                this.sockServer.close();
            }

            //  we may not get a close
            if('stdio' === this.io) {
                this.restoreIo(this.doorPty);
            }

            this.doorPty.removeAllListeners();
            delete this.doorPty;

            return cb(null);
        });
    }

    doorDataHandler(data) {
        this.client.term.write(decode(data, this.encoding));
    }

    restoreIo(piped) {
        if(!this.restored) {
            if(this.doorPty) {
                this.doorPty.kill();
            }

            if(this.client.term.output) {
                this.client.term.output.unpipe(piped);
                this.client.term.output.resume();
            }
            this.restored = true;
        }
    }
};
<|endoftext|>


<commit_before>const
    events = require('events'),
    url = require('url'),
    querystring = require('querystring'),
    util = require('util'),
    debug = require('debug')('EndPoint'),
    debugTimeout = require('debug')('EndPoint:timeout')
    ;

function EndPoint(http, client, url, channelID, startingVersion) {
    this.http = http;
    this.client = client;
    this.url = url;
    this.channelID = channelID;

    // startingVersion allows us to set a different version point for
    // each channel endpoint. This helps detect dupes
    this.version = (typeof(startingVersion) != 'undefined') ? startingVersion : 0;

    // for keeping track
    this.requestCount = 0;
    this.sentStartTime = 0;

    this.expectedVersion = 0;

    this.timeoutID = null;
    this.timedout = false;

    var self = this;

    client.on(channelID, function(version) {

        // timedout is set when we send the initial request
        if (self.timedout === true) return;

        // no need to timeout anymore
        clearTimeout(self.timeoutID);

        if (version === self.version) {
            // it's not really a dup, the protocol can send back
            // the current version id of all channels so the
            // proper? behaviour is to just ignore it
            return;
        }

        if (version === self.expectedVersion) {
            self.emitResult('GOT_VERSION_OK', Date.now() - self.serverPutTime);
            self.version = self.expectedVersion;
            return;
        }

        // finally ... we don't know what to do w/ it
        self.emitResult('ERR_VER_INVALID', {
            expected: self.expectedVersion,
            got: version
        });
    });
};
util.inherits(EndPoint, events.EventEmitter);

EndPoint.prototype.sendNextVersion = function(waitWSResponse) {
    waitWSResponse = waitWSResponse || 1000;
    var self = this;

    this.sentStartTime = Date.now();
    this.timedout = false;

    var u = url.parse(this.url);

    this.expectedVersion = this.version + 1;
    this.requestCount += 1;

    var requestID = this.requestCount;

    var data = querystring.stringify({'version': this.expectedVersion.toString()});
    var opts = {
        hostname: u.hostname,
        port: u.port,
        method: 'PUT',
        path: u.path,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': data.length
        }
    };

    debug('Sending req #%d on %s, qs=%s', this.requestCount, this.channelID, data);
    var req = this.http.request(opts, function(res) {
        debug("got response %s", res.statusCode);

        var body = '';

        res.on('data', function(chunk) {
            body += chunk;
        });

        res.once('end', function() {
            if(res.statusCode !== 200) {
                self.emitResult('PUT_FAIL', {
                    id: requestID,
                    code: res.statusCode,
                    body: body
                });
            } else {
                self.serverPutTime = Date.now();
                self.emitResult('PUT_OK', {
                    id: requestID,
                    code: res.statusCode,
                    body: body
                });

                // set a timeout for the websocket to send our version
                // back. This is to measure time it takes for go to
                // process and send back through the ws
                self.timeoutID = setTimeout(function() {

                    // possible weird race condition w/ timeout and
                    // the clearTimeout above?
                    if (self.version != self.expectedVersion) {
                        debugTimeout("time callback, but version already changed");
                        return;
                    }

                    self.timedout = true;
                    self.emitResult("TIMEOUT", waitWSResponse)
                }, waitWSResponse); // make this configurable
            }
        });
    });

    req.on('error', function(e) {
        self.emitResult('ERR_NETWORK', e);
        debug('Request Error: %s', e);
    });

    req.write(data);
    req.end();
};

EndPoint.prototype.emitResult = function(status, data) {
    data = data || false;

    var t = Date.now() - this.sentStartTime;

    this.emit('result', {
        endpoint: this
        , channelID: this.channelID
        , status: status
        , time: t
        , data: data
    });
}

module.exports = EndPoint;
<commit_msg>add more debug output to find timeout/race condition<commit_after>const
    events = require('events'),
    url = require('url'),
    querystring = require('querystring'),
    util = require('util'),
    debug = require('debug')('EndPoint'),
    debugTimeout = require('debug')('EndPoint:timeout')
    ;

function EndPoint(http, client, url, channelID, startingVersion) {
    this.http = http;
    this.client = client;
    this.url = url;
    this.channelID = channelID;

    // startingVersion allows us to set a different version point for
    // each channel endpoint. This helps detect dupes
    this.version = (typeof(startingVersion) != 'undefined') ? startingVersion : 0;

    // for keeping track
    this.requestCount = 0;
    this.sentStartTime = 0;

    this.expectedVersion = 0;

    this.timeoutID = null;
    this.timedout = false;

    var self = this;

    client.on(channelID, function(version) {

        // timedout is set when we send the initial request
        if (self.timedout === true) return;

        // no need to timeout anymore
        clearTimeout(self.timeoutID);

        if (version === self.version) {
            // it's not really a dup, the protocol can send back
            // the current version id of all channels so the
            // proper? behaviour is to just ignore it
            return;
        }

        if (version === self.expectedVersion) {
            self.emitResult('GOT_VERSION_OK', Date.now() - self.serverPutTime);
            self.version = self.expectedVersion;
            return;
        }

        // finally ... we don't know what to do w/ it
        self.emitResult('ERR_VER_INVALID', {
            expected: self.expectedVersion,
            got: version
        });
    });
};
util.inherits(EndPoint, events.EventEmitter);

EndPoint.prototype.sendNextVersion = function(waitWSResponse) {
    waitWSResponse = waitWSResponse || 1000;
    var self = this;

    this.sentStartTime = Date.now();
    this.timedout = false;

    var u = url.parse(this.url);

    this.expectedVersion = this.version + 1;
    this.requestCount += 1;

    var requestID = this.requestCount;

    var data = querystring.stringify({'version': this.expectedVersion.toString()});
    var opts = {
        hostname: u.hostname,
        port: u.port,
        method: 'PUT',
        path: u.path,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': data.length
        }
    };

    debug('Sending req #%d on %s, qs=%s', this.requestCount, this.channelID, data);
    var req = this.http.request(opts, function(res) {
        debug("got response %s", res.statusCode);

        var body = '';

        res.on('data', function(chunk) {
            body += chunk;
        });

        res.once('end', function() {
            if(res.statusCode !== 200) {
                self.emitResult('PUT_FAIL', {
                    id: requestID,
                    code: res.statusCode,
                    body: body
                });
            } else {
                self.serverPutTime = Date.now();
                self.emitResult('PUT_OK', {
                    id: requestID,
                    code: res.statusCode,
                    body: body
                });

                // set a timeout for the websocket to send our version
                // back. This is to measure time it takes for go to
                // process and send back through the ws
                self.timeoutID = setTimeout(function() {

                    // possible weird race condition w/ timeout and
                    // the clearTimeout above?
                    if (self.version != self.expectedVersion) {
                        debugTimeout("time callback, but version already changed");
                        return;
                    }

                    debugTimeout("Timed out. ver: %d, expected: %d",
                        self.version,
                        self.expectedVersion
                    );

                    self.timedout = true;
                    self.emitResult("TIMEOUT", waitWSResponse)
                }, waitWSResponse); // make this configurable
            }
        });
    });

    req.on('error', function(e) {
        self.emitResult('ERR_NETWORK', e);
        debug('Request Error: %s', e);
    });

    req.write(data);
    req.end();
};

EndPoint.prototype.emitResult = function(status, data) {
    data = data || false;

    var t = Date.now() - this.sentStartTime;

    this.emit('result', {
        endpoint: this
        , channelID: this.channelID
        , status: status
        , time: t
        , data: data
    });
}

module.exports = EndPoint;
<|endoftext|>


<commit_before>var request = require('request')
var _ = require('lodash')

var ejson = require('@carbon-io/ejson')
var HttpErrors = require('@carbon-io/http-errors')

var Collection = require('./Collection')

DEFAULT_OPTIONS = {
  json: true,
  strictSSL: true
}

/***************************************************************************************************
 * @class Endpoint
 * @description This is a private class. Should not call Endpoint constructor directly,
 *              but should use RestClient.getEndpoint(path)
 * @memberof carbon-client
 */
function Endpoint() {
  /*****************************************************************************
   * @property {xxx} client -- xxx
   */
  this.client = null
  /*****************************************************************************
   * @property {string} path -- xxx
   */
  this.path = null
  /*****************************************************************************
   * @property {xxx} parent -- xxx
   */
  this.parent = null
  /*****************************************************************************
   * @property {xxx} endpoints -- xxx
   */
  this.endpoints = {}
}

/***************************************************************************************************
 * collection class class variable
 *
 * @static
 *
 * @type {Collection|*|exports}
 */

Endpoint.collectionClass = Collection

/***************************************************************************************************
 * @method getAbsolutePath
 * @description getAbsolutePath description
 * returns {xxx} -- xxx
 */
Endpoint.prototype.getAbsolutePath = function() {
  if (!this.parent) {
    return '/'
  }

  var parentPath = this.parent.getAbsolutePath()
  if (parentPath === '/') {
    parentPath = ''
  }

  return parentPath + '/' + (this.path ? this.path : '')
}

/***************************************************************************************************
 * @method getFullUrl
 * @description getFullUrl description
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.getFullUrl = function() {
  var absolutePath = this.getAbsolutePath()
  if (absolutePath === '/') {
    absolutePath = ''
  }
  return this.client.url + absolutePath
}

/***************************************************************************************************
 * @method getEndpoint
 * @description getEndpoint description
 * @param {string} path -- (i.e. 'foo' or 'foo/bar')
 * @returns {xxx}
 */
Endpoint.prototype.getEndpoint = function(path) {
  if (!path) {
    return null // XXX null or this?
  }

  var pathList = path.split('/')
  if (pathList.length === 0) { /// XXX null or this?
    return null
  }

  if (pathList[0] === '') {
    // means path started with '/' XXX maybe we should not trim here and go
    // rel to root?
    pathList.shift()
  }

  if (pathList[pathList.length - 1] === '') { // means path ended with '/'
    pathList.pop()
  }

  return this._getEndpointFromPathList(pathList)
}

/***************************************************************************************************
 * @method getCollection
 * @description getCollection description
 * @param {string} path -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.getCollection = function(path) {
  var result = undefined

  var endpoint = this.getEndpoint(path)
  if (endpoint) {
    result = new Collection(endpoint)
  }

  return result
}

/***************************************************************************************************
 * @method get
 * @description Supported calling forms:
 *              get(cb)
 *              get(options, cb
 * @param {Object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.get = function() {
  return this._performOperation('get', arguments)
}

/***************************************************************************************************
 * @method post
 * @description Supported calling forms:
 *              post(cb)
 *              post(body, cb)
 *              post(body, options, cb)
 * @param {xxx} body -- (optional)
 * @param {xxx} options -- (optional)
 * @param {xxx} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.post = function() {
  return this._performOperation('post', arguments)
}

/***************************************************************************************************
 * @method put
 * @description Supported calling forms:
 *              put(cb)
 *              put(body, cb)
 *              put(body, options, cb)
 * @param {object} body -- (optional)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.put = function() {
  return this._performOperation('put', arguments)
}

/***************************************************************************************************
 * @method patch
 * @description Supported calling forms:
 *              patch(cb)
 *              patch(body, cb)
 *              patch(body, options, cb)
 * @param {object} body -- (optional)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.patch = function() {
  return this._performOperation('patch', arguments)
}

/***************************************************************************************************
 * @method delete
 * @description Supported calling forms:
 *              delete(cb)
 *              delete(options, cb)
 * @param {oject} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.delete = function() {
  return this._performOperation('delete', arguments)
}

/***************************************************************************************************
 * @method head
 * @description Supported calling forms:
 *              head(cb)
 *              head(options, cb)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.head = function() {
  return this._performOperation('head', arguments)
}

/***************************************************************************************************
 * @method options
 * @description Supported calling forms:
 *              options(cb)
 *              options(options, cb)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.options = function() {
  return this._performOperation('options', arguments)
}

/***************************************************************************************************
 * @method httpError
 * @description xxx
 * @param {xxx} code -- xxx
 * @param {xxx} message -- xxx
 * @param {xxx} response -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.httpError = function(code, message, response) {
  var errorClass = HttpErrors.codes[code]
  if (!errorClass) {
    errorClass = Error
  }

  var error = new errorClass(message, response)
  return error
}

/***************************************************************************************************
 * @method  _performOperation
 * @description _performOperation description
 * @param {Function} method -- xxx
 * @param {object} operationArguments -- xxx
 * @returns {undefined} -- undefined
 */
Endpoint.prototype._performOperation = function(method, operationArguments) {
  var body = undefined
  var options = {}
  var cb = undefined

  // Figure out what the arguments are
  switch (operationArguments.length) {
    case 3:
      body = operationArguments[0]
      options = operationArguments[1]
      cb = operationArguments[2]
      break
    case 2:
      if (['post', 'put', 'patch'].indexOf(method.toLowerCase()) !== -1) {
        body = operationArguments[0]
      } else {
        options = operationArguments[0]
      }
      cb = operationArguments[1]
      break
    case 1:
      cb = operationArguments[0]
      break
    default:
  }

  // apply default options
  options = this._applyDefaultOptions(options)

  // use ejson to serialize body so that extended types are preserved
  if(options.json && body) {
    try {
      body = ejson.serialize(body)
    } catch(e) {
      // ignore
    }
  }

  var parameters = options.parameters || options.params
  // Execute the request
  var self = this
  // XXX should we allow the user to influence redirect behavior?
  request({
    url: this.getFullUrl(),
    method: method.toUpperCase(),
    // qs: note that we serialize the json parameters to avoid wrong type conversions on server-side
    // see https://github.com/ljharb/qs/issues/91
    qs: serializeJsonParams(parameters),
    useQuerystring: true,
    body: body,
    json: options.json,
    headers: options.headers,
    strictSSL: options.strictSSL,
    cert: options.cert,
    key: options.key,
    ca: options.ca,
    forever: options.forever,
    timeout: options.timeout
  }, function(e, r, b) {
    // use ejson to deserialize response body
    if(options.json) {
      try {
        r.body = ejson.deserialize(r.body)
      } catch(e) {
        // ignore
      }
    }
    // redirects are handled by request, so no need to
    if (e) {
      var msg = e.message || JSON.stringify(e)
      cb(self.httpError(e.code, msg, r), r)
    } else if (r.statusCode >= 400) {
      var msg = r.body.message || JSON.stringify(r.body)
      cb(self.httpError(r.statusCode, msg, r), r)
    } else {
      cb(null, r)
    }
  })
}

/***************************************************************************************************
 * @method _getEndpointFromPathList
 * @description _getEndpointFromPathList description
 * @param {object} pathList -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype._getEndpointFromPathList = function(pathList) {
  // get child Endpoint
  var child = this._getChild(pathList.shift())

  // recurse if there is path left
  if (pathList.length > 0) {
    return child._getEndpointFromPathList(pathList)
  } else {
    return child
  }
}

/***************************************************************************************************
 * @method _applyDefaultOptions
 * @description applies client's default options to the specified options
 * @param {object} options -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype._applyDefaultOptions = function(options) {
  var mergedOptions = _.cloneDeep(options)
  var defaultOptions = _.cloneDeep(DEFAULT_OPTIONS)
  if (this.client) {
    mergedOptions = _.merge(defaultOptions,
      this.client.defaultOptions,
      mergedOptions)  // give preference to options passed
    // to `_performOperation`
  }

  return mergedOptions
}

/***************************************************************************************************
 * @method _getChild
 * @description _getChild description
 * @param {string} path -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype._getChild = function(path) {
  if (!this.endpoints[path]) {
    var newEndpoint = new Endpoint()
    newEndpoint.path = path
    newEndpoint.parent = this
    newEndpoint.client = this.client

    this.endpoints[path] = newEndpoint
  }

  return this.endpoints[path]
}

/***************************************************************************************************
 * @method serializeJsonParams
 * @param {object} params -- xxx
 * @returns {object} -- a new object with params json values serialized as json strings
 *
 */
function serializeJsonParams(params) {

  var result = {}
  if (params) {
    for (var key in params) {
      if (_.isArray(params[key])) {
        result[key] = _.map(params[key], function(val) {
          return _.isObject(val) ? ejson.stringify(val) : val
        })
      } else if (_.isObject(params[key])) {
        result[key] = ejson.stringify(params[key])
      } else {
        result[key] = params[key]
      }
    }
  }

  return result
}

/******************************************************************************
 * exports
 */
module.exports = Endpoint

<commit_msg>handle body undefined<commit_after>var request = require('request')
var _ = require('lodash')

var ejson = require('@carbon-io/ejson')
var HttpErrors = require('@carbon-io/http-errors')

var Collection = require('./Collection')

DEFAULT_OPTIONS = {
  json: true,
  strictSSL: true
}

/***************************************************************************************************
 * @class Endpoint
 * @description This is a private class. Should not call Endpoint constructor directly,
 *              but should use RestClient.getEndpoint(path)
 * @memberof carbon-client
 */
function Endpoint() {
  /*****************************************************************************
   * @property {xxx} client -- xxx
   */
  this.client = null
  /*****************************************************************************
   * @property {string} path -- xxx
   */
  this.path = null
  /*****************************************************************************
   * @property {xxx} parent -- xxx
   */
  this.parent = null
  /*****************************************************************************
   * @property {xxx} endpoints -- xxx
   */
  this.endpoints = {}
}

/***************************************************************************************************
 * collection class class variable
 *
 * @static
 *
 * @type {Collection|*|exports}
 */

Endpoint.collectionClass = Collection

/***************************************************************************************************
 * @method getAbsolutePath
 * @description getAbsolutePath description
 * returns {xxx} -- xxx
 */
Endpoint.prototype.getAbsolutePath = function() {
  if (!this.parent) {
    return '/'
  }

  var parentPath = this.parent.getAbsolutePath()
  if (parentPath === '/') {
    parentPath = ''
  }

  return parentPath + '/' + (this.path ? this.path : '')
}

/***************************************************************************************************
 * @method getFullUrl
 * @description getFullUrl description
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.getFullUrl = function() {
  var absolutePath = this.getAbsolutePath()
  if (absolutePath === '/') {
    absolutePath = ''
  }
  return this.client.url + absolutePath
}

/***************************************************************************************************
 * @method getEndpoint
 * @description getEndpoint description
 * @param {string} path -- (i.e. 'foo' or 'foo/bar')
 * @returns {xxx}
 */
Endpoint.prototype.getEndpoint = function(path) {
  if (!path) {
    return null // XXX null or this?
  }

  var pathList = path.split('/')
  if (pathList.length === 0) { /// XXX null or this?
    return null
  }

  if (pathList[0] === '') {
    // means path started with '/' XXX maybe we should not trim here and go
    // rel to root?
    pathList.shift()
  }

  if (pathList[pathList.length - 1] === '') { // means path ended with '/'
    pathList.pop()
  }

  return this._getEndpointFromPathList(pathList)
}

/***************************************************************************************************
 * @method getCollection
 * @description getCollection description
 * @param {string} path -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.getCollection = function(path) {
  var result = undefined

  var endpoint = this.getEndpoint(path)
  if (endpoint) {
    result = new Collection(endpoint)
  }

  return result
}

/***************************************************************************************************
 * @method get
 * @description Supported calling forms:
 *              get(cb)
 *              get(options, cb
 * @param {Object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.get = function() {
  return this._performOperation('get', arguments)
}

/***************************************************************************************************
 * @method post
 * @description Supported calling forms:
 *              post(cb)
 *              post(body, cb)
 *              post(body, options, cb)
 * @param {xxx} body -- (optional)
 * @param {xxx} options -- (optional)
 * @param {xxx} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.post = function() {
  return this._performOperation('post', arguments)
}

/***************************************************************************************************
 * @method put
 * @description Supported calling forms:
 *              put(cb)
 *              put(body, cb)
 *              put(body, options, cb)
 * @param {object} body -- (optional)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.put = function() {
  return this._performOperation('put', arguments)
}

/***************************************************************************************************
 * @method patch
 * @description Supported calling forms:
 *              patch(cb)
 *              patch(body, cb)
 *              patch(body, options, cb)
 * @param {object} body -- (optional)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.patch = function() {
  return this._performOperation('patch', arguments)
}

/***************************************************************************************************
 * @method delete
 * @description Supported calling forms:
 *              delete(cb)
 *              delete(options, cb)
 * @param {oject} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.delete = function() {
  return this._performOperation('delete', arguments)
}

/***************************************************************************************************
 * @method head
 * @description Supported calling forms:
 *              head(cb)
 *              head(options, cb)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.head = function() {
  return this._performOperation('head', arguments)
}

/***************************************************************************************************
 * @method options
 * @description Supported calling forms:
 *              options(cb)
 *              options(options, cb)
 * @param {object} options -- (optional)
 * @param {Function} cb -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.options = function() {
  return this._performOperation('options', arguments)
}

/***************************************************************************************************
 * @method httpError
 * @description xxx
 * @param {xxx} code -- xxx
 * @param {xxx} message -- xxx
 * @param {xxx} response -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype.httpError = function(code, message, response) {
  var errorClass = HttpErrors.codes[code]
  if (!errorClass) {
    errorClass = Error
  }

  var error = new errorClass(message, response)
  return error
}

/***************************************************************************************************
 * @method  _performOperation
 * @description _performOperation description
 * @param {Function} method -- xxx
 * @param {object} operationArguments -- xxx
 * @returns {undefined} -- undefined
 */
Endpoint.prototype._performOperation = function(method, operationArguments) {
  var body = undefined
  var options = {}
  var cb = undefined

  // Figure out what the arguments are
  switch (operationArguments.length) {
    case 3:
      body = operationArguments[0]
      options = operationArguments[1]
      cb = operationArguments[2]
      break
    case 2:
      if (['post', 'put', 'patch'].indexOf(method.toLowerCase()) !== -1) {
        body = operationArguments[0]
      } else {
        options = operationArguments[0]
      }
      cb = operationArguments[1]
      break
    case 1:
      cb = operationArguments[0]
      break
    default:
  }

  // apply default options
  options = this._applyDefaultOptions(options)

  // use ejson to serialize body so that extended types are preserved
  if(options.json && body) {
    try {
      body = ejson.serialize(body)
    } catch(e) {
      // ignore
    }
  }

  var parameters = options.parameters || options.params
  // Execute the request
  var self = this
  // XXX should we allow the user to influence redirect behavior?
  request({
    url: this.getFullUrl(),
    method: method.toUpperCase(),
    // qs: note that we serialize the json parameters to avoid wrong type conversions on server-side
    // see https://github.com/ljharb/qs/issues/91
    qs: serializeJsonParams(parameters),
    useQuerystring: true,
    body: body,
    json: options.json,
    headers: options.headers,
    strictSSL: options.strictSSL,
    cert: options.cert,
    key: options.key,
    ca: options.ca,
    forever: options.forever,
    timeout: options.timeout
  }, function(e, r, b) {
    // use ejson to deserialize response body
    if(options.json) {
      try {
        r.body = ejson.deserialize(r.body)
      } catch(e) {
        // ignore
      }
    }
    // redirects are handled by request, so no need to
    if (e) {
      var msg = e.message || JSON.stringify(e)
      cb(self.httpError(e.code, msg, r), r)
    } else if (r.statusCode >= 400) {
      var msg = _.get(r, 'body.message') || JSON.stringify(r.body)
      cb(self.httpError(r.statusCode, msg, r), r)
    } else {
      cb(null, r)
    }
  })
}

/***************************************************************************************************
 * @method _getEndpointFromPathList
 * @description _getEndpointFromPathList description
 * @param {object} pathList -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype._getEndpointFromPathList = function(pathList) {
  // get child Endpoint
  var child = this._getChild(pathList.shift())

  // recurse if there is path left
  if (pathList.length > 0) {
    return child._getEndpointFromPathList(pathList)
  } else {
    return child
  }
}

/***************************************************************************************************
 * @method _applyDefaultOptions
 * @description applies client's default options to the specified options
 * @param {object} options -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype._applyDefaultOptions = function(options) {
  var mergedOptions = _.cloneDeep(options)
  var defaultOptions = _.cloneDeep(DEFAULT_OPTIONS)
  if (this.client) {
    mergedOptions = _.merge(defaultOptions,
      this.client.defaultOptions,
      mergedOptions)  // give preference to options passed
    // to `_performOperation`
  }

  return mergedOptions
}

/***************************************************************************************************
 * @method _getChild
 * @description _getChild description
 * @param {string} path -- xxx
 * @returns {xxx} -- xxx
 */
Endpoint.prototype._getChild = function(path) {
  if (!this.endpoints[path]) {
    var newEndpoint = new Endpoint()
    newEndpoint.path = path
    newEndpoint.parent = this
    newEndpoint.client = this.client

    this.endpoints[path] = newEndpoint
  }

  return this.endpoints[path]
}

/***************************************************************************************************
 * @method serializeJsonParams
 * @param {object} params -- xxx
 * @returns {object} -- a new object with params json values serialized as json strings
 *
 */
function serializeJsonParams(params) {

  var result = {}
  if (params) {
    for (var key in params) {
      if (_.isArray(params[key])) {
        result[key] = _.map(params[key], function(val) {
          return _.isObject(val) ? ejson.stringify(val) : val
        })
      } else if (_.isObject(params[key])) {
        result[key] = ejson.stringify(params[key])
      } else {
        result[key] = params[key]
      }
    }
  }

  return result
}

/******************************************************************************
 * exports
 */
module.exports = Endpoint

<|endoftext|>