<commit_before>/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, window */

define(function (require, exports, module) {
    'use strict';

    // Load dependent modules
    var HTMLUtils       = require("language/HTMLUtils"),
        Menus           = require("command/Menus"),
        StringUtils     = require("utils/StringUtils"),
		HTMLTags        = require("text!CodeHints/HtmlTags.json"),
        EditorManager   = require("editor/EditorManager");

    /**
     * @private
     * Parse the code hints from JSON data and extract all hints from property names.
     * @param {string} a JSON string that has the code hints data
     * @return {!Array.<string>} An array of code hints read from the JSON data source.
     */
    function _getCodeHints(jsonStr) {
        var hintObj = JSON.parse(jsonStr);
        return $.map(hintObj, function (value, key) {
            return key;
        }).sort();
    }

    /**
     * @constructor
     *
     */
    function CodeHintList() {
        // TODO Ty: should pass array of objects instead of strings as source so richer data can
        // be passed to hint list
        this.source = _getCodeHints(HTMLTags);
        this.query = "";
        this.displayList = [];
        this.options = {
            maxResults: 8
        };

        this.opened = false;
        this.selectedIndex = -1;
        this.editor = null;
        this.$hintMenu = $("

");

        var $toggle = $("")
            .hide();

        this.$hintMenu.append($toggle)
            .append("

");

        $("#codehint-menu-bar > ul").append(this.$hintMenu);

    }

    /**
     * Adds a single item to the hint list
     * @param {string} name
     */
    CodeHintList.prototype.addItem = function (name) {
        var displayName = name.replace(
            new RegExp(StringUtils.regexEscape(this.query), "gi"),
            "$&"
        );

        var $item = $("
" + displayName + "
");
        var self = this;

        // TODO: factor click handler into separate function
        $item.on("click", function () {

            var start = {line: -1, ch: -1},
                end = {line: -1, ch: -1},
                cursor = self.editor.getCursorPos(),
                tagInfo = HTMLUtils.getTagInfo(self.editor, cursor),
                charCount = 0;

            if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
                charCount = tagInfo.tagName.length;
            } else if (tagInfo.position.tokenType === HTMLUtils.ATTR_NAME) {
                charCount = tagInfo.attr.name.length;
            } else if (tagInfo.position.tokenType === HTMLUtils.ATTR_VALUE) {
                charCount = tagInfo.attr.value.length;
            }

            end.line = start.line = cursor.line;
            start.ch = cursor.ch - tagInfo.position.offset;
            end.ch = start.ch + charCount;

            if (start.ch !== "-1" && end.ch !== "-1") {
                self.editor.document.replaceRange(name, start, end);
            } else {
                self.editor.document.replaceRange(name, start);
            }
        });

        this.$hintMenu.find("ul.dropdown-menu")
            .append($item);
    };

    /**
     * Rebuilds the hint list based on this.query
     */
    CodeHintList.prototype.updateList = function () {
        this.filterList();
        this.buildListView();
    };

    /**
     * Removes all list items from hint list
     */
    CodeHintList.prototype.clearList = function () {
        this.$hintMenu.find("li").remove();
    };

    /**
     * Rebuilds the list items for the hint list based on the display list
     * array
     */
    CodeHintList.prototype.buildListView = function () {
        this.clearList();
        var self = this;
        var count = 0;
        $.each(this.displayList, function (index, item) {
            if (count > self.options.maxResults) {
                return false;
            }
            self.addItem(item);
            count++;
        });

        if (count === 0) {
            this.close();
        } else {
            // Select the first item in the list
            this.setSelectedIndex(0);
        }
    };

    /**
     * Figures out the text to use for the hint list query based on the text
     * around the cursor. Query is the text from the start of a tag to the current
     * cursor position
     */
    CodeHintList.prototype.updateQueryFromCurPos = function () {
        var pos = this.editor.getCursorPos(),
            cursor = this.editor.indexFromPos(pos),
            tagInfo = HTMLUtils.getTagInfo(this.editor, pos);

        if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
            var text = this.editor.document.getText(),
                start = text.lastIndexOf("<", cursor) + 1;
            if (start < cursor) {
                this.query = text.slice(start, cursor);
            } else {
                this.query = null;
            }
        } else {
            this.query = null;
        }
    };

    CodeHintList.prototype.setSelectedIndex = function (index) {
        var items = this.$hintMenu.find("li");

        // Range check
        index = Math.max(0, Math.min(index, items.length - 1));

        // Clear old highlight
        if (this.selectedIndex !== -1) {
            $(items[this.selectedIndex]).find("a").removeClass("highlight");
        }

        // Highlight the new selected item
        this.selectedIndex = index;
        $(items[this.selectedIndex]).find("a").addClass("highlight");
    };

    /**
     * Handles key presses when the hint list is being displayed
     * @param {Editor} editor
     * @param {KeyBoardEvent} keyEvent
     */
    CodeHintList.prototype.handleKeyEvent = function (editor, event) {
        var keyCode = event.keyCode;

        // Up arrow, down arrow and enter key are always handled here
        if (keyCode === 38 || keyCode === 40 || keyCode === 13) {
            if (event.type === "keydown") {
                if (keyCode === 38) { // Up arrow
                    this.setSelectedIndex(this.selectedIndex - 1);
                } else if (keyCode === 40) { // Down arrow
                    this.setSelectedIndex(this.selectedIndex + 1);
                } else { // Enter/return key
                    // Trigger a click handler to commmit the selected item
                    $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click");

                    // Close the list
                    this.close();
                }
            }

            event.preventDefault();
            return;
        }

        // All other key events trigger a rebuild of the list, but only
        // on keyup events
        if (event.type !== "keyup") {
            return;
        }

        this.updateQueryFromCurPos();
        this.updateList();
    };

    /**
     * Return true if the CodeHistList is open.
     */
    CodeHintList.prototype.isOpen = function () {
        // We don't get a notification when the dropdown closes. The best
        // we can do is keep an "opened" flag and check to see if we
        // still have the "open" class applied.
        if (this.opened && !this.$hintMenu.hasClass("open")) {
            this.opened = false;
        }

        return this.opened;
    };

    /**
     * Displays the hint list at the current cursor position
     */
    CodeHintList.prototype.open = function (editor) {
        this.editor = editor;

        Menus.closeAll();

        this.updateQueryFromCurPos();
        this.updateList();

        if (this.displayList.length) {
            var hintPos = this.calcHintListLocation();
            this.$hintMenu.addClass("open")
                       .css({"left": hintPos.left, "top": hintPos.top});
            this.opened = true;
        }
    };

    CodeHintList.prototype.close = function () {
        Menus.closeAll();
        this.opened = false;
    };

    /**
     * Computes top left location for hint list so that the list is not clipped by the window
     * @ return {Object.<left: Number, to: Number> }
     */
    CodeHintList.prototype.calcHintListLocation = function () {
        var cursor = this.editor._codeMirror.cursorCoords(),
            posTop  = cursor.y,
            posLeft = cursor.x,
            $window = $(window),
            $menuWindow = this.$hintMenu.children("ul");

        // TODO Ty: factor out menu repositioning logic so code hints and Context menus share code
        // adjust positioning so menu is not clipped off bottom or right
        var bottomOverhang = posTop + 25 + $menuWindow.height() - $window.height();
        if (bottomOverhang > 0) {
            posTop = Math.max(0, posTop - bottomOverhang);
        }
        // todo: should be shifted by line height
        posTop -= 15;   // shift top for hidden parent element
        //posLeft += 5;

        var rightOverhang = posLeft + $menuWindow.width() - $window.width();
        if (rightOverhang > 0) {
            posLeft = Math.max(0, posLeft - rightOverhang);
        }

        return {left: posLeft, top: posTop};
    };

    /**
     * Filters the source list by query and stores the result in displayList
     */
    CodeHintList.prototype.filterList = function () {
        var self = this;
        this.displayList = $.map(this.source, function (item) {
            if (item.indexOf(self.query) === 0) {
                return item;
            }
        }).sort();
        // TODO: better sorting. Should rank tags based on portion of query that is present in tag
    };

    // TODO Ty: singleton for now. Figure out broader strategy for hint list across editors
    // and different types of hint list.
    var hintList = new CodeHintList();

     /**
      * Show the code hint list near the current cursor position for the specified editor
      * @param {Editor}
      */
    function _showHint(editor) {
    // To be used later when we get to attribute hinting
    //       var pos = editor.getCursorPos();
    //       var tagInfo = HTMLUtils.getTagInfo(editor, pos);
    //       if (tagInfo.position.type === HTMLUtils.ATTR_VALUE) {
    //           if (tagInfo.attr.name === "class") {
    //               _triggerClassHint(editor, pos, tagInfo);
    //           } else if (tagInfo.attr.name === "id") {
    //               _triggerIdHint(editor, pos, tagInfo);
    //           }
    //       }
    //      else if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
    //           console.log(_getCodeHints(HTMLTags, tagInfo.tagName));
    //       }

        hintList.open(editor);
    }

    function handleKeyEvent(editor, event) {
        // For now we only handle hints in html
        if (editor.getModeForSelection() !== "html") {
            return;
        }

        // Check for Control+Space
        if (event.type === "keydown" && event.keyCode === 32 && event.ctrlKey) {
            _showHint(editor);
            event.preventDefault();
        }

        // Pass to the hint list, if it's open
        if (hintList && hintList.isOpen()) {
            hintList.handleKeyEvent(editor, event);
        }
    }

    // Define public API
    exports.handleKeyEvent = handleKeyEvent;
});
<commit_msg>Fix my careless mistake; should be "<=" in comparison.<commit_after>/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, window */

define(function (require, exports, module) {
    'use strict';

    // Load dependent modules
    var HTMLUtils       = require("language/HTMLUtils"),
        Menus           = require("command/Menus"),
        StringUtils     = require("utils/StringUtils"),
		HTMLTags        = require("text!CodeHints/HtmlTags.json"),
        EditorManager   = require("editor/EditorManager");

    /**
     * @private
     * Parse the code hints from JSON data and extract all hints from property names.
     * @param {string} a JSON string that has the code hints data
     * @return {!Array.<string>} An array of code hints read from the JSON data source.
     */
    function _getCodeHints(jsonStr) {
        var hintObj = JSON.parse(jsonStr);
        return $.map(hintObj, function (value, key) {
            return key;
        }).sort();
    }

    /**
     * @constructor
     *
     */
    function CodeHintList() {
        // TODO Ty: should pass array of objects instead of strings as source so richer data can
        // be passed to hint list
        this.source = _getCodeHints(HTMLTags);
        this.query = "";
        this.displayList = [];
        this.options = {
            maxResults: 8
        };

        this.opened = false;
        this.selectedIndex = -1;
        this.editor = null;
        this.$hintMenu = $("

");

        var $toggle = $("")
            .hide();

        this.$hintMenu.append($toggle)
            .append("

");

        $("#codehint-menu-bar > ul").append(this.$hintMenu);

    }

    /**
     * Adds a single item to the hint list
     * @param {string} name
     */
    CodeHintList.prototype.addItem = function (name) {
        var displayName = name.replace(
            new RegExp(StringUtils.regexEscape(this.query), "gi"),
            "$&"
        );

        var $item = $("
" + displayName + "
");
        var self = this;

        // TODO: factor click handler into separate function
        $item.on("click", function () {

            var start = {line: -1, ch: -1},
                end = {line: -1, ch: -1},
                cursor = self.editor.getCursorPos(),
                tagInfo = HTMLUtils.getTagInfo(self.editor, cursor),
                charCount = 0;

            if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
                charCount = tagInfo.tagName.length;
            } else if (tagInfo.position.tokenType === HTMLUtils.ATTR_NAME) {
                charCount = tagInfo.attr.name.length;
            } else if (tagInfo.position.tokenType === HTMLUtils.ATTR_VALUE) {
                charCount = tagInfo.attr.value.length;
            }

            end.line = start.line = cursor.line;
            start.ch = cursor.ch - tagInfo.position.offset;
            end.ch = start.ch + charCount;

            if (start.ch !== "-1" && end.ch !== "-1") {
                self.editor.document.replaceRange(name, start, end);
            } else {
                self.editor.document.replaceRange(name, start);
            }
        });

        this.$hintMenu.find("ul.dropdown-menu")
            .append($item);
    };

    /**
     * Rebuilds the hint list based on this.query
     */
    CodeHintList.prototype.updateList = function () {
        this.filterList();
        this.buildListView();
    };

    /**
     * Removes all list items from hint list
     */
    CodeHintList.prototype.clearList = function () {
        this.$hintMenu.find("li").remove();
    };

    /**
     * Rebuilds the list items for the hint list based on the display list
     * array
     */
    CodeHintList.prototype.buildListView = function () {
        this.clearList();
        var self = this;
        var count = 0;
        $.each(this.displayList, function (index, item) {
            if (count > self.options.maxResults) {
                return false;
            }
            self.addItem(item);
            count++;
        });

        if (count === 0) {
            this.close();
        } else {
            // Select the first item in the list
            this.setSelectedIndex(0);
        }
    };

    /**
     * Figures out the text to use for the hint list query based on the text
     * around the cursor. Query is the text from the start of a tag to the current
     * cursor position
     */
    CodeHintList.prototype.updateQueryFromCurPos = function () {
        var pos = this.editor.getCursorPos(),
            cursor = this.editor.indexFromPos(pos),
            tagInfo = HTMLUtils.getTagInfo(this.editor, pos);

        if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
            var text = this.editor.document.getText(),
                start = text.lastIndexOf("<", cursor) + 1;
            if (start <= cursor) {
                this.query = text.slice(start, cursor);
            } else {
                this.query = null;
            }
        } else {
            this.query = null;
        }
    };

    CodeHintList.prototype.setSelectedIndex = function (index) {
        var items = this.$hintMenu.find("li");

        // Range check
        index = Math.max(0, Math.min(index, items.length - 1));

        // Clear old highlight
        if (this.selectedIndex !== -1) {
            $(items[this.selectedIndex]).find("a").removeClass("highlight");
        }

        // Highlight the new selected item
        this.selectedIndex = index;
        $(items[this.selectedIndex]).find("a").addClass("highlight");
    };

    /**
     * Handles key presses when the hint list is being displayed
     * @param {Editor} editor
     * @param {KeyBoardEvent} keyEvent
     */
    CodeHintList.prototype.handleKeyEvent = function (editor, event) {
        var keyCode = event.keyCode;

        // Up arrow, down arrow and enter key are always handled here
        if (keyCode === 38 || keyCode === 40 || keyCode === 13) {
            if (event.type === "keydown") {
                if (keyCode === 38) { // Up arrow
                    this.setSelectedIndex(this.selectedIndex - 1);
                } else if (keyCode === 40) { // Down arrow
                    this.setSelectedIndex(this.selectedIndex + 1);
                } else { // Enter/return key
                    // Trigger a click handler to commmit the selected item
                    $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click");

                    // Close the list
                    this.close();
                }
            }

            event.preventDefault();
            return;
        }

        // All other key events trigger a rebuild of the list, but only
        // on keyup events
        if (event.type !== "keyup") {
            return;
        }

        this.updateQueryFromCurPos();
        this.updateList();
    };

    /**
     * Return true if the CodeHistList is open.
     */
    CodeHintList.prototype.isOpen = function () {
        // We don't get a notification when the dropdown closes. The best
        // we can do is keep an "opened" flag and check to see if we
        // still have the "open" class applied.
        if (this.opened && !this.$hintMenu.hasClass("open")) {
            this.opened = false;
        }

        return this.opened;
    };

    /**
     * Displays the hint list at the current cursor position
     */
    CodeHintList.prototype.open = function (editor) {
        this.editor = editor;

        Menus.closeAll();

        this.updateQueryFromCurPos();
        this.updateList();

        if (this.displayList.length) {
            var hintPos = this.calcHintListLocation();
            this.$hintMenu.addClass("open")
                       .css({"left": hintPos.left, "top": hintPos.top});
            this.opened = true;
        }
    };

    CodeHintList.prototype.close = function () {
        Menus.closeAll();
        this.opened = false;
    };

    /**
     * Computes top left location for hint list so that the list is not clipped by the window
     * @ return {Object.<left: Number, to: Number> }
     */
    CodeHintList.prototype.calcHintListLocation = function () {
        var cursor = this.editor._codeMirror.cursorCoords(),
            posTop  = cursor.y,
            posLeft = cursor.x,
            $window = $(window),
            $menuWindow = this.$hintMenu.children("ul");

        // TODO Ty: factor out menu repositioning logic so code hints and Context menus share code
        // adjust positioning so menu is not clipped off bottom or right
        var bottomOverhang = posTop + 25 + $menuWindow.height() - $window.height();
        if (bottomOverhang > 0) {
            posTop = Math.max(0, posTop - bottomOverhang);
        }
        // todo: should be shifted by line height
        posTop -= 15;   // shift top for hidden parent element
        //posLeft += 5;

        var rightOverhang = posLeft + $menuWindow.width() - $window.width();
        if (rightOverhang > 0) {
            posLeft = Math.max(0, posLeft - rightOverhang);
        }

        return {left: posLeft, top: posTop};
    };

    /**
     * Filters the source list by query and stores the result in displayList
     */
    CodeHintList.prototype.filterList = function () {
        var self = this;
        this.displayList = $.map(this.source, function (item) {
            if (item.indexOf(self.query) === 0) {
                return item;
            }
        }).sort();
        // TODO: better sorting. Should rank tags based on portion of query that is present in tag
    };

    // TODO Ty: singleton for now. Figure out broader strategy for hint list across editors
    // and different types of hint list.
    var hintList = new CodeHintList();

     /**
      * Show the code hint list near the current cursor position for the specified editor
      * @param {Editor}
      */
    function _showHint(editor) {
    // To be used later when we get to attribute hinting
    //       var pos = editor.getCursorPos();
    //       var tagInfo = HTMLUtils.getTagInfo(editor, pos);
    //       if (tagInfo.position.type === HTMLUtils.ATTR_VALUE) {
    //           if (tagInfo.attr.name === "class") {
    //               _triggerClassHint(editor, pos, tagInfo);
    //           } else if (tagInfo.attr.name === "id") {
    //               _triggerIdHint(editor, pos, tagInfo);
    //           }
    //       }
    //      else if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) {
    //           console.log(_getCodeHints(HTMLTags, tagInfo.tagName));
    //       }

        hintList.open(editor);
    }

    function handleKeyEvent(editor, event) {
        // For now we only handle hints in html
        if (editor.getModeForSelection() !== "html") {
            return;
        }

        // Check for Control+Space
        if (event.type === "keydown" && event.keyCode === 32 && event.ctrlKey) {
            _showHint(editor);
            event.preventDefault();
        }

        // Pass to the hint list, if it's open
        if (hintList && hintList.isOpen()) {
            hintList.handleKeyEvent(editor, event);
        }
    }

    // Define public API
    exports.handleKeyEvent = handleKeyEvent;
});
<|endoftext|>


<commit_before>import Backbone from 'backbone';
import { appendStyles } from 'utils/mixins';

const $ = Backbone.$;

export default Backbone.View.extend({
  initialize() {
    const { model } = this;
    model.view = this;
    this.conf = model.config;
    this.pn = model.get('Panels');
    this.cv = model.get('Canvas');
    model.once('change:ready', () => {
      this.pn.active();
      this.pn.disableButtons();
      model.set('changesCount', 0);
      setTimeout(() => {
        model.trigger('load', model.get('Editor'));
      });
    });
  },

  render() {
    const { model, $el, conf } = this;
    const pfx = conf.stylePrefix;
    const contEl = $(conf.el || `body ${conf.container}`);
    appendStyles(conf.cssIcons, { unique: 1, prepand: 1 });
    $el.empty();

    if (conf.width) contEl.css('width', conf.width);
    if (conf.height) contEl.css('height', conf.height);

    $el.append(this.cv.render());
    $el.append(this.pn.render());
    $el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
    contEl
      .addClass(`${pfx}editor-cont`)
      .empty()
      .append($el);

    return this;
  }
});
<commit_msg>Move changesCount reset<commit_after>import Backbone from 'backbone';
import { appendStyles } from 'utils/mixins';

const $ = Backbone.$;

export default Backbone.View.extend({
  initialize() {
    const { model } = this;
    model.view = this;
    this.conf = model.config;
    this.pn = model.get('Panels');
    this.cv = model.get('Canvas');
    model.once('change:ready', () => {
      this.pn.active();
      this.pn.disableButtons();
      setTimeout(() => {
        model.trigger('load', model.get('Editor'));
        model.set('changesCount', 0);
      });
    });
  },

  render() {
    const { model, $el, conf } = this;
    const pfx = conf.stylePrefix;
    const contEl = $(conf.el || `body ${conf.container}`);
    appendStyles(conf.cssIcons, { unique: 1, prepand: 1 });
    $el.empty();

    if (conf.width) contEl.css('width', conf.width);
    if (conf.height) contEl.css('height', conf.height);

    $el.append(this.cv.render());
    $el.append(this.pn.render());
    $el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
    contEl
      .addClass(`${pfx}editor-cont`)
      .empty()
      .append($el);

    return this;
  }
});
<|endoftext|>


<commit_before>'use strict';

var aFrom        = require('es5-ext/array/from')
  , d            = require('d')
  , isStyleSheet = require('html-dom-ext/link/is-style-sheet')

  , forEach = Array.prototype.forEach, defineProperty = Object.defineProperty;

module.exports = exports = function (nodes, parent) {
	var styleSheetsData, result;
	if (!exports.enabled) return aFrom(nodes);
	styleSheetsData = [];
	result = [];
	forEach.call(nodes, function self(node) {
		var sibling;
		if (isStyleSheet(node)) {
			if (node.hasAttribute('href')) {
				sibling = node.nextSibling;
				if (!sibling) {
					sibling = node.ownerDocument.createTextNode('');
					result.push(defineProperty(sibling, '$siteTreeTemporary', d(true)));
				}
				styleSheetsData.push({ styleSheet: node, nextSibling: sibling });
				return;
			}
		}
		result.push(node);
	});
	if (!styleSheetsData.length) return result;
	setTimeout(function () {
		styleSheetsData.forEach(function (data) {
			if (data.nextSibling.parentNode) {
				data.nextSibling.parentNode.insertBefore(data.styleSheet, data.nextSibling);
				if (data.nextSibling.$siteTreeTemporary) {
					data.nextSibling.parentNode.removeChild(data.nextSibling);
				}
			} else if (data.styleSheet.parentNode) {
				data.styleSheet.parentNode.removeChild(data.styleSheet);
			}
		});
	}, 100);
	return result;
};

exports.enabled = true;
<commit_msg>Update site-tree package<commit_after>'use strict';

var aFrom        = require('es5-ext/array/from')
  , d            = require('d')
  , isStyleSheet = require('html-dom-ext/link/is-style-sheet')

  , forEach = Array.prototype.forEach, defineProperty = Object.defineProperty;

module.exports = exports = function (nodes, parent) {
	var styleSheetsData, result;
	if (!exports.enabled) return aFrom(nodes);
	styleSheetsData = [];
	result = [];
	forEach.call(nodes, function self(node) {
		var sibling;
		if (isStyleSheet(node)) {
			if (node.hasAttribute('href')) {
				sibling = node.nextSibling;
				if (!sibling) {
					sibling = parent.appendChild(node.ownerDocument.createTextNode(''));
					result.push(defineProperty(sibling, '$siteTreeTemporary', d(true)));
				}
				styleSheetsData.push({ styleSheet: node, nextSibling: sibling });
				return;
			}
		}
		result.push(node);
	});
	if (!styleSheetsData.length) return result;
	setTimeout(function () {
		styleSheetsData.forEach(function (data) {
			if (data.nextSibling.parentNode) {
				data.nextSibling.parentNode.insertBefore(data.styleSheet, data.nextSibling);
				if (data.nextSibling.$siteTreeTemporary) {
					data.nextSibling.parentNode.removeChild(data.nextSibling);
				}
			} else if (data.styleSheet.parentNode) {
				data.styleSheet.parentNode.removeChild(data.styleSheet);
			}
		});
	}, 100);
	return result;
};

exports.enabled = true;
<|endoftext|>


<commit_before>import { List } from 'immutable';
import { escapeRegExp } from 'lodash';
import consoleError from 'Lib/consoleError';
import { CONFIG_SUCCESS } from 'Actions/config';
import { FILES, FOLDER } from 'Constants/collectionTypes';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from 'Constants/fieldInference';
import { formatToExtension } from 'Formats/formats';

const collections = (state = null, action) => {
  switch (action.type) {
    case CONFIG_SUCCESS: {
      const configCollections = action.payload ? action.payload.get('collections') : List();
      return configCollections
        .toOrderedMap()
        .map(collection => {
          if (collection.has('folder')) {
            return collection.set('type', FOLDER);
          }
          if (collection.has('files')) {
            return collection.set('type', FILES);
          }
        })
        .mapKeys((key, collection) => collection.get('name'));
    }
    default:
      return state;
  }
};

const selectors = {
  [FOLDER]: {
    entryExtension(collection) {
      return (collection.get('extension') || formatToExtension(collection.get('format') || 'frontmatter')).replace(/^\./, '');
    },
    fields(collection) {
      return collection.get('fields');
    },
    entryPath(collection, slug) {
      return `${ collection.get('folder').replace(/\/$/, '') }/${ slug }.${ this.entryExtension(collection) }`;
    },
    entrySlug(collection, path) {
      return path.split('/').pop().replace(new RegExp(`\.${ escapeRegExp(this.entryExtension(collection)) }$`), '');
    },
    listMethod() {
      return 'entriesByFolder';
    },
    allowNewEntries(collection) {
      return collection.get('create');
    },
    allowDeletion(collection) {
      return collection.get('delete', true);
    },
    templateName(collection) {
      return collection.get('name');
    },
  },
  [FILES]: {
    fileForEntry(collection, slug) {
      const files = collection.get('files');
      return files.filter(f => f.get('name') === slug).get(0);
    },
    fields(collection, slug) {
      const file = this.fileForEntry(collection, slug);
      return file && file.get('fields');
    },
    entryPath(collection, slug) {
      const file = this.fileForEntry(collection, slug);
      return file && file.get('file');
    },
    entrySlug(collection, path) {
      const file = collection.get('files').filter(f => f.get('file') === path).get(0);
      return file && file.get('name');
    },
    listMethod() {
      return 'entriesByFiles';
    },
    allowNewEntries() {
      return false;
    },
    allowDeletion(collection) {
      return collection.get('delete', true);
    },
    templateName(collection, slug) {
      return slug;
    },
  },
};

export const selectFields = (collection, slug) => selectors[collection.get('type')].fields(collection, slug);
export const selectFolderEntryExtension = (collection) => selectors[FOLDER].entryExtension(collection);
export const selectEntryPath = (collection, slug) => selectors[collection.get('type')].entryPath(collection, slug);
export const selectEntrySlug = (collection, path) => selectors[collection.get('type')].entrySlug(collection, path);
export const selectListMethod = collection => selectors[collection.get('type')].listMethod();
export const selectAllowNewEntries = collection => selectors[collection.get('type')].allowNewEntries(collection);
export const selectAllowDeletion = collection => selectors[collection.get('type')].allowDeletion(collection);
export const selectTemplateName = (collection, slug) => selectors[collection.get('type')].templateName(collection, slug);
export const selectIdentifier = collection => {
  const fieldNames = collection.get('fields').map(field => field.get('name'));
  return IDENTIFIER_FIELDS.find(id => fieldNames.find(name => name.toLowerCase().trim() === id));
};
export const selectInferedField = (collection, fieldName) => {
  const inferableField = INFERABLE_FIELDS[fieldName];
  const fields = collection.get('fields');
  let field;

  // If colllection has no fields or fieldName is not defined within inferables list, return null
  if (!fields || !inferableField) return null;
  // Try to return a field of the specified type with one of the synonyms
  const mainTypeFields = fields.filter(f => f.get('widget', 'string') === inferableField.type).map(f => f.get('name'));
  field = mainTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
  if (field && field.size > 0) return field.first();

  // Try to return a field for each of the specified secondary types
  const secondaryTypeFields = fields.filter(f => inferableField.secondaryTypes.indexOf(f.get('widget', 'string')) !== -1).map(f => f.get('name'));
  field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
  if (field && field.size > 0) return field.first();

  // Try to return the first field of the specified type
  if (inferableField.fallbackToFirstField && mainTypeFields.size > 0) return mainTypeFields.first();

  // Coundn't infer the field. Show error and return null.
  if (inferableField.showError) {
    consoleError(
      `The Field ${ fieldName } is missing for the collection “${ collection.get('name') }”`,
      `Netlify CMS tries to infer the entry ${ fieldName } automatically, but one couldn\'t be found for entries of the collection “${ collection.get('name') }”. Please check your site configuration.`
    );
  }

  return null;
};

export default collections;
<commit_msg>fix: only trim periods from file extensions (#1554)<commit_after>import { List } from 'immutable';
import { escapeRegExp } from 'lodash';
import consoleError from 'Lib/consoleError';
import { CONFIG_SUCCESS } from 'Actions/config';
import { FILES, FOLDER } from 'Constants/collectionTypes';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from 'Constants/fieldInference';
import { formatToExtension } from 'Formats/formats';

const collections = (state = null, action) => {
  switch (action.type) {
    case CONFIG_SUCCESS: {
      const configCollections = action.payload ? action.payload.get('collections') : List();
      return configCollections
        .toOrderedMap()
        .map(collection => {
          if (collection.has('folder')) {
            return collection.set('type', FOLDER);
          }
          if (collection.has('files')) {
            return collection.set('type', FILES);
          }
        })
        .mapKeys((key, collection) => collection.get('name'));
    }
    default:
      return state;
  }
};

const selectors = {
  [FOLDER]: {
    entryExtension(collection) {
      return (collection.get('extension') || formatToExtension(collection.get('format') || 'frontmatter')).replace(/^\./, '');
    },
    fields(collection) {
      return collection.get('fields');
    },
    entryPath(collection, slug) {
      return `${ collection.get('folder').replace(/\/$/, '') }/${ slug }.${ this.entryExtension(collection) }`;
    },
    entrySlug(collection, path) {
      return path.split('/').pop().replace(new RegExp(`\\.${ escapeRegExp(this.entryExtension(collection)) }$`), '');
    },
    listMethod() {
      return 'entriesByFolder';
    },
    allowNewEntries(collection) {
      return collection.get('create');
    },
    allowDeletion(collection) {
      return collection.get('delete', true);
    },
    templateName(collection) {
      return collection.get('name');
    },
  },
  [FILES]: {
    fileForEntry(collection, slug) {
      const files = collection.get('files');
      return files.filter(f => f.get('name') === slug).get(0);
    },
    fields(collection, slug) {
      const file = this.fileForEntry(collection, slug);
      return file && file.get('fields');
    },
    entryPath(collection, slug) {
      const file = this.fileForEntry(collection, slug);
      return file && file.get('file');
    },
    entrySlug(collection, path) {
      const file = collection.get('files').filter(f => f.get('file') === path).get(0);
      return file && file.get('name');
    },
    listMethod() {
      return 'entriesByFiles';
    },
    allowNewEntries() {
      return false;
    },
    allowDeletion(collection) {
      return collection.get('delete', true);
    },
    templateName(collection, slug) {
      return slug;
    },
  },
};

export const selectFields = (collection, slug) => selectors[collection.get('type')].fields(collection, slug);
export const selectFolderEntryExtension = (collection) => selectors[FOLDER].entryExtension(collection);
export const selectEntryPath = (collection, slug) => selectors[collection.get('type')].entryPath(collection, slug);
export const selectEntrySlug = (collection, path) => selectors[collection.get('type')].entrySlug(collection, path);
export const selectListMethod = collection => selectors[collection.get('type')].listMethod();
export const selectAllowNewEntries = collection => selectors[collection.get('type')].allowNewEntries(collection);
export const selectAllowDeletion = collection => selectors[collection.get('type')].allowDeletion(collection);
export const selectTemplateName = (collection, slug) => selectors[collection.get('type')].templateName(collection, slug);
export const selectIdentifier = collection => {
  const fieldNames = collection.get('fields').map(field => field.get('name'));
  return IDENTIFIER_FIELDS.find(id => fieldNames.find(name => name.toLowerCase().trim() === id));
};
export const selectInferedField = (collection, fieldName) => {
  const inferableField = INFERABLE_FIELDS[fieldName];
  const fields = collection.get('fields');
  let field;

  // If colllection has no fields or fieldName is not defined within inferables list, return null
  if (!fields || !inferableField) return null;
  // Try to return a field of the specified type with one of the synonyms
  const mainTypeFields = fields.filter(f => f.get('widget', 'string') === inferableField.type).map(f => f.get('name'));
  field = mainTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
  if (field && field.size > 0) return field.first();

  // Try to return a field for each of the specified secondary types
  const secondaryTypeFields = fields.filter(f => inferableField.secondaryTypes.indexOf(f.get('widget', 'string')) !== -1).map(f => f.get('name'));
  field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
  if (field && field.size > 0) return field.first();

  // Try to return the first field of the specified type
  if (inferableField.fallbackToFirstField && mainTypeFields.size > 0) return mainTypeFields.first();

  // Coundn't infer the field. Show error and return null.
  if (inferableField.showError) {
    consoleError(
      `The Field ${ fieldName } is missing for the collection “${ collection.get('name') }”`,
      `Netlify CMS tries to infer the entry ${ fieldName } automatically, but one couldn\'t be found for entries of the collection “${ collection.get('name') }”. Please check your site configuration.`
    );
  }

  return null;
};

export default collections;
<|endoftext|>


<commit_before>var models = require("../../models")
var getRedisConnection = require("../../utils/getRedisConnection")
module.exports = function(token,text){
    // validate
    if(!text) return Promise.reject("text-is-require")
    var post = new models.posts();
    post.app = token.app;
    post.user = token.user;
    post.user.postsCount++;
    post.text = text;
    return Promise.all([
        post.save(),
        post.user.save(),
        models.follows.find({toUser:token.user.id})
    ]).then(function(_){
        var post = _[0]
        var redis = getRedisConnection();
        redis.publish("kyoppie:posts-timeline:"+token.user.id,post.id)
        _[2].forEach(function(following){
            redis.publish("kyoppie:posts-timeline:"+following.fromUser,post.id)
        })
        redis.quit();
        return Promise.resolve(post)
    });
}<commit_msg>implement #46<commit_after>var models = require("../../models")
var getRedisConnection = require("../../utils/getRedisConnection")
module.exports = function(token,text){
    // validate
    if(!text) return Promise.reject("text-is-require")
    var post = new models.posts();
    post.app = token.app;
    post.user = token.user;
    post.user.postsCount++;
    post.text = text.replace(/\n+/g,"\n");
    return Promise.all([
        post.save(),
        post.user.save(),
        models.follows.find({toUser:token.user.id})
    ]).then(function(_){
        var post = _[0]
        var redis = getRedisConnection();
        redis.publish("kyoppie:posts-timeline:"+token.user.id,post.id)
        _[2].forEach(function(following){
            redis.publish("kyoppie:posts-timeline:"+following.fromUser,post.id)
        })
        redis.quit();
        return Promise.resolve(post)
    });
}<|endoftext|>


<commit_before>'use strict';

let trello,
    List,
    Member,
    Label;

class Board {
    constructor(board) {
        this._board = board;
        this.name = board.name;
        this.id = board.id;
        this.closed = board.closed;
        this._lists = undefined;
        this._members = undefined;
        this._labels = undefined;
    }

    get raw() {
        return this._board;
    }

    *getMembers() {
        if (Array.isArray(this._members)) return this._members;

        if (Array.isArray(this._board.members)) {
            this._members = this._board.members.map(m => new Member(m));
        } else {
            let members = yield trello.get(`${this.id}/members`);
            this._members = members.map(m => new Member(m));
        }

        return this._members;
    }

    *getLabels() {
        if (Array.isArray(this._labels)) return this._labels;

        if (Array.isArray(this._board.labels)) {
            this._labels = this._board.labels.map(l => new Label(l));
        } else {
            let labels = yield trello.get(`${this.id}/labels`);
            this._labels = labels.map(l => new Label(l));
        }

        return this._labels;
    }

    *getLists(recursive) {
        if (!Array.isArray(this._lists)) {
            this._lists = yield* List.getAll(this.id, recursive);
        }

        return this._lists;
    }

    *getOrAddList(name) {
        yield* this.getLists();
        let list = yield* List.getOrAdd(this.id, name),
            found = this._lists.find(l => list.id === l.id);

        if (found) {
            return found;
        } else {
            this._lists.push(list);
            return list;
        }
    }

    *iterateAllCards(callbackFn, context) {
        for (let l of yield* this.getLists()) {
            for (let c of yield* this.getCards()) {
                callbackFn.call(context, c);
            }
        }
    }

    static *getOrAdd(name, recursive) {
        let board = yield* Board.get(name, recursive);

        if (!board) {
            board = yield* Board.add(name);
        }

        return board;
    }

    static *get(name, recursive) {
        let boards = yield* Board.getAll(),
            board = boards.find(board => board.name === name && !board.closed);

        if (!(recursive && board)) return board;

        yield* board.getLists(recursive);
        yield* board.getMembers();
        yield* board.getLabels();

        return board;
    }

    static *add(name) {
        let board = yield trello.post('', {
            name
        });
        return new Board(board);
    }

    static *getAll() {
        let boards = yield trello.request('get', 'members/me/boards');

        return boards
            .filter(board => !board.closed)
            .map(board => new Board(board));
    }

    static *getBulk(name) {
        let board = yield* Board.getOrAdd(name),
            bulkData = yield trello.get(board.id, {
                lists: 'open',
                cards: 'open',
                card_checklists: 'all',
                members: 'all',
                labels: 'all'
            });

        // because the bulk board get only returns memberIds for cards, not full member objects
        bulkData.cards.forEach(c => {
            c.members = new Array();
            c.idMembers.forEach(memberId => {
                let member = bulkData.members.find(m => m.id === memberId);
                if (member) {
                    c.members.push(member);
                }
            });
        });

        board._members = Member.getBulk({
            members: bulkData.members
        });

        board._lables = Label.getBulk({
            labels: bulkData.labels
        });

        board._lists = List.getBulk(bulkData);

        return board;
    }
}

module.exports = function (TrelloAPI, list, member, label) {
    trello = new TrelloAPI(Board);
    List = list;
    Member = member;
    Label = label;
    return Board;
};<commit_msg>Fix typo in iterateAllCards<commit_after>'use strict';

let trello,
    List,
    Member,
    Label;

class Board {
    constructor(board) {
        this._board = board;
        this.name = board.name;
        this.id = board.id;
        this.closed = board.closed;
        this._lists = undefined;
        this._members = undefined;
        this._labels = undefined;
    }

    get raw() {
        return this._board;
    }

    *getMembers() {
        if (Array.isArray(this._members)) return this._members;

        if (Array.isArray(this._board.members)) {
            this._members = this._board.members.map(m => new Member(m));
        } else {
            let members = yield trello.get(`${this.id}/members`);
            this._members = members.map(m => new Member(m));
        }

        return this._members;
    }

    *getLabels() {
        if (Array.isArray(this._labels)) return this._labels;

        if (Array.isArray(this._board.labels)) {
            this._labels = this._board.labels.map(l => new Label(l));
        } else {
            let labels = yield trello.get(`${this.id}/labels`);
            this._labels = labels.map(l => new Label(l));
        }

        return this._labels;
    }

    *getLists(recursive) {
        if (!Array.isArray(this._lists)) {
            this._lists = yield* List.getAll(this.id, recursive);
        }

        return this._lists;
    }

    *getOrAddList(name) {
        yield* this.getLists();
        let list = yield* List.getOrAdd(this.id, name),
            found = this._lists.find(l => list.id === l.id);

        if (found) {
            return found;
        } else {
            this._lists.push(list);
            return list;
        }
    }

    *iterateAllCards(callbackFn, context) {
        for (let l of yield* this.getLists()) {
            for (let c of yield* l.getCards()) {
                callbackFn.call(context, c);
            }
        }
    }

    static *getOrAdd(name, recursive) {
        let board = yield* Board.get(name, recursive);

        if (!board) {
            board = yield* Board.add(name);
        }

        return board;
    }

    static *get(name, recursive) {
        let boards = yield* Board.getAll(),
            board = boards.find(board => board.name === name && !board.closed);

        if (!(recursive && board)) return board;

        yield* board.getLists(recursive);
        yield* board.getMembers();
        yield* board.getLabels();

        return board;
    }

    static *add(name) {
        let board = yield trello.post('', {
            name
        });
        return new Board(board);
    }

    static *getAll() {
        let boards = yield trello.request('get', 'members/me/boards');

        return boards
            .filter(board => !board.closed)
            .map(board => new Board(board));
    }

    static *getBulk(name) {
        let board = yield* Board.getOrAdd(name),
            bulkData = yield trello.get(board.id, {
                lists: 'open',
                cards: 'open',
                card_checklists: 'all',
                members: 'all',
                labels: 'all'
            });

        // because the bulk board get only returns memberIds for cards, not full member objects
        bulkData.cards.forEach(c => {
            c.members = new Array();
            c.idMembers.forEach(memberId => {
                let member = bulkData.members.find(m => m.id === memberId);
                if (member) {
                    c.members.push(member);
                }
            });
        });

        board._members = Member.getBulk({
            members: bulkData.members
        });

        board._lables = Label.getBulk({
            labels: bulkData.labels
        });

        board._lists = List.getBulk(bulkData);

        return board;
    }
}

module.exports = function (TrelloAPI, list, member, label) {
    trello = new TrelloAPI(Board);
    List = list;
    Member = member;
    Label = label;
    return Board;
};
<|endoftext|>


<commit_before>'use strict';

/**
 * Module dependencies
 */
import * as mongo from 'mongodb';
import Post from '../../models/post';
import User from '../../models/user';
import serialize from '../../serializers/post';
//import savePostMentions from '../../core/save-post-mentions';
//import extractHashtags from '../../core/extract-hashtags';
//import registerHashtags from '../../core/register-hashtags';
import getDriveFile from '../../common/get-drive-file';
import event from '../../event';
import es from '../../db/elasticsearch';

/**
 * 最大文字数
 */
const maxTextLength = 300;

/**
 * 添付できるファイルの数
 */
const maxFileLength = 4;

/**
 * Create a post
 *
 * @param {Object} params
 * @param {Object} reply
 * @param {Object} app
 * @param {Object} user
 * @return {void}
 */
module.exports = async (params, reply, app, user) =>
{
	// Init 'text' parameter
	let text = params.text;
	if (text !== undefined && text !== null) {
		text = text.trim();
		if (text.length === 0) {
			text = null;
		} else if (text.length > maxTextLength) {
			return reply(400, 'too long text');
		}
	} else {
		text = null;
	}

	// Init 'reply_to' parameter
	let replyTo = params.reply_to;
	if (replyTo !== undefined && replyTo !== null) {
		const replyToPost = await Post.findOne({_id: new mongo.ObjectID(replyTo)});

		if (replyToPost === null) {
			return reply(404, 'reply to post is not found');
		} else if (replyToPost.hasOwnProperty('repost')) {
			return reply(400, 'cannot reply to repost');
		}
	} else {
		replyTo = null;
	}

	// Init 'files' parameter
	let files = params.files;
	if (files !== undefined && files !== null) {
		files = files.split(',');

		if (files.length === 0) {
			files = null;
		} else if (files.length > maxFileLength) {
			return reply(400, 'too many files');
		}

		if (files !== null) {
			// 重複チェック
			files = files.filter((x, i, self) => self.indexOf(x) === i);
		}
	} else {
		files = null;
	}

	// テキストが無いかつ添付ファイルも無かったらエラー
	if (text === null && files === null) {
		return reply(400, 'text or files is required');
	}

	// 添付ファイルがあれば添付ファイル取得
	if (files !== null) {
		files = await Promise.all(files.map(file => getDriveFile(user._id, file)));
	}

	// 投稿を作成
	const res = await Post.insert({
		created_at: Date.now(),
		files: files ? files.map(file => file.id) : null,
		reaction_counts: [],
		replies_count: 0,
		reply_to: replyTo,
		text: text,
		user: user._id
	});

	const post = res.ops[0];

	user.posts_count++;
	post.user = user;

	const postObj = await serialize(post);

	reply(postObj);

	// Publish to stream
	event.publishPost(user._id, postObj);

	// ハッシュタグ抽出
	//const hashtags = extractHashtags(text);

	// ハッシュタグをデータベースに登録
	//registerHashtags(user, hashtags);

	// メンションを抽出してデータベースに登録
	//savePostMentions(user, post, post.text);

	// ユーザー情報更新
	User.updateOne({_id: user._id}, {
		$set: user
	});

/*
	// Register to search database
	es.index({
		index: 'posts',
		type: 'post',
		id: post._id.toHexString(),
		body: {
			text: post.text
		}
	}, (error, response) => {
		if (error) {
			console.error(error);
		} else {
			console.log(response);
		}
	});*/
};
<commit_msg>Implement repost<commit_after>'use strict';

/**
 * Module dependencies
 */
import * as mongo from 'mongodb';
import Post from '../../models/post';
import User from '../../models/user';
import serialize from '../../serializers/post';
//import savePostMentions from '../../core/save-post-mentions';
//import extractHashtags from '../../core/extract-hashtags';
//import registerHashtags from '../../core/register-hashtags';
import getDriveFile from '../../common/get-drive-file';
import event from '../../event';
import es from '../../db/elasticsearch';

/**
 * 最大文字数
 */
const maxTextLength = 300;

/**
 * 添付できるファイルの数
 */
const maxFileLength = 4;

/**
 * Create a post
 *
 * @param {Object} params
 * @param {Object} reply
 * @param {Object} app
 * @param {Object} user
 * @return {void}
 */
module.exports = async (params, reply, app, user) =>
{
	// Init 'repost' parameter
	const repost = params.repost;
	if (repost !== undefined && repost !== null) {
		const repostee = await Post.findOne({_id: new mongo.ObjectID(repost)});

		if (repostee === null) {
			return reply(404, 'repostee is not found');
		} else if (repostee.hasOwnProperty('repost')) {
			return reply(400, 'cannot repost from repost');
		}

		// Repostを作成
		const res = await Post.insert({
			created_at: Date.now(),
			repost: repostee._id,
			user: user._id
		});

		const post = res.ops[0];

		return created(post);
	}

	// Init 'text' parameter
	let text = params.text;
	if (text !== undefined && text !== null) {
		text = text.trim();
		if (text.length === 0) {
			text = null;
		} else if (text.length > maxTextLength) {
			return reply(400, 'too long text');
		}
	} else {
		text = null;
	}

	// Init 'reply_to' parameter
	let replyTo = params.reply_to;
	let replyToEntity = null;
	if (replyTo !== undefined && replyTo !== null) {
		replyToEntity = await Post.findOne({_id: new mongo.ObjectID(replyTo)});

		if (replyToEntity === null) {
			return reply(404, 'reply to post is not found');
		} else if (replyToEntity.hasOwnProperty('repost')) {
			return reply(400, 'cannot reply to repost');
		}
	} else {
		replyTo = null;
	}

	// Init 'files' parameter
	let files = params.files;
	if (files !== undefined && files !== null) {
		files = files.split(',');

		if (files.length === 0) {
			files = null;
		} else if (files.length > maxFileLength) {
			return reply(400, 'too many files');
		}

		if (files !== null) {
			// 重複チェック
			files = files.filter((x, i, self) => self.indexOf(x) === i);
		}
	} else {
		files = null;
	}

	// テキストが無いかつ添付ファイルも無かったらエラー
	if (text === null && files === null) {
		return reply(400, 'text or files is required');
	}

	// 添付ファイルがあれば添付ファイル取得
	if (files !== null) {
		files = await Promise.all(files.map(file => getDriveFile(user._id, file)));
	}

	// 投稿を作成
	const res = await Post.insert({
		created_at: Date.now(),
		files: files ? files.map(file => file.id) : null,
		reaction_counts: [],
		replies_count: 0,
		reply_to: replyToEntity._id || null,
		text: text,
		user: user._id
	});

	const post = res.ops[0];

	created(post);

	async function created(post) {
		user.posts_count++;
		post.user = user;

		const postObj = await serialize(post);

		reply(postObj);

		// Publish to stream
		event.publishPost(user._id, postObj);

		// ハッシュタグ抽出
		//const hashtags = extractHashtags(text);

		// ハッシュタグをデータベースに登録
		//registerHashtags(user, hashtags);

		// メンションを抽出してデータベースに登録
		//savePostMentions(user, post, post.text);

		// ユーザー情報更新
		User.updateOne({_id: user._id}, {
			$set: user
		});

	/*
		// Register to search database
		es.index({
			index: 'posts',
			type: 'post',
			id: post._id.toHexString(),
			body: {
				text: post.text
			}
		}, (error, response) => {
			if (error) {
				console.error(error);
			} else {
				console.log(response);
			}
		});*/
	}
};
<|endoftext|>


<commit_before>'use strict';

/**
 * Module dependencies
 */
import * as mongo from 'mongodb';
import User from '../../models/user';
import serialize from '../../serializers/user';
import es from '../../db/elasticsearch';

const size = 10;

/**
 * Search a post
 *
 * @param {Object} params
 * @param {Object} reply
 * @param {Object} user
 * @return {void}
 */
module.exports = async (params, reply, user) =>
{
	// Init 'query' parameter
	let query = params.query;
	if (query === undefined || query === null || query.trim() === '') {
		return reply(400, 'query is required');
	}

	// Init 'page' parameter
	let page = params.page;
	if (page === undefined) {
		page = 1;
	} else if (page === 0) {
		page = 1;
	}

	const from = (page - 1) * size;

	es.search({
		index: 'users',
		body: {
			fields: [],
			size: size,
			from: from,
			query: {
				simple_query_string: {
					fields: ['username', 'name', 'bio'],
					query: query,
					default_operator: 'and'
				}
			}
		}
	}, async (error, response) => {
		if (error) {
			return reply(500);
		}

		if (response.hits.total === 0) {
			return reply([]);
		}

		const users = await User
			.find({
				_id: {
					$in: response.hits.hits.map(hit => hit._id)
				}
			})
			.toArray();

		// serialize
		reply(await Promise.all(users.map(async user => await serialize(user))));
	});
};
<commit_msg>Fix bug<commit_after>'use strict';

/**
 * Module dependencies
 */
import * as mongo from 'mongodb';
import User from '../../models/user';
import serialize from '../../serializers/user';
import es from '../../db/elasticsearch';

const size = 10;

/**
 * Search a post
 *
 * @param {Object} params
 * @param {Object} reply
 * @param {Object} user
 * @return {void}
 */
module.exports = async (params, reply, user) =>
{
	// Init 'query' parameter
	let query = params.query;
	if (query === undefined || query === null || query.trim() === '') {
		return reply(400, 'query is required');
	}

	// Init 'page' parameter
	let page = params.page;
	if (page === undefined) {
		page = 1;
	} else if (page === 0) {
		page = 1;
	}

	const from = (page - 1) * size;

	es.search({
		index: 'misskey',
		type: 'user',
		body: {
			size: size,
			from: from,
			query: {
				simple_query_string: {
					fields: ['username', 'name', 'bio'],
					query: query,
					default_operator: 'and'
				}
			}
		}
	}, async (error, response) => {
		if (error) {
			return reply(500);
		}

		if (response.hits.total === 0) {
			return reply([]);
		}

		const users = await User
			.find({
				_id: {
					$in: response.hits.hits.map(hit => new mongo.ObjectID(hit._id))
				}
			})
			.toArray();

		// serialize
		reply(await Promise.all(users.map(async user => await serialize(user))));
	});
};
<|endoftext|>


<commit_before>var async = require('async'),
    format = require('pg-format'),
    util = require('util'),
    _ = require('lodash');

const KEY_PRIMARY = 'PRIMARY KEY';
const KEY_FOREIGN = 'FOREIGN KEY';

class Table {

    /**
     *
     * @param {DB} db
     * @param {String} table_name
     * @param {String} [alias]
     */
    constructor(db, table_name, alias) {
        this.name = table_name;
        this.alias = alias || '';
        this.aliasClause = alias ? `${alias}.` : '';
        this.columns = {};
        this.persistentUpdatesSuspended = 0;
        this.db = db;
    }

    init(callback) {
        var self = this;
        async.waterfall([
            /**
             * Request columns info
             * @param next
             */
            function(next) {
                self.db.query(`
                                SELECT column_name
                                FROM information_schema.columns
                                WHERE table_name = %L;`, self.name, next);
            },

            /**
             * Request constraints info
             * @param columns
             * @param next
             */
            function(columns, next) {
                self.columns = {};
                _.each(columns, function(column) {
                    self.columns[column.column_name] = {name: column.column_name}
                });
                self.db.query(`
                                SELECT
                                    kcu.column_name, constraint_type
                                FROM
                                    information_schema.table_constraints AS tc
                                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
                                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
                                WHERE constraint_type IN (%L) AND tc.table_name=%L;`, [[KEY_PRIMARY, KEY_FOREIGN], self.name], next);
            },

            /**
             * Fill constraints
             * @param constraints
             * @param next
             */
            function(constraints, next) {
                var constraint_found = false;
                self.primary = [];
                self.defaultIds = [];
                _.each(constraints, function(constraint) {
                    if (constraint.constraint_type == KEY_PRIMARY) {
                        self.columns[constraint.column_name].is_primary = true;
                        constraint_found = true;
                        self.primary.push(constraint.column_name);
                        self.defaultIds.push(util.format('%s = %L', constraint.column_name));
                    }
                    if (constraint.constraint_type == KEY_FOREIGN) {
                        self.columns[constraint.column_name].is_foreign = true;
                        constraint_found = true;
                    }
                });
                if (!constraint_found) console.log(`[WARNING] No constraints on table ${self.name}`);
                self.defaultIds = self.defaultIds.length ? ` AND ${self.defaultIds.join(' AND ')}` : '';
                next();
            },

            /**
             * Initialise filters and queries
             * @param next
             */
            function(next) {
                //Set default filters for all table columns
                var defaultFilters = {};
                _.each(self.columns, function(props, column){
                    var columnDefinition = `${self.aliasClause}${column}`;
                    defaultFilters[column] = {where: ` AND ${columnDefinition} = %L`};
                    defaultFilters[`${column}s`] = {where: ` AND ${columnDefinition} IN (%L)`};
                    defaultFilters[`not_${column}`] = {where: ` AND ${columnDefinition} <> %L`};
                    defaultFilters[`not_${column}s`] = {where: ` AND ${columnDefinition} NOT IN (%L)`};
                    defaultFilters[`null_${column}`] = {where: ` AND IS NULL ${columnDefinition}`};
                    defaultFilters[`not_null_${column}`] = {where: ` AND IS NOT NULL ${columnDefinition}`};
                });

                //Use class filters (if any) with default filters fallback
                self.filters = _.defaults(self.filters || {}, defaultFilters);

                var aliasDefinition = self.alias ? ` AS ${self.alias}` : '';

                //If the queries are already defined in model class, we use them instead of default queries
                self.queries = _.defaults(self.queries || {}, {
                    row: `SELECT *{{select}} FROM "${self.name}"${aliasDefinition}{{join}} WHERE true{{whereClause}} {{where}} {{group}} {{having}} {{order}} LIMIT 1`,
                    list: `SELECT *{{select}} FROM "${self.name}"${aliasDefinition}{{join}} WHERE true{{where}} {{group}} {{having}} {{order}} {{limit}}`,
                    insert: `INSERT INTO "${self.name}"${aliasDefinition} ({{columns}}) VALUES ({{values}}){{returning}}`,
                    update: `UPDATE "${self.name}"${aliasDefinition} SET {{columns}} WHERE true ${self.defaultIds}`,
                    del: `DELETE FROM "${self.name}"${aliasDefinition} WHERE true ${self.defaultIds}`,
                    delWhere: `DELETE FROM "${self.name}"${aliasDefinition} WHERE true {{where}}`,
                    count: `SELECT COUNT(*) as cnt FROM "${self.name}" WHERE true {{where}}`
                });

                self.persistentAssoc = self.persistentAssoc || {};

                next();
            }
        ], callback);

    };

    /**
     * Prevent persistent fields updates from being fired
     */
    suspendPersistentUpdates() {
        this.persistentUpdatesSuspended++;
        console.log("Persistent lock for table '" + this.name + "' is set to " + this.persistentUpdatesSuspended);
    };

    /**
     * Make persistent updates possible again
     * @param callback
     * @param preventUpdating
     */
    resumePersistentUpdates(callback, preventUpdating) {
        var self = this;
        self.persistentUpdatesSuspended = Math.max(self.persistentUpdatesSuspended - 1, 0);
        console.log("Persistent lock for table '" + self.name + "' is set to " + self.persistentUpdatesSuspended);
        if (self.persistentUpdatesSuspended == 0 && !preventUpdating) {
            self.updatePersistent(function(err){
                if (err) {
                    console.log('ERROR UPDATING PERSISTENT ' + self.name, err);
                }
                callback(err);
            });
        } else {
            if (_.isFunction(callback)) {
                callback();
            }
        }
    };

    /**
     * Call persistent data update functions
     * @param callback
     */
    updatePersistent(callback){
        var self = this;
        if ((!_.isEmpty(self.persistent) || !_.isEmpty(self.persistentAssoc)) && self.persistentUpdatesSuspended == 0) {
            self.suspendPersistentUpdates();
            async.waterfall([
                function(callback) {
                    if (self.persistent) {
                        async.forEachOf(self.persistent, function (persistent, key, callback) {
                            if (_.isFunction(persistent)) {
                                persistent(function (err, data) {
                                    self.p[key] = data || false;
                                    callback(err);
                                });
                            } else {
                                console.log('Attempted to build persistent', key, 'with', persistent);
                                callback({error: 'Persistent update function is not a function'});
                            }
                        }, callback)
                    } else callback();
                },
                function(callback) {
                    if (self.persistentAssoc) {
                        async.forEachOf(self.persistentAssoc, function (id_field, key, callback) {
                            self.list(function (err, list_elements) {
                                if (err) callback(err);
                                else {
                                    var assoc = {};
                                    _.each(list_elements, function (list_element) {
                                        assoc[list_element[id_field]] = list_element;
                                    });
                                    self.p[key] = assoc;
                                    self[key] = (function (key) {
                                        return function (id) {
                                            return self.p[key][id]
                                        };
                                    })(key);
                                    callback();
                                }
                            });
                        }, callback);
                    } else callback();
                }
            ], function(err){
                self.resumePersistentUpdates(null, true);
                callback(err);
            });

        } else callback();
    };

    /**
     * Inject sorting into the request
     * @param sql
     * @param filters
     * @returns {*}
     */
    static injectSort(sql, filters){
        if (filters['order']) {
            var sort = filters['order'].split(',');
            if (_.isArray(sort)){
                var direction = 'ASC';
                if (sort.indexOf('desc')>-1){
                    direction = 'DESC';
                    delete sort[sort.indexOf('desc')];
                }
                if (sort.length) {
                    var fields = sort.join(", ");
                    sql = sql.replace('{{order}}', util.format(" ORDER BY %s %s", fields, direction));
                }
            } else throw "Failed to parse order filter"
        }
        return sql;
    };

    /**
     * Inject limit into the request
     * @param sql
     * @param filters
     */
    static injectLimit(sql, filters){
        if (filters['limit']) {
            var limit = filters['limit'].split(',');
            if (_.isArray(limit)){
                for (var i=0; i<limit.length; i++){
                    limit[i] = parseInt(limit[i]);
                }
                sql = sql.replace('{{limit}}', util.format(" LIMIT %s", limit.join(', ')))
            } else throw "Failed to parse limit filter"
        }
        return sql;
    };

    /**
     * Inject RETURNING statement into request if it's required
     * @param {String} sql
     * @returns {string}
     */
    injectReturning(sql) {
        var returning = '';
        if (this.primary && this.primary.length) {
            returning = ' RETURNING ' + this.primary.join(', ');
        }
        return sql.replace('{{returning}}', returning);
    }

    /**
     * Select one row as an object
     * @param {int|String|Object} ids
     * @param {Object} [filters]
     * @param {Function} callback
     */
    row(ids, filters, callback){
        var self = this;
        if (_.isFunction(filters)) {
            callback = filters;
            filters = {};
        }
        if (!_.isFunction(callback)) throw new Error("Callback must be a function");

        var whereClause = "";
        if (!_.isObject(ids)) {
            whereClause = ` AND ${self.primary[0]} = %L`;
        } else {
            var fields = [];
            var values = _.values(ids);
            _.each(ids, function(value, key){
                fields.push(`${key} = %L`);
            });
            ids = values;
            whereClause = fields.join(" AND ");
        }

        var columns = [];
        _.each(self.columns, function(column, name){
            columns.push(`${name}`);
        });
        var sql = self.queries.row
            .replace('{{whereClause}}', whereClause);

        sql = self.db.injectFilters(sql, filters, self.filters);
        self.db.row(sql, ids, function(err, row){
            if (err) return callback(err);
            if (_.isEmpty(row)) return callback({error: `Entry not found in "${self.name}" table for identifier(s) ${ids}`, code: 404});
            callback(null, row);
        });
    };

    /**
     * Select data by query as an array of objects
     * @param {Object} filters
     * @param {Function} callback
     */
    list(filters, callback) {
        var self = this;
        if (_.isFunction(filters)) {
            callback = filters;
            filters = {};
        } else {
            if (!_.isObject(filters)) {
                console.log("WARNING. Invalid filters provided for " + self.name + ".list(). Object required");
                console.log(filters);
                filters = {};
            }
        }
        if (!_.isFunction(callback)) throw new Error("Callback must be a function");

        var columns = [];
        _.each(self.columns, function (column, name) {
            columns.push(`${self.name}.${name}`);
        });

        var sql = self.queries.list.replace('{{columns}}', columns.join(', '));
        sql = Table.injectLimit(sql, filters);
        sql = Table.injectSort(sql, filters);
        sql = self.db.injectFilters(sql, filters, self.filters);
        return self.db.query(sql, callback);
    };

    /**
     * Insert
     * @param {Object} data
     * @param {Object} [options]
     * @param {Function} callback
     */
    insert(data, options, callback) {
        var self = this;

        if (_.isFunction(options)) {
            callback = options;
            options = {};
        }

        if (!options) options = {};

        var insert_data = _.pick(data, _.keys(self.columns));

        var sql = self.queries.insert
            .replace('{{columns}}', '%I')
            .replace('{{values}}', '%L');

        sql = self.injectReturning(sql);

        var q = self.db.row(sql, [_.keys(insert_data), _.values(insert_data)], function(err, res){
            if (err) callback(err);
            else {
                callback(null, res);
                self.updatePersistent(function(err){
                    if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
                });
            }
        });
    };

    /**
     * Update
     * @param data
     * @param options
     * @param callback
     */
    update(data, options, callback) {
        var self = this;
        var valuesStr = [];

        if (_.isFunction(options)) {
            callback = options;
            options = {};
        }

        var update_data = _.pick(data, _.difference(_.keys(self.columns), self.primary));
        var where_data = _.values(_.pick(data, self.primary));

        _.each(update_data, function(value, column){
            valuesStr.push(format('%I = %L', column, value));
        });

        var sql = self.queries.update.replace('{{columns}}', valuesStr.join(', '));

        return self.db.query(sql, where_data, function(err){
            if (err) callback(err);
            else {
                callback(null, {update: 'success'});
                self.updatePersistent(function(err){
                    if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
                });
            }
        });
    };

    /**
     * Simple deletion by primary ids
     * @param ids
     * @param callback
     */
    del(ids, callback) {
        var self = this;
        var sql = self.queries.del;
        var queryIds = [];
        if (!_.isObject(ids)) {
            if (self.primary.length == 1) {
                queryIds = ids;
            } else callback({Error: "Incorrect delete id"});
        } else {
            var whereReplace = '';
            _.each(ids, function(value, column){
                whereReplace += ' AND ' + column + (_.isArray(value) ? ' IN (%L)' : ' = %L');
                queryIds.push(value);
            });
            sql = self.queries.delWhere.replace('{{where}}', whereReplace);
        }
        return self.db.query(sql, queryIds, function(err, res){
            if (err) return callback(err);

            callback(null, {delete: 'success'});
            self.updatePersistent(function(err){
                if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
            });

        });
    };

    /**
     * Return the number of records matching the request. Count all records by default
     * @param filters
     * @param callback
     */
    count(filters, callback) {
        if (_.isFunction(filters)) {
            callback = filters;
            filters = {};
        }
        var sql = self.db.injectFilters(self.queries.count, filters, self.filters);
        self.db.query(sql, function(err, count){
            if (err) callback(err);
            else callback(null, count[0]['cnt']);
        });
    };

    /**
     * Returns true if query
     *
     * @param filters
     * @param callback
     */
    exists(filters, callback) {
        this.count(filters, function(err, count){
            callback(err, count > 0);
        });
    };
}

module.exports = Table;<commit_msg>[*] fixed default 'row' method<commit_after>var async = require('async'),
    format = require('pg-format'),
    util = require('util'),
    _ = require('lodash');

const KEY_PRIMARY = 'PRIMARY KEY';
const KEY_FOREIGN = 'FOREIGN KEY';

class Table {

    /**
     *
     * @param {DB} db
     * @param {String} table_name
     * @param {String} [alias]
     */
    constructor(db, table_name, alias) {
        this.name = table_name;
        this.alias = alias || '';
        this.aliasClause = alias ? `${alias}.` : '';
        this.columns = {};
        this.persistentUpdatesSuspended = 0;
        this.db = db;
    }

    init(callback) {
        var self = this;
        async.waterfall([
            /**
             * Request columns info
             * @param next
             */
            function(next) {
                self.db.query(`
                                SELECT column_name
                                FROM information_schema.columns
                                WHERE table_name = %L;`, self.name, next);
            },

            /**
             * Request constraints info
             * @param columns
             * @param next
             */
            function(columns, next) {
                self.columns = {};
                _.each(columns, function(column) {
                    self.columns[column.column_name] = {name: column.column_name}
                });
                self.db.query(`
                                SELECT
                                    kcu.column_name, constraint_type
                                FROM
                                    information_schema.table_constraints AS tc
                                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
                                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
                                WHERE constraint_type IN (%L) AND tc.table_name=%L;`, [[KEY_PRIMARY, KEY_FOREIGN], self.name], next);
            },

            /**
             * Fill constraints
             * @param constraints
             * @param next
             */
            function(constraints, next) {
                var constraint_found = false;
                self.primary = [];
                self.defaultIds = [];
                _.each(constraints, function(constraint) {
                    if (constraint.constraint_type == KEY_PRIMARY) {
                        self.columns[constraint.column_name].is_primary = true;
                        constraint_found = true;
                        self.primary.push(constraint.column_name);
                        self.defaultIds.push(util.format('%s = %L', constraint.column_name));
                    }
                    if (constraint.constraint_type == KEY_FOREIGN) {
                        self.columns[constraint.column_name].is_foreign = true;
                        constraint_found = true;
                    }
                });
                if (!constraint_found) console.log(`[WARNING] No constraints on table ${self.name}`);
                self.defaultIds = self.defaultIds.length ? ` AND ${self.defaultIds.join(' AND ')}` : '';
                next();
            },

            /**
             * Initialise filters and queries
             * @param next
             */
            function(next) {
                //Set default filters for all table columns
                var defaultFilters = {};
                _.each(self.columns, function(props, column){
                    var columnDefinition = `${self.aliasClause}${column}`;
                    defaultFilters[column] = {where: ` AND ${columnDefinition} = %L`};
                    defaultFilters[`${column}s`] = {where: ` AND ${columnDefinition} IN (%L)`};
                    defaultFilters[`not_${column}`] = {where: ` AND ${columnDefinition} <> %L`};
                    defaultFilters[`not_${column}s`] = {where: ` AND ${columnDefinition} NOT IN (%L)`};
                    defaultFilters[`null_${column}`] = {where: ` AND IS NULL ${columnDefinition}`};
                    defaultFilters[`not_null_${column}`] = {where: ` AND IS NOT NULL ${columnDefinition}`};
                });

                //Use class filters (if any) with default filters fallback
                self.filters = _.defaults(self.filters || {}, defaultFilters);

                var aliasDefinition = self.alias ? ` AS ${self.alias}` : '';

                //If the queries are already defined in model class, we use them instead of default queries
                self.queries = _.defaults(self.queries || {}, {
                    row: `SELECT *{{select}} FROM "${self.name}"${aliasDefinition}{{join}} WHERE true{{whereClause}} {{where}} {{group}} {{having}} {{order}} LIMIT 1`,
                    list: `SELECT *{{select}} FROM "${self.name}"${aliasDefinition}{{join}} WHERE true{{where}} {{group}} {{having}} {{order}} {{limit}}`,
                    insert: `INSERT INTO "${self.name}"${aliasDefinition} ({{columns}}) VALUES ({{values}}){{returning}}`,
                    update: `UPDATE "${self.name}"${aliasDefinition} SET {{columns}} WHERE true ${self.defaultIds}`,
                    del: `DELETE FROM "${self.name}"${aliasDefinition} WHERE true ${self.defaultIds}`,
                    delWhere: `DELETE FROM "${self.name}"${aliasDefinition} WHERE true {{where}}`,
                    count: `SELECT COUNT(*) as cnt FROM "${self.name}" WHERE true {{where}}`
                });

                self.persistentAssoc = self.persistentAssoc || {};

                next();
            }
        ], callback);

    };

    /**
     * Prevent persistent fields updates from being fired
     */
    suspendPersistentUpdates() {
        this.persistentUpdatesSuspended++;
        console.log("Persistent lock for table '" + this.name + "' is set to " + this.persistentUpdatesSuspended);
    };

    /**
     * Make persistent updates possible again
     * @param callback
     * @param preventUpdating
     */
    resumePersistentUpdates(callback, preventUpdating) {
        var self = this;
        self.persistentUpdatesSuspended = Math.max(self.persistentUpdatesSuspended - 1, 0);
        console.log("Persistent lock for table '" + self.name + "' is set to " + self.persistentUpdatesSuspended);
        if (self.persistentUpdatesSuspended == 0 && !preventUpdating) {
            self.updatePersistent(function(err){
                if (err) {
                    console.log('ERROR UPDATING PERSISTENT ' + self.name, err);
                }
                callback(err);
            });
        } else {
            if (_.isFunction(callback)) {
                callback();
            }
        }
    };

    /**
     * Call persistent data update functions
     * @param callback
     */
    updatePersistent(callback){
        var self = this;
        if ((!_.isEmpty(self.persistent) || !_.isEmpty(self.persistentAssoc)) && self.persistentUpdatesSuspended == 0) {
            self.suspendPersistentUpdates();
            async.waterfall([
                function(callback) {
                    if (self.persistent) {
                        async.forEachOf(self.persistent, function (persistent, key, callback) {
                            if (_.isFunction(persistent)) {
                                persistent(function (err, data) {
                                    self.p[key] = data || false;
                                    callback(err);
                                });
                            } else {
                                console.log('Attempted to build persistent', key, 'with', persistent);
                                callback({error: 'Persistent update function is not a function'});
                            }
                        }, callback)
                    } else callback();
                },
                function(callback) {
                    if (self.persistentAssoc) {
                        async.forEachOf(self.persistentAssoc, function (id_field, key, callback) {
                            self.list(function (err, list_elements) {
                                if (err) callback(err);
                                else {
                                    var assoc = {};
                                    _.each(list_elements, function (list_element) {
                                        assoc[list_element[id_field]] = list_element;
                                    });
                                    self.p[key] = assoc;
                                    self[key] = (function (key) {
                                        return function (id) {
                                            return self.p[key][id]
                                        };
                                    })(key);
                                    callback();
                                }
                            });
                        }, callback);
                    } else callback();
                }
            ], function(err){
                self.resumePersistentUpdates(null, true);
                callback(err);
            });

        } else callback();
    };

    /**
     * Inject sorting into the request
     * @param sql
     * @param filters
     * @returns {*}
     */
    static injectSort(sql, filters){
        if (filters['order']) {
            var sort = filters['order'].split(',');
            if (_.isArray(sort)){
                var direction = 'ASC';
                if (sort.indexOf('desc')>-1){
                    direction = 'DESC';
                    delete sort[sort.indexOf('desc')];
                }
                if (sort.length) {
                    var fields = sort.join(", ");
                    sql = sql.replace('{{order}}', util.format(" ORDER BY %s %s", fields, direction));
                }
            } else throw "Failed to parse order filter"
        }
        return sql;
    };

    /**
     * Inject limit into the request
     * @param sql
     * @param filters
     */
    static injectLimit(sql, filters){
        if (filters['limit']) {
            var limit = filters['limit'].split(',');
            if (_.isArray(limit)){
                for (var i=0; i<limit.length; i++){
                    limit[i] = parseInt(limit[i]);
                }
                sql = sql.replace('{{limit}}', util.format(" LIMIT %s", limit.join(', ')))
            } else throw "Failed to parse limit filter"
        }
        return sql;
    };

    /**
     * Inject RETURNING statement into request if it's required
     * @param {String} sql
     * @returns {string}
     */
    injectReturning(sql) {
        var returning = '';
        if (this.primary && this.primary.length) {
            returning = ' RETURNING ' + this.primary.join(', ');
        }
        return sql.replace('{{returning}}', returning);
    }

    /**
     * Select one row as an object
     * @param {int|String|Object} ids
     * @param {Object} [filters]
     * @param {Function} callback
     */
    row(ids, filters, callback){
        var self = this;
        if (_.isFunction(filters)) {
            callback = filters;
            filters = {};
        }
        if (!_.isFunction(callback)) throw new Error("Callback must be a function");

        var whereClause = "";
        if (!_.isObject(ids)) {
            whereClause = ` AND ${self.primary[0]} = %L`;
        } else {
            var fields = [];
            var values = _.values(ids);
            _.each(ids, function(value, key){
                fields.push(`${key} = %L`);
            });
            ids = values;
            whereClause = fields.map(function(_item) {
                return " AND " + _item;
            }).join(" ");
        }

        var columns = [];
        _.each(self.columns, function(column, name){
            columns.push(`${name}`);
        });
        var sql = self.queries.row
            .replace('{{whereClause}}', whereClause);

        sql = self.db.injectFilters(sql, filters, self.filters);
        self.db.row(sql, ids, function(err, row){
            if (err) return callback(err);
            if (_.isEmpty(row)) return callback({error: `Entry not found in "${self.name}" table for identifier(s) ${ids}`, code: 404});
            callback(null, row);
        });
    };

    /**
     * Select data by query as an array of objects
     * @param {Object} filters
     * @param {Function} callback
     */
    list(filters, callback) {
        var self = this;
        if (_.isFunction(filters)) {
            callback = filters;
            filters = {};
        } else {
            if (!_.isObject(filters)) {
                console.log("WARNING. Invalid filters provided for " + self.name + ".list(). Object required");
                console.log(filters);
                filters = {};
            }
        }
        if (!_.isFunction(callback)) throw new Error("Callback must be a function");

        var columns = [];
        _.each(self.columns, function (column, name) {
            columns.push(`${self.name}.${name}`);
        });

        var sql = self.queries.list.replace('{{columns}}', columns.join(', '));
        sql = Table.injectLimit(sql, filters);
        sql = Table.injectSort(sql, filters);
        sql = self.db.injectFilters(sql, filters, self.filters);
        return self.db.query(sql, callback);
    };

    /**
     * Insert
     * @param {Object} data
     * @param {Object} [options]
     * @param {Function} callback
     */
    insert(data, options, callback) {
        var self = this;

        if (_.isFunction(options)) {
            callback = options;
            options = {};
        }

        if (!options) options = {};

        var insert_data = _.pick(data, _.keys(self.columns));

        var sql = self.queries.insert
            .replace('{{columns}}', '%I')
            .replace('{{values}}', '%L');

        sql = self.injectReturning(sql);

        var q = self.db.row(sql, [_.keys(insert_data), _.values(insert_data)], function(err, res){
            if (err) callback(err);
            else {
                callback(null, res);
                self.updatePersistent(function(err){
                    if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
                });
            }
        });
    };

    /**
     * Update
     * @param data
     * @param options
     * @param callback
     */
    update(data, options, callback) {
        var self = this;
        var valuesStr = [];

        if (_.isFunction(options)) {
            callback = options;
            options = {};
        }

        var update_data = _.pick(data, _.difference(_.keys(self.columns), self.primary));
        var where_data = _.values(_.pick(data, self.primary));

        _.each(update_data, function(value, column){
            valuesStr.push(format('%I = %L', column, value));
        });

        var sql = self.queries.update.replace('{{columns}}', valuesStr.join(', '));

        return self.db.query(sql, where_data, function(err){
            if (err) callback(err);
            else {
                callback(null, {update: 'success'});
                self.updatePersistent(function(err){
                    if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
                });
            }
        });
    };

    /**
     * Simple deletion by primary ids
     * @param ids
     * @param callback
     */
    del(ids, callback) {
        var self = this;
        var sql = self.queries.del;
        var queryIds = [];
        if (!_.isObject(ids)) {
            if (self.primary.length == 1) {
                queryIds = ids;
            } else callback({Error: "Incorrect delete id"});
        } else {
            var whereReplace = '';
            _.each(ids, function(value, column){
                whereReplace += ' AND ' + column + (_.isArray(value) ? ' IN (%L)' : ' = %L');
                queryIds.push(value);
            });
            sql = self.queries.delWhere.replace('{{where}}', whereReplace);
        }
        return self.db.query(sql, queryIds, function(err, res){
            if (err) return callback(err);

            callback(null, {delete: 'success'});
            self.updatePersistent(function(err){
                if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
            });

        });
    };

    /**
     * Return the number of records matching the request. Count all records by default
     * @param filters
     * @param callback
     */
    count(filters, callback) {
        if (_.isFunction(filters)) {
            callback = filters;
            filters = {};
        }
        var sql = self.db.injectFilters(self.queries.count, filters, self.filters);
        self.db.query(sql, function(err, count){
            if (err) callback(err);
            else callback(null, count[0]['cnt']);
        });
    };

    /**
     * Returns true if query
     *
     * @param filters
     * @param callback
     */
    exists(filters, callback) {
        this.count(filters, function(err, count){
            callback(err, count > 0);
        });
    };
}

module.exports = Table;<|endoftext|>