<commit_before>/**
 * Created by barakedry on 6/19/15.
 */
'use strict';
var _ = require('lodash');
var util = require('util');
var utils = require('./utils');
var DiffTracker = require('./diff-tracker');
var EventEmitter = require('events').EventEmitter;
var debug = require('debuglog')('merge-diff');

function MergeDiff(object, options) {
    this.options = _.defaults(options || {}, {
        emitEvents: true, // settings this to false should allow faster merging but it is not implemented yet
        pathEventPrefix: 'PATH:',
        undefinedKeyword: '__$$U',
        deleteKeyword: '__$$D',
        protoKeyword: '__$$P',
        mergeDeletions: true,
        mergeAdditions: true,
        emitAdditions: true,
        emitUpdates: true,
        emitDifferences: true,
        maxKeysInLevel: 1000,
        maxLevels: 50,
        maxListeners: 1000000
    });

    this.object = object || {};
    //this.prototypes = []; // prototypes are stored in a special collection

    this.setMaxListeners(this.options.maxListeners);
}

util.inherits(MergeDiff, EventEmitter);

MergeDiff.prototype._emitTopLevelEvent = function (event, eventData, options) {
    var levelDiffs = eventData.levelDiffs;

    if (options.emitEvents && levelDiffs && (levelDiffs.hasDeletions || levelDiffs.hasUpdates || levelDiffs.hasDifferences || levelDiffs.hasAdditions)) {
        this.emit(event, eventData); //specific top level event: override, merge, delete, ...
        this.emit('change', eventData); //general top level event
    }
};

MergeDiff.prototype.merge = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    var levelDiffs = this._mergeObject(this.object, utils.wrapByPath(delta, path), '', options, 0);

    this._emitTopLevelEvent('merge', {
        path: path || '*',
        object: this.object,
        delta: delta,
        levelDiffs: levelDiffs
    }, options);
};

MergeDiff.prototype.override = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    var levelDiffs = this._mergeObject(this.object, utils.wrapByPath(delta, path), '', options, 0, path);

    this._emitTopLevelEvent('override', {
        path: path || '*',
        object: this.object,
        delta: delta,
        levelDiffs: levelDiffs
    }, options);
};

MergeDiff.prototype.delete = function (path) {

    if (!(path && _.isString(path))) {
        debug('invalid path, cannot delete');
        return;
    }

    var levelDiffs = this._mergeObject(this.object, utils.wrapByPath(this.options.deleteKeyword, path), '', this.options, 0);

    this._emitTopLevelEvent('delete', {
        path: path || '*',
        object: this.object,
        levelDiffs: levelDiffs
    }, this.options);
};

MergeDiff.prototype.get = function (path) {

    if (path && (!_.isString(path))) {
        debug('invalid path, cannot get');
        return;
    }

    if (path) {
        return _.get(this.object, path);
    }

    return this.object;
};

/************************************************************************************
 * The basic merging recursion implementation:
 * ._mergeObject() -> ._mergeAtKey() -> ._mergeObject() -> ._mergeAtKey() -> ...
 *
 * ._mergeObject() iterate keys at level and calls ._mergeAtKey() for each key,
 * after iteration ends it emits and returns level changes to the caller.
 *
 * ._mergeAtKey() assigns/delete primitives and calls _.mergeObject() for objects
 ************************************************************************************/
MergeDiff.prototype._mergeObject = function (source, delta, path, options, level, override) {
    var keys,
        key,
        i,
        length,
        levelDiffs,
        isSourceArray;

    if (!(_.isObject(source) && _.isObject(delta))) {
        debug('invalid merge, source and delta must be objects');
        this.emit('error', new Error('invalid merge, source and delta must be objects'));
        return;
    }

    if (level > options.maxLevels) {
        debug('Trying to merge too deep, stopping at level %d', level);
        this.emit('error', new Error('Trying to merge too deep, stopping at level ' + level));
        return;
    }

    keys = _.keys(delta);
    length = keys.length;
    isSourceArray = _.isArray(source);

    if (options.emitEvents) {
        levelDiffs = DiffTracker.create();
        levelDiffs.path = path;
    }

    if (isSourceArray) {
        levelDiffs = levelDiffs || {};
        levelDiffs.arrayOffset = 0;
    }

    if (length > options.maxKeysInLevel) {
        debug('Stopping Merge, Too many keys in object - %d out of %d allowed keys.', length, options.maxKeysInLevel);
        this.emit('error', new Error('Stopping Merge, Too many keys in object - ' + length + ' out of ' + options.maxKeysInLevel + ' allowed keys.'));
        return levelDiffs;
    }

    // override is either undefined, a path or true
    if (!_.isUndefined(override) && (override === true || override === path)) {
        override = true;
    }

    for (i = 0; i < length; i++) {
        key = keys[i];

        if (utils.isValid(delta[key]) && delta[key] !== source[key]) {
            levelDiffs = this._mergeAtKey(source, delta, path, key, levelDiffs, options, isSourceArray, level, override);
        }

    }

    if (override && (typeof override === 'boolean')) {
        // find keys at this level that exists at the source object and delete them
        levelDiffs = this._detectDeletionsAtLevel(source, delta, levelDiffs, path, options, isSourceArray, level);
    }

    if (options.emitEvents) {
        this.emit(options.pathEventPrefix + (path || '*'), levelDiffs);
    }

    return levelDiffs;
};

MergeDiff.prototype._mergeAtKey = function (source, delta, path, key, levelDiffs, options, isSourceArray, level, override) {

    var childDiffs,
        mergingValue,
        existingValue,
        srcKey,
        mergedValue, // this value is what goes out to the tracker its not allways the same as mergingValue
        isExistingValueArray,
        isObjectMerge = false;

    mergingValue = delta[key];

    if (isSourceArray) {
        srcKey = Number(key) + levelDiffs.arrayOffset;
    } else {
        srcKey = key;
    }

    if (_.isFunction(mergingValue)) {
        mergedValue = utils.SERIALIZABLE_FUNCTION;
    } else {
        isObjectMerge = _.isObject(mergingValue);
        if (_.isUndefined(mergingValue)) {
            mergedValue = options.undefinedKeyword;
        } else if (mergingValue === options.undefinedKeyword) {
            mergedValue = mergingValue;
            mergingValue = undefined;
        } else {
            mergedValue = mergingValue;
        }
    }

    // new
    if (!source.hasOwnProperty(srcKey)) {
        if (options.mergeAdditions && delta[key] !== options.deleteKeyword) {

            // add new object
            if (isObjectMerge) {

                source[srcKey] = mergingValue.constructor.call(Object.create(Object.getPrototypeOf(mergingValue)));

                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               level + 1,
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // add new primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasAdditions = true;
                levelDiffs.additions[key] = mergedValue;
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }
        }
    // existing
    } else {

        existingValue = source[srcKey];
        isExistingValueArray = _.isArray(existingValue);

        // delete
        if (delta[key] === options.deleteKeyword) {

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs);

        // update
        } else {

            // update object
            if (isObjectMerge) {

                // we should replace the source value, todo: array merges check is not sufficient
                if (!isExistingValueArray && !utils.hasSamePrototype(existingValue, mergingValue)) {

                    // this is a restructure
                    // handle prototypes
                    source[srcKey] = Object.create(this.getPrototypeOf(mergingValue));

                }

                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               level + 1,
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // update primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasUpdates = true;
                levelDiffs.updates[key] = {'old': existingValue, 'new': mergedValue};
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }

        }

    }

    return levelDiffs;
};

MergeDiff.prototype._deleteAtKey = function (source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs) {
    if (options.mergeDeletions) {
        if (isSourceArray) {
            source.splice(srcKey, 1);
            levelDiffs.arrayOffset--;
        } else {
            delete source[srcKey];
        }

    }

    levelDiffs.deletions[key] = existingValue;
    levelDiffs.differences[key] = options.deleteKeyword;
    levelDiffs.hasDeletions = true;

    this.emitInnerDeletions(path, existingValue);

    return levelDiffs;
};

MergeDiff.prototype._detectDeletionsAtLevel = function (source, delta, levelDiffs, path, options, isSourceArray) {
    var keys = _.keys(source),
        length = keys.length,
        existingValue,
        srcKey,
        key,
        i;

    // run source object at this level
    for (i = 0; i < length; i++) {
        key = keys[i];
        if (isSourceArray) {
            srcKey = Number(key) + levelDiffs.arrayOffset;
        } else {
            srcKey = key;
        }

        if (!delta.hasOwnProperty(key)) {
            existingValue = source[key];

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue,
                isSourceArray, levelDiffs);

        }

    }
    return levelDiffs;
};

MergeDiff.prototype.getPrototypeOf = function (object) {
    return Object.getPrototypeOf(object);
};

MergeDiff.prototype.emitInnerDeletions = function (deletedObject, path) {
    var keys, key, i;

    if (!_.isObject(deletedObject)) {
        return;
    }

    keys = _.keys(deletedObject);
    for (i = 0; i < keys.length; i++) {
        key = keys[i];
        if (_.isObject(deletedObject[key])) {
            this.emitInnerDeletions(deletedObject[key], utils.concatPath(path, key));
        }
    }

    this.deletions.emit(options.pathEventPrefix + path, deletedObject);
    this.difference.emit(options.pathEventPrefix + path, this.options.deleteKeyword);
};

module.exports = MergeDiff;
<commit_msg>fixed overidde with no path<commit_after>/**
 * Created by barakedry on 6/19/15.
 */
'use strict';
var _ = require('lodash');
var util = require('util');
var utils = require('./utils');
var DiffTracker = require('./diff-tracker');
var EventEmitter = require('events').EventEmitter;
var debug = require('debuglog')('merge-diff');

function MergeDiff(object, options) {
    this.options = _.defaults(options || {}, {
        emitEvents: true, // settings this to false should allow faster merging but it is not implemented yet
        pathEventPrefix: 'PATH:',
        undefinedKeyword: '__$$U',
        deleteKeyword: '__$$D',
        protoKeyword: '__$$P',
        mergeDeletions: true,
        mergeAdditions: true,
        emitAdditions: true,
        emitUpdates: true,
        emitDifferences: true,
        maxKeysInLevel: 1000,
        maxLevels: 50,
        maxListeners: 1000000
    });

    this.object = object || {};
    //this.prototypes = []; // prototypes are stored in a special collection

    this.setMaxListeners(this.options.maxListeners);
}

util.inherits(MergeDiff, EventEmitter);

MergeDiff.prototype._emitTopLevelEvent = function (event, eventData, options) {
    var levelDiffs = eventData.levelDiffs;

    if (options.emitEvents && levelDiffs && (levelDiffs.hasDeletions || levelDiffs.hasUpdates || levelDiffs.hasDifferences || levelDiffs.hasAdditions)) {
        this.emit(event, eventData); //specific top level event: override, merge, delete, ...
        this.emit('change', eventData); //general top level event
    }
};

MergeDiff.prototype.merge = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    var levelDiffs = this._mergeObject(this.object, utils.wrapByPath(delta, path), '', options, 0);

    this._emitTopLevelEvent('merge', {
        path: path || '*',
        object: this.object,
        delta: delta,
        levelDiffs: levelDiffs
    }, options);
};

MergeDiff.prototype.override = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    var levelDiffs = this._mergeObject(this.object, utils.wrapByPath(delta, path), '', options, 0, path || '');

    this._emitTopLevelEvent('override', {
        path: path || '*',
        object: this.object,
        delta: delta,
        levelDiffs: levelDiffs
    }, options);
};

MergeDiff.prototype.delete = function (path) {

    if (!(path && _.isString(path))) {
        debug('invalid path, cannot delete');
        return;
    }

    var levelDiffs = this._mergeObject(this.object, utils.wrapByPath(this.options.deleteKeyword, path), '', this.options, 0);

    this._emitTopLevelEvent('delete', {
        path: path || '*',
        object: this.object,
        levelDiffs: levelDiffs
    }, this.options);
};

MergeDiff.prototype.get = function (path) {

    if (path && (!_.isString(path))) {
        debug('invalid path, cannot get');
        return;
    }

    if (path) {
        return _.get(this.object, path);
    }

    return this.object;
};

/************************************************************************************
 * The basic merging recursion implementation:
 * ._mergeObject() -> ._mergeAtKey() -> ._mergeObject() -> ._mergeAtKey() -> ...
 *
 * ._mergeObject() iterate keys at level and calls ._mergeAtKey() for each key,
 * after iteration ends it emits and returns level changes to the caller.
 *
 * ._mergeAtKey() assigns/delete primitives and calls _.mergeObject() for objects
 ************************************************************************************/
MergeDiff.prototype._mergeObject = function (source, delta, path, options, level, override) {
    var keys,
        key,
        i,
        length,
        levelDiffs,
        isSourceArray;

    if (!(_.isObject(source) && _.isObject(delta))) {
        debug('invalid merge, source and delta must be objects');
        this.emit('error', new Error('invalid merge, source and delta must be objects'));
        return;
    }

    if (level > options.maxLevels) {
        debug('Trying to merge too deep, stopping at level %d', level);
        this.emit('error', new Error('Trying to merge too deep, stopping at level ' + level));
        return;
    }

    keys = _.keys(delta);
    length = keys.length;
    isSourceArray = _.isArray(source);

    if (options.emitEvents) {
        levelDiffs = DiffTracker.create();
        levelDiffs.path = path;
    }

    if (isSourceArray) {
        levelDiffs = levelDiffs || {};
        levelDiffs.arrayOffset = 0;
    }

    if (length > options.maxKeysInLevel) {
        debug('Stopping Merge, Too many keys in object - %d out of %d allowed keys.', length, options.maxKeysInLevel);
        this.emit('error', new Error('Stopping Merge, Too many keys in object - ' + length + ' out of ' + options.maxKeysInLevel + ' allowed keys.'));
        return levelDiffs;
    }

    // override is either undefined, a path or true
    if (!_.isUndefined(override) && (override === true || override === path)) {
        override = true;
    }

    for (i = 0; i < length; i++) {
        key = keys[i];

        if (utils.isValid(delta[key]) && delta[key] !== source[key]) {
            levelDiffs = this._mergeAtKey(source, delta, path, key, levelDiffs, options, isSourceArray, level, override);
        }

    }

    if (override && (typeof override === 'boolean')) {
        // find keys at this level that exists at the source object and delete them
        levelDiffs = this._detectDeletionsAtLevel(source, delta, levelDiffs, path, options, isSourceArray, level);
    }

    if (options.emitEvents) {
        this.emit(options.pathEventPrefix + (path || '*'), levelDiffs);
    }

    return levelDiffs;
};

MergeDiff.prototype._mergeAtKey = function (source, delta, path, key, levelDiffs, options, isSourceArray, level, override) {

    var childDiffs,
        mergingValue,
        existingValue,
        srcKey,
        mergedValue, // this value is what goes out to the tracker its not allways the same as mergingValue
        isExistingValueArray,
        isObjectMerge = false;

    mergingValue = delta[key];

    if (isSourceArray) {
        srcKey = Number(key) + levelDiffs.arrayOffset;
    } else {
        srcKey = key;
    }

    if (_.isFunction(mergingValue)) {
        mergedValue = utils.SERIALIZABLE_FUNCTION;
    } else {
        isObjectMerge = _.isObject(mergingValue);
        if (_.isUndefined(mergingValue)) {
            mergedValue = options.undefinedKeyword;
        } else if (mergingValue === options.undefinedKeyword) {
            mergedValue = mergingValue;
            mergingValue = undefined;
        } else {
            mergedValue = mergingValue;
        }
    }

    // new
    if (!source.hasOwnProperty(srcKey)) {
        if (options.mergeAdditions && delta[key] !== options.deleteKeyword) {

            // add new object
            if (isObjectMerge) {

                source[srcKey] = mergingValue.constructor.call(Object.create(Object.getPrototypeOf(mergingValue)));

                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               level + 1,
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // add new primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasAdditions = true;
                levelDiffs.additions[key] = mergedValue;
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }
        }
    // existing
    } else {

        existingValue = source[srcKey];
        isExistingValueArray = _.isArray(existingValue);

        // delete
        if (delta[key] === options.deleteKeyword) {

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs);

        // update
        } else {

            // update object
            if (isObjectMerge) {

                // we should replace the source value, todo: array merges check is not sufficient
                if (!isExistingValueArray && !utils.hasSamePrototype(existingValue, mergingValue)) {

                    // this is a restructure
                    // handle prototypes
                    source[srcKey] = Object.create(this.getPrototypeOf(mergingValue));

                }

                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               level + 1,
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // update primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasUpdates = true;
                levelDiffs.updates[key] = {'old': existingValue, 'new': mergedValue};
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }

        }

    }

    return levelDiffs;
};

MergeDiff.prototype._deleteAtKey = function (source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs) {
    if (options.mergeDeletions) {
        if (isSourceArray) {
            source.splice(srcKey, 1);
            levelDiffs.arrayOffset--;
        } else {
            delete source[srcKey];
        }

    }

    levelDiffs.deletions[key] = existingValue;
    levelDiffs.differences[key] = options.deleteKeyword;
    levelDiffs.hasDeletions = true;

    this.emitInnerDeletions(path, existingValue);

    return levelDiffs;
};

MergeDiff.prototype._detectDeletionsAtLevel = function (source, delta, levelDiffs, path, options, isSourceArray) {
    var keys = _.keys(source),
        length = keys.length,
        existingValue,
        srcKey,
        key,
        i;

    // run source object at this level
    for (i = 0; i < length; i++) {
        key = keys[i];
        if (isSourceArray) {
            srcKey = Number(key) + levelDiffs.arrayOffset;
        } else {
            srcKey = key;
        }

        if (!delta.hasOwnProperty(key)) {
            existingValue = source[key];

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue,
                isSourceArray, levelDiffs);

        }

    }
    return levelDiffs;
};

MergeDiff.prototype.getPrototypeOf = function (object) {
    return Object.getPrototypeOf(object);
};

MergeDiff.prototype.emitInnerDeletions = function (deletedObject, path) {
    var keys, key, i;

    if (!_.isObject(deletedObject)) {
        return;
    }

    keys = _.keys(deletedObject);
    for (i = 0; i < keys.length; i++) {
        key = keys[i];
        if (_.isObject(deletedObject[key])) {
            this.emitInnerDeletions(deletedObject[key], utils.concatPath(path, key));
        }
    }

    this.deletions.emit(options.pathEventPrefix + path, deletedObject);
    this.difference.emit(options.pathEventPrefix + path, this.options.deleteKeyword);
};

module.exports = MergeDiff;
<|endoftext|>


<commit_before>/**
 * Created by barakedry on 6/19/15.
 */
'use strict';
var _ = require('lodash');
var util = require('util');
var utils = require('./utils');
var DiffTracker = require('./diff-tracker');
var EventEmitter = require('events').EventEmitter;
var debug = require('debuglog')('merge-diff');

function MergeDiff(object, options) {
    this.options = _.defaults(options || {}, {
        emitEvents: true, // settings this to false should allow faster merging but it is not implemented yet
        undefinedKeyword: '__$$U',
        deleteKeyword: '__$$D',
        protoKeyword: '__$$P',
        mergeDeletions: true,
        mergeAdditions: true,
        emitAdditions: true,
        emitUpdates: true,
        emitDifferences: true,
        maxKeysInLevel: 1000,
        maxLevels: 50,
        maxListeners: 1000000
    });

    this.object = object || {};
    //this.prototypes = []; // prototypes are stored in a special collection

    this.setMaxListeners(this.options.maxListeners);
}

util.inherits(MergeDiff, EventEmitter);

MergeDiff.prototype.merge = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    this._mergeObject(this.object, utils.wrapByPath(delta, path), "", options, 0);
};

MergeDiff.prototype.override = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    this._mergeObject(this.object, utils.wrapByPath(delta, path), "", options, 0, path);
};

MergeDiff.prototype.delete = function (path) {

    if (!(path && _.isString(path))) {
        debug('invalid path, cannot delete');
        return;
    }

    this._mergeObject(this.object, utils.wrapByPath(this.options.deleteKeyword, path), "", this.options, 0);
};

MergeDiff.prototype.get = function (path) {

    if (!(path && _.isString(path))) {
        debug('invalid path, cannot get');
        return;
    }

    return _.get(this.object, path);
};

/************************************************************************************
 * The basic merging recursion implementation:
 * ._mergeObject() -> ._mergeAtKey() -> ._mergeObject() -> ._mergeAtKey() -> ...
 *
 * ._mergeObject() iterate keys at level and calls ._mergeAtKey() for each key,
 * after iteration ends it emits and returns level changes to the caller.
 *
 * ._mergeAtKey() assigns/delete primitives and calls _.mergeObject() for objects
 ************************************************************************************/
MergeDiff.prototype._mergeObject = function (source, delta, path, options, level, override) {
    var keys,
        key,
        i,
        length,
        levelDiffs,
        isSourceArray;

    if (!(_.isObject(source) && _.isObject(delta))) {
        debug('invalid merge, source and delta must be objects');
        this.emit('error', new Error('invalid merge, source and delta must be objects'));
        return;
    }

    if (level > options.maxLevels) {
        debug('Trying to merge too deep, stopping at level %d', level);
        this.emit('error', new Error('Trying to merge too deep, stopping at level ' + level));
        return;
    }

    keys = _.keys(delta);
    length = keys.length;
    isSourceArray = _.isArray(source);

    if (options.emitEvents) {
        levelDiffs = DiffTracker.create();
        levelDiffs.path = path;
    }

    if (isSourceArray) {
        levelDiffs = levelDiffs || {};
        levelDiffs.arrayOffset = 0;
    }

    if (length > options.maxKeysInLevel) {
        debug('Stopping Merge, Too many keys in object - %d out of %d allowed keys.', length, options.maxKeysInLevel);
        this.emit('error', new Error('Stopping Merge, Too many keys in object - ' + length + ' out of ' + options.maxKeysInLevel + ' allowed keys.'));
        return levelDiffs;
    }

    // override is either undefined, a path or true
    if (!_.isUndefined(override) && (override === true || override === path)) {
        override = true;
    }

    for (i = 0; i < length; i++) {
        key = keys[i];

        if (utils.isValid(delta[key]) && delta[key] !== source[key]) {
            levelDiffs = this._mergeAtKey(source, delta, path, key, levelDiffs, options, isSourceArray, override);
        }

    }

    if (override) {
        // find keys at this level that exists at the source object and delete them
        levelDiffs = this._detectDeletionsAtLevel(source, delta, levelDiffs, path, options, isSourceArray);
    }

    if (options.emitEvents) {
        this.emit(path || "*", levelDiffs);
    }

    return levelDiffs;
};

MergeDiff.prototype._mergeAtKey = function (source, delta, path, key, levelDiffs, options, isSourceArray, override) {

    var childDiffs,
        mergingValue,
        existingValue,
        srcKey,
        mergedValue, // this value is what goes out to the tracker its not allways the same as mergingValue
        isExistingValueArray,
        isObjectMerge = false;

    mergingValue = delta[key];

    if (isSourceArray) {
        srcKey = Number(key) + levelDiffs.arrayOffset;
    } else {
        srcKey = key;
    }

    if (_.isFunction(mergingValue)) {
        mergedValue = utils.SERIALIZABLE_FUNCTION;
    } else {
        isObjectMerge = _.isObject(mergingValue);
        if (_.isUndefined(mergingValue)) {
            mergedValue = options.undefinedKeyword;
        } else if (mergingValue === options.undefinedKeyword) {
            mergedValue = mergingValue;
            mergingValue = undefined;
        } else {
            mergedValue = mergingValue;
        }
    }

    // new
    if (!source.hasOwnProperty(srcKey)) {
        if (options.mergeAdditions && delta[key] !== options.deleteKeyword) {

            // add new object
            if (isObjectMerge) {

                source[srcKey] = Object.create(this.getPrototypeOf(mergingValue));
                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               _.isArray(source[srcKey]),
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // add new primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasAdditions = true;
                levelDiffs.additions[key] = mergedValue;
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }
        }
    // existing
    } else {

        existingValue = source[srcKey];
        isExistingValueArray = _.isArray(existingValue);

        // delete
        if (delta[key] === options.deleteKeyword) {

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs);

        // update
        } else {

            // update object
            if (isObjectMerge) {

                // we should replace the source value, todo: array merges check is not sufficient
                if (!isExistingValueArray && !utils.hasSamePrototype(existingValue, mergingValue)) {

                    // this is a restructure
                    // handle prototypes
                    source[srcKey] = Object.create(this.getPrototypeOf(mergingValue));

                }

                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               isExistingValueArray,
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // update primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasUpdates = true;
                levelDiffs.updates[key] = {'old': existingValue, 'new': mergedValue};
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }

        }

    }

    return levelDiffs;
};

MergeDiff.prototype._deleteAtKey = function (source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs) {
    if (options.mergeDeletions) {
        if (isSourceArray) {
            source.splice(srcKey, 1);
            levelDiffs.arrayOffset--;
        } else {
            delete source[srcKey];
        }

    }

    levelDiffs.deletions[key] = existingValue;
    levelDiffs.differences[key] = options.deleteKeyword;
    levelDiffs.hasDeletions = true;

    this.emitInnerDeletions(path, existingValue);

    return levelDiffs;
};

MergeDiff.prototype._detectDeletionsAtLevel = function (source, delta, levelDiffs, path, options, isSourceArray) {
    var keys = _.keys(source),
        length = keys.length,
        existingValue,
        srcKey,
        key,
        i;

    // run source object at this level
    for (i = 0; i < length; i++) {
        key = keys[i];
        if (isSourceArray) {
            srcKey = Number(key) + levelDiffs.arrayOffset;
        } else {
            srcKey = key;
        }

        if (!delta.hasOwnProperty(key)) {
            existingValue = source[key];

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue,
                isSourceArray, levelDiffs);

        }

    }
    return levelDiffs;
};

MergeDiff.prototype.getPrototypeOf = function (object) {
    return Object.getPrototypeOf(object);
};

MergeDiff.prototype.emitInnerDeletions = function (deletedObject, path) {
    var keys, key, i;

    if (!_.isObject(deletedObject)) {
        return;
    }

    keys = _.keys(deletedObject);
    for (i = 0; i < keys.length; i++) {
        key = keys[i];
        if (_.isObject(deletedObject[key])) {
            this.emitInnerDeletions(deletedObject[key], utils.concatPath(path, key));
        }
    }

    this.deletions.emit(path, deletedObject);
    this.difference.emit(path, this.options.deleteKeyword);
};

module.exports = MergeDiff;
<commit_msg>add top level events<commit_after>/**
 * Created by barakedry on 6/19/15.
 */
'use strict';
var _ = require('lodash');
var util = require('util');
var utils = require('./utils');
var DiffTracker = require('./diff-tracker');
var EventEmitter = require('events').EventEmitter;
var debug = require('debuglog')('merge-diff');

function MergeDiff(object, options) {
    this.options = _.defaults(options || {}, {
        emitEvents: true, // settings this to false should allow faster merging but it is not implemented yet
        undefinedKeyword: '__$$U',
        deleteKeyword: '__$$D',
        protoKeyword: '__$$P',
        mergeDeletions: true,
        mergeAdditions: true,
        emitAdditions: true,
        emitUpdates: true,
        emitDifferences: true,
        maxKeysInLevel: 1000,
        maxLevels: 50,
        maxListeners: 1000000
    });

    this.object = object || {};
    //this.prototypes = []; // prototypes are stored in a special collection

    this.setMaxListeners(this.options.maxListeners);
}

util.inherits(MergeDiff, EventEmitter);

MergeDiff.prototype.merge = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    this._mergeObject(this.object, utils.wrapByPath(delta, path), '', options, 0);

    if (options.emitEvents) {
        this.emit('merge', {
            path: path || '*',
            object: this.object,
            delta: delta
        });
    }
};

MergeDiff.prototype.override = function (delta, path, options) {

    options = _.defaults(options || {}, this.options);

    if (!_.isObject(delta) && !path) {
        debug('invalid merge, source and delta must be objects');
        return;
    }

    this._mergeObject(this.object, utils.wrapByPath(delta, path), '', options, 0, path);

    if (options.emitEvents) {
        this.emit('override', {
            path: path || '*',
            object: this.object,
            delta: delta
        });
    }
};

MergeDiff.prototype.delete = function (path) {

    if (!(path && _.isString(path))) {
        debug('invalid path, cannot delete');
        return;
    }

    this._mergeObject(this.object, utils.wrapByPath(this.options.deleteKeyword, path), '', this.options, 0);

    if (this.options.emitEvents) {
        this.emit('delete', {
            path: path || '*',
            object: this.object
        });
    }
};

MergeDiff.prototype.get = function (path) {

    if (!(path && _.isString(path))) {
        debug('invalid path, cannot get');
        return;
    }

    return _.get(this.object, path);
};

/************************************************************************************
 * The basic merging recursion implementation:
 * ._mergeObject() -> ._mergeAtKey() -> ._mergeObject() -> ._mergeAtKey() -> ...
 *
 * ._mergeObject() iterate keys at level and calls ._mergeAtKey() for each key,
 * after iteration ends it emits and returns level changes to the caller.
 *
 * ._mergeAtKey() assigns/delete primitives and calls _.mergeObject() for objects
 ************************************************************************************/
MergeDiff.prototype._mergeObject = function (source, delta, path, options, level, override) {
    var keys,
        key,
        i,
        length,
        levelDiffs,
        isSourceArray;

    if (!(_.isObject(source) && _.isObject(delta))) {
        debug('invalid merge, source and delta must be objects');
        this.emit('error', new Error('invalid merge, source and delta must be objects'));
        return;
    }

    if (level > options.maxLevels) {
        debug('Trying to merge too deep, stopping at level %d', level);
        this.emit('error', new Error('Trying to merge too deep, stopping at level ' + level));
        return;
    }

    keys = _.keys(delta);
    length = keys.length;
    isSourceArray = _.isArray(source);

    if (options.emitEvents) {
        levelDiffs = DiffTracker.create();
        levelDiffs.path = path;
    }

    if (isSourceArray) {
        levelDiffs = levelDiffs || {};
        levelDiffs.arrayOffset = 0;
    }

    if (length > options.maxKeysInLevel) {
        debug('Stopping Merge, Too many keys in object - %d out of %d allowed keys.', length, options.maxKeysInLevel);
        this.emit('error', new Error('Stopping Merge, Too many keys in object - ' + length + ' out of ' + options.maxKeysInLevel + ' allowed keys.'));
        return levelDiffs;
    }

    // override is either undefined, a path or true
    if (!_.isUndefined(override) && (override === true || override === path)) {
        override = true;
    }

    for (i = 0; i < length; i++) {
        key = keys[i];

        if (utils.isValid(delta[key]) && delta[key] !== source[key]) {
            levelDiffs = this._mergeAtKey(source, delta, path, key, levelDiffs, options, isSourceArray, override);
        }

    }

    if (override) {
        // find keys at this level that exists at the source object and delete them
        levelDiffs = this._detectDeletionsAtLevel(source, delta, levelDiffs, path, options, isSourceArray);
    }

    if (options.emitEvents) {
        this.emit(path || '*', levelDiffs);
    }

    return levelDiffs;
};

MergeDiff.prototype._mergeAtKey = function (source, delta, path, key, levelDiffs, options, isSourceArray, override) {

    var childDiffs,
        mergingValue,
        existingValue,
        srcKey,
        mergedValue, // this value is what goes out to the tracker its not allways the same as mergingValue
        isExistingValueArray,
        isObjectMerge = false;

    mergingValue = delta[key];

    if (isSourceArray) {
        srcKey = Number(key) + levelDiffs.arrayOffset;
    } else {
        srcKey = key;
    }

    if (_.isFunction(mergingValue)) {
        mergedValue = utils.SERIALIZABLE_FUNCTION;
    } else {
        isObjectMerge = _.isObject(mergingValue);
        if (_.isUndefined(mergingValue)) {
            mergedValue = options.undefinedKeyword;
        } else if (mergingValue === options.undefinedKeyword) {
            mergedValue = mergingValue;
            mergingValue = undefined;
        } else {
            mergedValue = mergingValue;
        }
    }

    // new
    if (!source.hasOwnProperty(srcKey)) {
        if (options.mergeAdditions && delta[key] !== options.deleteKeyword) {

            // add new object
            if (isObjectMerge) {

                source[srcKey] = Object.create(this.getPrototypeOf(mergingValue));
                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               _.isArray(source[srcKey]),
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // add new primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasAdditions = true;
                levelDiffs.additions[key] = mergedValue;
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }
        }
    // existing
    } else {

        existingValue = source[srcKey];
        isExistingValueArray = _.isArray(existingValue);

        // delete
        if (delta[key] === options.deleteKeyword) {

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs);

        // update
        } else {

            // update object
            if (isObjectMerge) {

                // we should replace the source value, todo: array merges check is not sufficient
                if (!isExistingValueArray && !utils.hasSamePrototype(existingValue, mergingValue)) {

                    // this is a restructure
                    // handle prototypes
                    source[srcKey] = Object.create(this.getPrototypeOf(mergingValue));

                }

                childDiffs = this._mergeObject(source[srcKey],
                                               mergingValue,
                                               utils.concatPath(path, key),
                                               options,
                                               isExistingValueArray,
                                               override);

                levelDiffs.addChildTracking(childDiffs, key);

            // update primitive
            } else {

                source[srcKey] = mergingValue;

                levelDiffs.hasUpdates = true;
                levelDiffs.updates[key] = {'old': existingValue, 'new': mergedValue};
                levelDiffs.hasDifferences = true;
                levelDiffs.differences[key] = mergedValue;
            }

        }

    }

    return levelDiffs;
};

MergeDiff.prototype._deleteAtKey = function (source, path, key, srcKey, options, existingValue, isSourceArray, levelDiffs) {
    if (options.mergeDeletions) {
        if (isSourceArray) {
            source.splice(srcKey, 1);
            levelDiffs.arrayOffset--;
        } else {
            delete source[srcKey];
        }

    }

    levelDiffs.deletions[key] = existingValue;
    levelDiffs.differences[key] = options.deleteKeyword;
    levelDiffs.hasDeletions = true;

    this.emitInnerDeletions(path, existingValue);

    return levelDiffs;
};

MergeDiff.prototype._detectDeletionsAtLevel = function (source, delta, levelDiffs, path, options, isSourceArray) {
    var keys = _.keys(source),
        length = keys.length,
        existingValue,
        srcKey,
        key,
        i;

    // run source object at this level
    for (i = 0; i < length; i++) {
        key = keys[i];
        if (isSourceArray) {
            srcKey = Number(key) + levelDiffs.arrayOffset;
        } else {
            srcKey = key;
        }

        if (!delta.hasOwnProperty(key)) {
            existingValue = source[key];

            levelDiffs = this._deleteAtKey(source, path, key, srcKey, options, existingValue,
                isSourceArray, levelDiffs);

        }

    }
    return levelDiffs;
};

MergeDiff.prototype.getPrototypeOf = function (object) {
    return Object.getPrototypeOf(object);
};

MergeDiff.prototype.emitInnerDeletions = function (deletedObject, path) {
    var keys, key, i;

    if (!_.isObject(deletedObject)) {
        return;
    }

    keys = _.keys(deletedObject);
    for (i = 0; i < keys.length; i++) {
        key = keys[i];
        if (_.isObject(deletedObject[key])) {
            this.emitInnerDeletions(deletedObject[key], utils.concatPath(path, key));
        }
    }

    this.deletions.emit(path, deletedObject);
    this.difference.emit(path, this.options.deleteKeyword);
};

module.exports = MergeDiff;
<|endoftext|>


<commit_before>/**
 * @module lib/merge-sort
 * @license MIT Copyright 2014 Daniel Imms (http://www.growingwiththeweb.com)
 */
'use strict';

var defaultCompare = require('./common/default-compare');

/**
 * Sorts an array using top down merge sort.
 * @param {Array} array The array to sort.
 * @param {function} compare The compare function.
 * @returns The sorted array.
 */
function sort(array, compare) {
  if (array.length <= 1) {
    return array;
  }

  var i;
  var middle = Math.floor(array.length / 2);
  var left = new Array(middle);
  var right = new Array(array.length - middle);

  for (i = 0; i < left.length; i++) {
    left[i] = array[i];
  }
  for (i = 0; i < right.length; i++) {
    right[i] = array[i + left.length];
  }

  return merge(sort(left, compare), sort(right, compare), compare);
}

/**
 * Merges two arrays in to a new array.
 * @param {Array} left The first array.
 * @param {Array} left The second array.
 * @param {function} compare The compare function.
 * @return The merged array.
 */
function merge(left, right, compare) {
  var result = new Array(left.length + right.length);
  var leftIndex = 0;
  var rightIndex = 0;
  var resultIndex = 0;

  while (leftIndex < left.length || rightIndex < right.length) {
    if (leftIndex < left.length && rightIndex < right.length) {
      if (compare(left[leftIndex], right[rightIndex]) <= 0) {
        result[resultIndex++] = left[leftIndex++];
      } else {
        result[resultIndex++] = right[rightIndex++];
      }
    } else if (leftIndex < left.length) {
      result[resultIndex++] = left[leftIndex++];
    } else if (rightIndex < right.length) {
      result[resultIndex++] = right[rightIndex++];
    }
  }
  return result;
}

/**
 * Sorts an array using top down merge sort.
 * @param {Array} array The array to sort.
 * @param {function} customCompare A custom compare function.
 * @returns The sorted array.
 */
module.exports = function (array, customCompare) {
  var compare = customCompare || defaultCompare;
  return sort(array, compare);
};
<commit_msg>Use push instead of resultIndex<commit_after>/**
 * @module lib/merge-sort
 * @license MIT Copyright 2014 Daniel Imms (http://www.growingwiththeweb.com)
 */
'use strict';

var defaultCompare = require('./common/default-compare');

/**
 * Sorts an array using top down merge sort.
 * @param {Array} array The array to sort.
 * @param {function} compare The compare function.
 * @returns The sorted array.
 */
function sort(array, compare) {
  if (array.length <= 1) {
    return array;
  }

  var i;
  var middle = Math.floor(array.length / 2);
  var left = new Array(middle);
  var right = new Array(array.length - middle);

  for (i = 0; i < left.length; i++) {
    left[i] = array[i];
  }
  for (i = 0; i < right.length; i++) {
    right[i] = array[i + left.length];
  }

  return merge(sort(left, compare), sort(right, compare), compare);
}

/**
 * Merges two arrays in to a new array.
 * @param {Array} left The first array.
 * @param {Array} left The second array.
 * @param {function} compare The compare function.
 * @return The merged array.
 */
function merge(left, right, compare) {
  var result = [];
  var leftIndex = 0;
  var rightIndex = 0;

  while (leftIndex < left.length || rightIndex < right.length) {
    if (leftIndex < left.length && rightIndex < right.length) {
      if (compare(left[leftIndex], right[rightIndex]) <= 0) {
        result.push(left[leftIndex++]);
      } else {
        result.push(right[rightIndex++]);
      }
    } else if (leftIndex < left.length) {
      result.push(left[leftIndex++]);
    } else if (rightIndex < right.length) {
      result.push(right[rightIndex++]);
    }
  }
  return result;
}

/**
 * Sorts an array using top down merge sort.
 * @param {Array} array The array to sort.
 * @param {function} customCompare A custom compare function.
 * @returns The sorted array.
 */
module.exports = function (array, customCompare) {
  var compare = customCompare || defaultCompare;
  return sort(array, compare);
};
<|endoftext|>


<commit_before>var utils   = require('./utils'),
    helpers = require('./helpers'),
    custom  = require('./custom'),
    errors  = require('./errors'),
    connect = require('express/node_modules/connect'),
    models  = require('./models'),
    config  = require('./config');

// Custom middleware used by the application.
module.exports = {
  // Transparently handle JSONP. Although this does require wrapping the
  // Request#send() method which is a bit nasty. Other alternative is to
  // always pass the response on and include this middleware after the
  // routes.
  jsonp: function () {
    return function (req, res, next) {
      var _send = res.send;
      res.send = function (body) {
        var callback = req.param('callback'),
            isJSONP  = res.get('Content-Type') === 'application/json' && callback;

        if (body && req.method !== 'HEAD' && isJSONP) {
          res.contentType('js');
          body = callback + '(' + body.toString().trim() + ');';
        }
        _send.call(this, body);
      };
      next();
    };
  },
  // Check for ajax requests and set the Request/Response#ajax flag to true.
  ajax: function () {
    return function (req, res, next) {
      req.ajax = res.ajax = utils.isAjax(req);

      // if ((req.get('X-Stream') || '').toLowerCase() === 'true') {
      //   req.ajax = false;
      // }

      next();
    };
  },
  // Add relevant CORS headers for the application.
  cors: function () {
    return function (req, res, next) {
      var headers = req.header('Access-Control-Request-Headers');
      var origin = req.header('Origin');

      if (req.method === 'OPTIONS' || (req.method === 'GET' && utils.isAjax(req)) || (req.method === 'GET' && req.headers.origin)) {
        res.header({
          'Access-Control-Allow-Origin':  origin,
          'Access-Control-Allow-Headers': headers
        });
        req.cors = true;
      }

      if (req.method === 'OPTIONS') {
        res.send(204);
      } else {
        next();
      }
    };
  },
  flash: function () {
    function flash(key, message) {
      var cache = this.session.flash = this.session.flash || {},
          value;

      if (arguments.length === 2) {
        cache[key] = message;
        return this;
      } else if (arguments.length === 1) {
        value = cache[key];
        delete cache[key];
        return value;
      }

      this.session.flash = {};
      return cache;
    }

    return function (req, res, next) {
      req.flash = res.flash = flash.bind(req);

      req.flash.INFO = 'info';
      req.flash.ERROR = 'error';
      req.flash.NOTIFICATION = 'notification';

      next();
    };
  },
  // Redirect urls with trailing slashes to their non-trailing counterpart,
  // requires the "strict routing" setting to be false.
  noslashes: function () {
    return function (req, res, next) {
      if (req.path !== '/' && req.originalUrl.slice(-1) === '/') {
        return res.redirect(301, req.originalUrl.slice(0, -1));
      }
      next();
    };
  },
  // monkey patch for express' own csrf method, so we can ignore specific url patterns
  csrf: function (options) {
    var ignore = options.ignore || [],
        csrf = connect.csrf(options),
        always = {OPTIONS: 1, GET: 1, HEAD: 1};

    return function (req, res, next) {
      if (always[req.method]) {
        return csrf(req, res, next);
      } else {
        var skipCSRF = false;
        ignore.forEach(function(matcher) {
          if (typeof matcher === 'string') {
            if (matcher == req.url) {
              skipCSRF = true;
            }
          } else {
            // regular expression matcher
            if (req.url.match(matcher)) {
              skipCSRF = true;
            }
          }
        });

        if (skipCSRF) {
          next();
        } else {
          return csrf(req, res, next);
        }
      }
    };
  },
  // Checks for a subdomain in the current url, if found it sets the
  // req.subdomain property. This supports existing behaviour that allows
  // subdomains to load custom config files.
  subdomain: function (app) {
    return function (req, res, next) {
      var apphost = app.set('url host').split(':')[0],
          host = req.header('Host', ''),
          offset = host.indexOf(apphost);

      if (offset) {
        // Slice the host from the subdomain and subtract 1 for
        // trailing . on the subdomain.
        req.subdomain = host.slice(0, offset - 1);
      }
      next();
    };
  },

  // Limit the file size that can be uploaded.
  limitContentLength: function (options) {
    // Parse a string representing a file size and convert it into bytes.
    // A number on it's own will be assumed to be bytes. A multiple such as
    // "k" or "m" can be appended to the string to handle larger numbers. This
    // is case insensitive and uses powers of 1024 rather than (1000).
    // So both 1kB and 1kb == 1024.
    function parseLimit(string) {
      var matches = ('' + string).toLowerCase().match(regexp),
          bytes = null, power;

      if (matches) {
        bytes = parseFloat(matches[1]);
        power = powers[matches[2]];

        if (bytes && power) {
          bytes = Math.pow(bytes * 1024, power);
        }
      }

      return bytes || null;
    }

    var powers = { k: 1, m: 2, g: 3, t: 4 },
        regexp = /^(\d+(?:.\d+)?)\s*([kmgt]?)b?$/,
        limit  = options && parseLimit(options.limit);

    return function (req, res, next) {
      if (limit) {
        var contentLength = parseInt(req.header('Content-Length', 0), 10),
            message = 'Sorry, the content you have uploaded is larger than JS Bin can handle. Max size is ' + options.limit;

        if (limit && contentLength > limit) {
          return next(new errors.RequestEntityTooLarge(message));
        }
      }
      next();
    };
  },

  // detect if this is an API request and add flag isApi to the request object
  api: function(options) {
    return function (req, res, next) {
      var apiKey,
          userModel = models.createModels(options.app.store).user;

      if (req.url.indexOf('/api') === 0) {
        req.isApi = true;

        if (config.api.requireSSL) {
          if (!req.secure && (String(req.headers['x-forwarded-proto']).toLowerCase() !== "https") ) {
            res.status(403); // forbidden
            res.json({ error: 'All API requests must be made over SSL/TLS' });
            return;
          }
        }

        if (req.query.api_key) {
          req.apiKey = req.query.api_key;
        } else if (req.headers.authorization) {
          req.apiKey = req.headers.authorization.replace(/token\s/,'');
        }

        var validateApiRequest = function() {
          if (config.api.allowAnonymousReadWrite || (config.api.allowAnonymousRead && req.method === 'GET')) {
            next();
          } else {
            if (!req.apiKey) {
              res.status(403); // forbidden
              res.json({ error: 'You need to provide a valid API key when using this API' });
            } else {
              next();
            }
          }
        };

        if (req.apiKey) {
          userModel.loadByApiKey(req.apiKey, function (err, user) {
            if (err) {
              return next(err);
            }
            if (user) {
              req.session.user = user;
              validateApiRequest();
            } else {
              res.status(403); // forbidden
              res.json({ error: 'The API key you provided is not valid' });
            }
          });
        } else {
          validateApiRequest();
        }
      } else {
        next();
      }
    };
  }
};
<commit_msg>Ensure API is stateless (no session cookie)<commit_after>var utils   = require('./utils'),
    helpers = require('./helpers'),
    custom  = require('./custom'),
    errors  = require('./errors'),
    connect = require('express/node_modules/connect'),
    models  = require('./models'),
    config  = require('./config');

// Custom middleware used by the application.
module.exports = {
  // Transparently handle JSONP. Although this does require wrapping the
  // Request#send() method which is a bit nasty. Other alternative is to
  // always pass the response on and include this middleware after the
  // routes.
  jsonp: function () {
    return function (req, res, next) {
      var _send = res.send;
      res.send = function (body) {
        var callback = req.param('callback'),
            isJSONP  = res.get('Content-Type') === 'application/json' && callback;

        if (body && req.method !== 'HEAD' && isJSONP) {
          res.contentType('js');
          body = callback + '(' + body.toString().trim() + ');';
        }
        _send.call(this, body);
      };
      next();
    };
  },
  // Check for ajax requests and set the Request/Response#ajax flag to true.
  ajax: function () {
    return function (req, res, next) {
      req.ajax = res.ajax = utils.isAjax(req);

      // if ((req.get('X-Stream') || '').toLowerCase() === 'true') {
      //   req.ajax = false;
      // }

      next();
    };
  },
  // Add relevant CORS headers for the application.
  cors: function () {
    return function (req, res, next) {
      var headers = req.header('Access-Control-Request-Headers');
      var origin = req.header('Origin');

      if (req.method === 'OPTIONS' || (req.method === 'GET' && utils.isAjax(req)) || (req.method === 'GET' && req.headers.origin)) {
        res.header({
          'Access-Control-Allow-Origin':  origin,
          'Access-Control-Allow-Headers': headers
        });
        req.cors = true;
      }

      if (req.method === 'OPTIONS') {
        res.send(204);
      } else {
        next();
      }
    };
  },
  flash: function () {
    function flash(key, message) {
      var cache = this.session.flash = this.session.flash || {},
          value;

      if (arguments.length === 2) {
        cache[key] = message;
        return this;
      } else if (arguments.length === 1) {
        value = cache[key];
        delete cache[key];
        return value;
      }

      this.session.flash = {};
      return cache;
    }

    return function (req, res, next) {
      req.flash = res.flash = flash.bind(req);

      req.flash.INFO = 'info';
      req.flash.ERROR = 'error';
      req.flash.NOTIFICATION = 'notification';

      next();
    };
  },
  // Redirect urls with trailing slashes to their non-trailing counterpart,
  // requires the "strict routing" setting to be false.
  noslashes: function () {
    return function (req, res, next) {
      if (req.path !== '/' && req.originalUrl.slice(-1) === '/') {
        return res.redirect(301, req.originalUrl.slice(0, -1));
      }
      next();
    };
  },
  // monkey patch for express' own csrf method, so we can ignore specific url patterns
  csrf: function (options) {
    var ignore = options.ignore || [],
        csrf = connect.csrf(options),
        always = {OPTIONS: 1, GET: 1, HEAD: 1};

    return function (req, res, next) {
      if (always[req.method]) {
        return csrf(req, res, next);
      } else {
        var skipCSRF = false;
        ignore.forEach(function(matcher) {
          if (typeof matcher === 'string') {
            if (matcher == req.url) {
              skipCSRF = true;
            }
          } else {
            // regular expression matcher
            if (req.url.match(matcher)) {
              skipCSRF = true;
            }
          }
        });

        if (skipCSRF) {
          next();
        } else {
          return csrf(req, res, next);
        }
      }
    };
  },
  // Checks for a subdomain in the current url, if found it sets the
  // req.subdomain property. This supports existing behaviour that allows
  // subdomains to load custom config files.
  subdomain: function (app) {
    return function (req, res, next) {
      var apphost = app.set('url host').split(':')[0],
          host = req.header('Host', ''),
          offset = host.indexOf(apphost);

      if (offset) {
        // Slice the host from the subdomain and subtract 1 for
        // trailing . on the subdomain.
        req.subdomain = host.slice(0, offset - 1);
      }
      next();
    };
  },

  // Limit the file size that can be uploaded.
  limitContentLength: function (options) {
    // Parse a string representing a file size and convert it into bytes.
    // A number on it's own will be assumed to be bytes. A multiple such as
    // "k" or "m" can be appended to the string to handle larger numbers. This
    // is case insensitive and uses powers of 1024 rather than (1000).
    // So both 1kB and 1kb == 1024.
    function parseLimit(string) {
      var matches = ('' + string).toLowerCase().match(regexp),
          bytes = null, power;

      if (matches) {
        bytes = parseFloat(matches[1]);
        power = powers[matches[2]];

        if (bytes && power) {
          bytes = Math.pow(bytes * 1024, power);
        }
      }

      return bytes || null;
    }

    var powers = { k: 1, m: 2, g: 3, t: 4 },
        regexp = /^(\d+(?:.\d+)?)\s*([kmgt]?)b?$/,
        limit  = options && parseLimit(options.limit);

    return function (req, res, next) {
      if (limit) {
        var contentLength = parseInt(req.header('Content-Length', 0), 10),
            message = 'Sorry, the content you have uploaded is larger than JS Bin can handle. Max size is ' + options.limit;

        if (limit && contentLength > limit) {
          return next(new errors.RequestEntityTooLarge(message));
        }
      }
      next();
    };
  },

  // detect if this is an API request and add flag isApi to the request object
  api: function(options) {
    return function (req, res, next) {
      var apiKey,
          userModel = models.createModels(options.app.store).user;

      if (req.url.indexOf('/api') === 0) {
        req.isApi = true;

        // Make the API requests stateless by removin the cookie set by middleware cookieSession
        res.on('header', function() {
          res.removeHeader("Set-Cookie");
        });

        if (config.api.requireSSL) {
          if (!req.secure && (String(req.headers['x-forwarded-proto']).toLowerCase() !== "https") ) {
            res.status(403); // forbidden
            res.json({ error: 'All API requests must be made over SSL/TLS' });
            return;
          }
        }

        if (req.query.api_key) {
          req.apiKey = req.query.api_key;
        } else if (req.headers.authorization) {
          req.apiKey = req.headers.authorization.replace(/token\s/,'');
        }

        var validateApiRequest = function() {
          if (config.api.allowAnonymousReadWrite || (config.api.allowAnonymousRead && req.method === 'GET')) {
            next();
          } else {
            if (!req.apiKey) {
              res.status(403); // forbidden
              res.json({ error: 'You need to provide a valid API key when using this API' });
            } else {
              next();
            }
          }
        };

        if (req.apiKey) {
          userModel.loadByApiKey(req.apiKey, function (err, user) {
            if (err) {
              return next(err);
            }
            if (user) {
              req.session.user = user;
              validateApiRequest();
            } else {
              res.status(403); // forbidden
              res.json({ error: 'The API key you provided is not valid' });
            }
          });
        } else {
          validateApiRequest();
        }
      } else {
        next();
      }
    };
  }
};
<|endoftext|>


<commit_before>var utils = require('./utils')

function oa(x, y) { return Object.assign({}, x, y) }

function verifyAction(action, sourceName) {
    var isValid = (action && action.type && action.meta)
    if (!isValid) throw new Error((sourceName || 'Argument') + ' should be an action')
}

// Redux middleware for handling async actions from services
module.exports = function makeMiddleware(creators) {
    return function factionServiceMiddleware(store) {
        var dispatch = store.dispatch

        return function(next) {
            return function(action) {
                if (!action.meta || !action.meta._fromFactionService) return next(action)
                if (!utils.isPromise(action.payload)) return next(action)

                // Read the optional success/error callbacks from action.meta
                var _successCb = action.meta._successCb
                var _errorCb = action.meta._errorCb
                delete action.meta._successCb
                delete action.meta._errorCb

                // Dispatch a "pending" version of the action, so reducers
                // can set up any state related to loading:
                dispatch(oa(action, {
                    payload: null,
                    meta: oa(action.meta, { isPending: true })
                }))

                action.payload.then(function(result) {
                    dispatch(oa(action, { payload: result }))

                    if (typeof _successCb === 'function') {
                        var followUpAction = _successCb(creators, action)
                        var multi = Array.isArray(followUpAction)
                        ;(multi ? followUpAction : [followUpAction]).forEach(function(action) {
                            verifyAction(action, 'onSuccess cb return value')
                            dispatch(action)
                        })
                    }
                }).catch(function(error) {
                    dispatch(oa(action, { payload: error, error: true }))

                    if (typeof _errorCb === 'function') {
                        var followUpAction = _errorCb(creators, action)
                        var multi = Array.isArray(followUpAction)
                        ;(multi ? followUpAction : [followUpAction]).forEach(function(action) {
                            verifyAction(action, 'onError cb return value')
                            dispatch(action)
                        })
                    }
                })
            }
        }
    }
}
<commit_msg>Middleware refactor to keep things DRY<commit_after>var utils = require('./utils')

function oa(x, y) { return Object.assign({}, x, y) }

// Redux middleware for handling async actions from services
module.exports = function makeMiddleware(creators) {
    return function factionServiceMiddleware(store) {
        var dispatch = store.dispatch

        // Takes an action (or array of them); verifies, and dispatches them
        function handleFollowUp(action, contextName) {
            var isMulti = Array.isArray(action)
            var actionList = (isMulti ? action : [action])
            actionList.forEach(function(action) {
                var isValid = (action && action.type && action.meta)
                if (!isValid) {
                    throw new Error(contextName + ' callback should return action(s)')
                }
                dispatch(action)
            })
        }

        return function(next) {
            return function(action) {
                if (!action.meta || !action.meta._fromFactionService) return next(action)
                if (!utils.isPromise(action.payload)) return next(action)

                // Read the optional success/error callbacks from action.meta
                var _successCb = action.meta._successCb
                var _errorCb = action.meta._errorCb
                delete action.meta._successCb
                delete action.meta._errorCb

                // Dispatch a "pending" version of the action, so reducers
                // can set up any state related to loading:
                dispatch(oa(action, {
                    payload: null,
                    meta: oa(action.meta, { isPending: true })
                }))

                // When the Promise payload resolves, dispatch the success/error
                // action, and fire any follow up actions that were registered:
                action.payload.then(function(result) {
                    dispatch(oa(action, { payload: result }))

                    if (typeof _successCb === 'function') {
                        handleFollowUp(_successCb(creators, action), 'onSuccess')
                    }
                }).catch(function(error) {
                    dispatch(oa(action, { payload: error, error: true }))

                    if (typeof _errorCb === 'function') {
                        handleFollowUp(_errorCb(creators, action), 'onError')
                    }
                })
            }
        }
    }
}
<|endoftext|>


<commit_before>var path = require("path");
var events = require("events");

var mime = require("mime");
var MemoryFileSystem = require("memory-fs");

module.exports = function(compiler, options) {
	options = options || {};
	options.watchOptions = options.watchOptions || {};
	options.watchOptions.aggregateTimeout = options.watchOptions.aggregateTimeout || 200;

	var fs = new MemoryFileSystem();
	var emitter = new events.EventEmitter();
	var valid = false;

	function* middleware(next) {

		/* get the requested filename */
		var filename = getFilename(this.path);

		/* get the file */
		var content = yield getFileContent(filename);

		/* return it */
		this.body = content;

		/* set some headers */
		this.set("Access-Control-Allow-Origin", "*"); // To support XHR, etc.
		this.set("Content-Type", mime.lookup(filename));
		this.set("Content-Length", content.length);

		if (options.headers) {
			for( var name in options.headers) {
				this.set(name, options.headers[name]);
			}
		}

		/* and we're done */
		yield next;
	}

	function getFilename(filename) {
		var publicPrefix = options.publicPath || "/";

		if (filename == '/')
			return "/index.html";
		else
			return filename.slice(publicPrefix.length -1);
	}

	function* getFileContent(filename) {
		while (!valid) {
			yield emitter.once.bind(emitter, 'done');
			yield process.nextTick;
		}

		var text = yield fs.readFile.bind(fs, filename);

		return text;
	}

	function donePlugin(stats) {
		console.log(stats.toString());
		valid = true;
		emitter.emit('done');
	}

	function invalidatePlugin(compiler, callback) {
		if (valid)
			console.info("webpack: bundle is now INVALID.");

		valid = false;
		if (callback) callback();
	}

	compiler.plugin("invalid", invalidatePlugin);
	compiler.plugin("watch-run", invalidatePlugin);
	compiler.plugin("run", invalidatePlugin);
	compiler.plugin("done", donePlugin);

	compiler.outputFileSystem = fs;

	var watching = compiler.watch(options.watchOptions, function(err) {
		if(err) throw err;
	});

	middleware.invalidate = function() {
		if(watching) watching.invalidate();
	};

	middleware.close = function(callback) {
		callback = callback || function(){};
		if(watching) watching.close(callback);
		else callback();
	};

	return middleware;
}
<commit_msg>fix(routes): support routes<commit_after>var path = require("path");
var events = require("events");

var mime = require("mime");
var MemoryFileSystem = require("memory-fs");

module.exports = function(compiler, options) {
	options = options || {};
	options.watchOptions = options.watchOptions || {};
	options.watchOptions.aggregateTimeout = options.watchOptions.aggregateTimeout || 200;
	options.publicPath = options.publicPath || '/';

	var fs = new MemoryFileSystem();
	var emitter = new events.EventEmitter();
	var valid = false;

	function* middleware(next) {

		/* we only support GET requests */
 		if (this.method !== 'GET') return yield next;

		/* get the requested filename */
		var filename = getFilename(this.path);

		/* get the file contents */
		var content = yield getFileContent(filename);

		/* return it */
		this.body = content;

		/* set some headers */
		this.set("Access-Control-Allow-Origin", "*"); // To support XHR, etc.
		this.set("Content-Type", mime.lookup(filename));
		this.set("Content-Length", content.length);

		if (options.headers) {
			for( var name in options.headers) {
				this.set(name, options.headers[name]);
			}
		}

		/* and we're done */
		yield next;
	}

	function getFilename(filename) {
		/* all routes get redirected to /index.html */
		if (path.basename(filename).indexOf(".") < 0)
			return "/index.html";
		else
			return filename.slice(options.publicPath -1);
	}

	function* getFileContent(filename) {
		while (!valid) {
			yield emitter.once.bind(emitter, 'done');
			yield process.nextTick;
		}

		var text = yield fs.readFile.bind(fs, filename);

		return text;
	}

	function donePlugin(stats) {
		console.log(stats.toString());
		valid = true;
		emitter.emit('done');
	}

	function invalidatePlugin(compiler, callback) {
		if (valid)
			console.info("webpack: bundle is now INVALID.");

		valid = false;
		if (callback) callback();
	}

	compiler.plugin("invalid", invalidatePlugin);
	compiler.plugin("watch-run", invalidatePlugin);
	compiler.plugin("run", invalidatePlugin);
	compiler.plugin("done", donePlugin);

	compiler.outputFileSystem = fs;

	var watching = compiler.watch(options.watchOptions, function(err) {
		if(err) throw err;
	});

	middleware.invalidate = function() {
		if(watching) watching.invalidate();
	};

	middleware.close = function(callback) {
		callback = callback || function(){};
		if(watching) watching.close(callback);
		else callback();
	};

	return middleware;
}
<|endoftext|>


<commit_before>/**
 *  Copyright (c) 2015, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

import * as Kind from '../kinds';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { parse } from '../parser';
import { Source } from '../source';
import { readFileSync } from 'fs';
import { join } from 'path';

describe('Parser', () => {

  it('accepts option to not include source', () => {
    expect(parse(`{ field }`, { noSource: true })).to.deep.equal({
      kind: 'Document',
      loc: { start: 0, end: 9 },
      definitions:
       [ { kind: 'OperationDefinition',
           loc: { start: 0, end: 9 },
           operation: 'query',
           name: null,
           variableDefinitions: null,
           directives: [],
           selectionSet: {
             kind: 'SelectionSet',
             loc: { start: 0, end: 9 },
             selections:
              [ { kind: 'Field',
                  loc: { start: 2, end: 7 },
                  alias: null,
                  name:
                   { kind: 'Name',
                     loc: { start: 2, end: 7 },
                     value: 'field' },
                  arguments: [],
                  directives: [],
                  selectionSet: null } ] } } ]
    });
  });

  it('parse provides useful errors', () => {

    expect(
      () => parse(
`{ ...MissingOn }
fragment MissingOn Type
`)
    ).to.throw('Syntax Error GraphQL (2:20) Expected "on", found Name "Type"');

    expect(
      () => parse('{ field: {} }')
    ).to.throw('Syntax Error GraphQL (1:10) Expected Name, found {');

    expect(
      () => parse('notanoperation Foo { field }')
    ).to.throw('Syntax Error GraphQL (1:1) Unexpected Name "notanoperation"');

    expect(
      () => parse('...')
    ).to.throw('Syntax Error GraphQL (1:1) Unexpected ...');

  });

  it('parse provides useful error when using source', () => {
    expect(
      () => parse(new Source('query', 'MyQuery.graphql'))
    ).to.throw('Syntax Error MyQuery.graphql (1:6) Expected Name, found EOF');
  });

  it('parses variable inline values', () => {
    expect(
      () => parse('{ field(complex: { a: { b: [ $var ] } }) }')
    ).to.not.throw();
  });

  it('parses constant default values', () => {
    expect(
      () => parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }')
    ).to.throw('Syntax Error GraphQL (1:37) Unexpected $');
  });

  it('duplicate keys in input object is syntax error', () => {
    expect(
      () => parse('{ field(arg: { a: 1, a: 2 }) }')
    ).to.throw('Syntax Error GraphQL (1:22) Duplicate input object field a.');
  });

  it('does not accept fragments named "on"', () => {
    expect(
      () => parse('fragment on on on { on }')
    ).to.throw('Syntax Error GraphQL (1:10) Unexpected Name "on"');
  });

  it('does not accept fragments spread of "on"', () => {
    expect(
      () => parse('{ ...on }')
    ).to.throw('Syntax Error GraphQL (1:9) Expected Name, found }');
  });

  it('does not allow null as value', async () => {
    expect(
      () => parse('{ fieldWithNullableStringInput(input: null) }')
    ).to.throw('Syntax Error GraphQL (1:39) Unexpected Name "null"');
  });

  var kitchenSink = readFileSync(
    join(__dirname, '/kitchen-sink.graphql'),
    { encoding: 'utf8' }
  );

  it('parses kitchen sink', () => {
    expect(() => parse(kitchenSink)).to.not.throw();
  });

  it('parse creates ast', () => {

    var source = new Source(`{
  node(id: 4) {
    id,
    name
  }
}
`);
    var result = parse(source);

    expect(result).to.deep.equal(
      { kind: Kind.DOCUMENT,
        loc: { start: 0, end: 41, source },
        definitions:
         [ { kind: Kind.OPERATION_DEFINITION,
             loc: { start: 0, end: 40, source },
             operation: 'query',
             name: null,
             variableDefinitions: null,
             directives: [],
             selectionSet:
              { kind: Kind.SELECTION_SET,
                loc: { start: 0, end: 40, source },
                selections:
                 [ { kind: Kind.FIELD,
                     loc: { start: 4, end: 38, source },
                     alias: null,
                     name:
                      { kind: Kind.NAME,
                        loc: { start: 4, end: 8, source },
                        value: 'node' },
                     arguments:
                      [ { kind: Kind.ARGUMENT,
                          name:
                           { kind: Kind.NAME,
                             loc: { start: 9, end: 11, source },
                             value: 'id' },
                          value:
                           { kind: Kind.INT,
                             loc: { start: 13, end: 14, source },
                             value: '4' },
                          loc: { start: 9, end: 14, source } } ],
                     directives: [],
                     selectionSet:
                      { kind: Kind.SELECTION_SET,
                        loc: { start: 16, end: 38, source },
                        selections:
                         [ { kind: Kind.FIELD,
                             loc: { start: 22, end: 24, source },
                             alias: null,
                             name:
                              { kind: Kind.NAME,
                                loc: { start: 22, end: 24, source },
                                value: 'id' },
                             arguments: [],
                             directives: [],
                             selectionSet: null },
                           { kind: Kind.FIELD,
                             loc: { start: 30, end: 34, source },
                             alias: null,
                             name:
                              { kind: Kind.NAME,
                                loc: { start: 30, end: 34, source },
                                value: 'name' },
                             arguments: [],
                             directives: [],
                             selectionSet: null } ] } } ] } } ] }
    );
  });
});
<commit_msg>Add parser tests for accepting non-keywords<commit_after>/**
 *  Copyright (c) 2015, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

import * as Kind from '../kinds';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { parse } from '../parser';
import { Source } from '../source';
import { readFileSync } from 'fs';
import { join } from 'path';

describe('Parser', () => {

  it('accepts option to not include source', () => {
    expect(parse(`{ field }`, { noSource: true })).to.deep.equal({
      kind: 'Document',
      loc: { start: 0, end: 9 },
      definitions:
       [ { kind: 'OperationDefinition',
           loc: { start: 0, end: 9 },
           operation: 'query',
           name: null,
           variableDefinitions: null,
           directives: [],
           selectionSet: {
             kind: 'SelectionSet',
             loc: { start: 0, end: 9 },
             selections:
              [ { kind: 'Field',
                  loc: { start: 2, end: 7 },
                  alias: null,
                  name:
                   { kind: 'Name',
                     loc: { start: 2, end: 7 },
                     value: 'field' },
                  arguments: [],
                  directives: [],
                  selectionSet: null } ] } } ]
    });
  });

  it('parse provides useful errors', () => {

    expect(
      () => parse(
`{ ...MissingOn }
fragment MissingOn Type
`)
    ).to.throw('Syntax Error GraphQL (2:20) Expected "on", found Name "Type"');

    expect(
      () => parse('{ field: {} }')
    ).to.throw('Syntax Error GraphQL (1:10) Expected Name, found {');

    expect(
      () => parse('notanoperation Foo { field }')
    ).to.throw('Syntax Error GraphQL (1:1) Unexpected Name "notanoperation"');

    expect(
      () => parse('...')
    ).to.throw('Syntax Error GraphQL (1:1) Unexpected ...');

  });

  it('parse provides useful error when using source', () => {
    expect(
      () => parse(new Source('query', 'MyQuery.graphql'))
    ).to.throw('Syntax Error MyQuery.graphql (1:6) Expected Name, found EOF');
  });

  it('parses variable inline values', () => {
    expect(
      () => parse('{ field(complex: { a: { b: [ $var ] } }) }')
    ).to.not.throw();
  });

  it('parses constant default values', () => {
    expect(
      () => parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }')
    ).to.throw('Syntax Error GraphQL (1:37) Unexpected $');
  });

  it('duplicate keys in input object is syntax error', () => {
    expect(
      () => parse('{ field(arg: { a: 1, a: 2 }) }')
    ).to.throw('Syntax Error GraphQL (1:22) Duplicate input object field a.');
  });

  it('does not accept fragments named "on"', () => {
    expect(
      () => parse('fragment on on on { on }')
    ).to.throw('Syntax Error GraphQL (1:10) Unexpected Name "on"');
  });

  it('does not accept fragments spread of "on"', () => {
    expect(
      () => parse('{ ...on }')
    ).to.throw('Syntax Error GraphQL (1:9) Expected Name, found }');
  });

  it('does not allow null as value', async () => {
    expect(
      () => parse('{ fieldWithNullableStringInput(input: null) }')
    ).to.throw('Syntax Error GraphQL (1:39) Unexpected Name "null"');
  });

  var kitchenSink = readFileSync(
    join(__dirname, '/kitchen-sink.graphql'),
    { encoding: 'utf8' }
  );

  it('parses kitchen sink', () => {
    expect(() => parse(kitchenSink)).to.not.throw();
  });

  it('allows non-keywords anywhere a Name is allowed', () => {
    const nonKeywords = [
      'on',
      'fragment',
      'query',
      'mutation',
      'true',
      'false'
    ];
    nonKeywords.forEach((keyword) => {
      let fragmentName = keyword;
      // You can't define or reference a fragment named `on`.
      if (keyword === 'on') {
        fragmentName = 'a';
      }
      expect(() => {
        parse(`query ${keyword} {
  ... ${fragmentName}
  ... on ${keyword} { field }
}
fragment ${fragmentName} on Type {
  ${keyword}(${keyword}: $${keyword}) @${keyword}(${keyword}: ${keyword})
}`
        );
      }).to.not.throw();
    });
  });

  it('parse creates ast', () => {

    var source = new Source(`{
  node(id: 4) {
    id,
    name
  }
}
`);
    var result = parse(source);

    expect(result).to.deep.equal(
      { kind: Kind.DOCUMENT,
        loc: { start: 0, end: 41, source },
        definitions:
         [ { kind: Kind.OPERATION_DEFINITION,
             loc: { start: 0, end: 40, source },
             operation: 'query',
             name: null,
             variableDefinitions: null,
             directives: [],
             selectionSet:
              { kind: Kind.SELECTION_SET,
                loc: { start: 0, end: 40, source },
                selections:
                 [ { kind: Kind.FIELD,
                     loc: { start: 4, end: 38, source },
                     alias: null,
                     name:
                      { kind: Kind.NAME,
                        loc: { start: 4, end: 8, source },
                        value: 'node' },
                     arguments:
                      [ { kind: Kind.ARGUMENT,
                          name:
                           { kind: Kind.NAME,
                             loc: { start: 9, end: 11, source },
                             value: 'id' },
                          value:
                           { kind: Kind.INT,
                             loc: { start: 13, end: 14, source },
                             value: '4' },
                          loc: { start: 9, end: 14, source } } ],
                     directives: [],
                     selectionSet:
                      { kind: Kind.SELECTION_SET,
                        loc: { start: 16, end: 38, source },
                        selections:
                         [ { kind: Kind.FIELD,
                             loc: { start: 22, end: 24, source },
                             alias: null,
                             name:
                              { kind: Kind.NAME,
                                loc: { start: 22, end: 24, source },
                                value: 'id' },
                             arguments: [],
                             directives: [],
                             selectionSet: null },
                           { kind: Kind.FIELD,
                             loc: { start: 30, end: 34, source },
                             alias: null,
                             name:
                              { kind: Kind.NAME,
                                loc: { start: 30, end: 34, source },
                                value: 'name' },
                             arguments: [],
                             directives: [],
                             selectionSet: null } ] } } ] } } ] }
    );
  });
});
<|endoftext|>