<commit_before>var templates = {
	item: 'item'
	, list: 'list'
	, engine: 'jquery'
};

var values = [
	{ value1:"hej", value2:"hej2" }
	, { value1:"då", value2:"då2" }
	, { value1:"a", value2:12 }
	, { value1:"v", value2:3, value3:1 }
	, { value1:"b", value2:1, value3:1 }
	, { value1:"d", value2:43 }
	, { value1:"h", value2:234, value3:1337 }
	, { value1:"j", value2:11 }
	, { value1:"s", value2:1455 }
	, { value1:"3", value2:0 }
];

function List(id, templates, values) {
	this.listContainer = $('#'+id);
	this.templates = templates;
	this.items = [];
	this.list = null;
	this._init(values);
}

List.prototype = {
	_init: function(values) {
		$('#'+this.templates.list).tmpl({}).appendTo(this.listContainer);
		this.list = $('.list', this.listContainer);
		$('.search', this.listContainer).keyup($.proxy(this.search, this));
		$('.sort', this.listContainer).click($.proxy(this.sort, this));
		this.addExisting();
		this.add(values);
	},
	addExisting: function() {
		$('.item',this.list)
	},
	add: function(values, effect, position) {
		var self = this;
		var added = [];
		if (!$.isArray(values)){
			values = [values];
		}
		$(values).each(function(i) {
			var item = null;
			if (this instanceof self.Item) {
				item = this;
				item.reload();
			} else {
				item = new self.Item(this, self.templates.item);
			}
			if (effect) {
				item.elm.hide();
				self.list.append(item.elm);
				item.elm[effect]();
			} else {
				self.list.append(item.elm);
			}
			self.items.push(item);
			added.push(item);
		});
		return added;
	},
	remove: function(valueName, value, effect) {
		var self = this;
		$(this.items).each(function(i) {
			if (this.values[valueName] === value) {
				if (effect) {
					$(this.elm)[effect]($(this.elm).remove);
				} else {
					$(this.elm).remove();
				}
				self.items.splice(i,1);
				return true;
			}
		});
		return false;
	},
	get: function(valueName, value) {
		var item = [];
		$(this.items).each(function(i) {
			if (this.values[valueName] === value) {
				item.push(this);
			}
		});
		return item;
	},
	sort: function(event, sortFunction) {
		var self = this,
			length = this.items.length,
			value = null;
		if (event.target === 'undefined') {
			value = event;
		} else {
			value = $(event.target).attr('rel');
		}
		if (sortFunction) {
			sortFunction = sortFunction;
		} else {
			sortFunction = function(a, b) {
				return self._sorter._alphanum(a.values[value], b.values[value]);
			};
		}
		this.items.sort(sortFunction);
		this.list.html("");
		$(this.items).each(function(i) {
			self.list.append(this.elm);
		});
	},
	 var sorter = {
		_alphanum: function(a,b) {
			if (typeof a === 'undefined') {
				a = "";
			}
			if (typeof b === 'undefined') {
				b = "";
			}
			a = a.toString().replace(/&(lt|gt);/g, function (strMatch, p1){
	 		 	return (p1 == "lt")? "<" : ">";
	 		});
	 		a = a.replace(/<\/?[^>]+(>|$)/g, "");

			b = b.toString().replace(/&(lt|gt);/g, function (strMatch, p1){
	 		 	return (p1 == "lt")? "<" : ">";
	 		});
	 		b = b.replace(/<\/?[^>]+(>|$)/g, "");
			var aa = this._chunkify(a);
			var bb = this._chunkify(b);

			for (x = 0; aa[x] && bb[x]; x++) {
				if (aa[x] !== bb[x]) {
					var c = Number(aa[x]), d = Number(bb[x]);
					if (c == aa[x] && d == bb[x]) {
						return c - d;
					} else {
						return (aa[x] > bb[x]) ? 1 : -1;
					}
				}
			}
			return aa.length - bb.length;
		},
		_chunkify: function(t) {
			var tz = [], x = 0, y = -1, n = 0, i, j;

			while (i = (j = t.charAt(x++)).charCodeAt(0)) {
				var m = (i == 46 || (i >=48 && i <= 57));
				if (m !== n) {
					tz[++y] = "";
					n = m;
				}
				tz[y] += j;
			}
			return tz;
		}
	},
	search: function(event, columns) {
		var searchString = '';
		if (typeof event.target !== 'undefined') {
			searchString = event.target.value.toLowerCase();
		} else {
			searchString = event;
		}
		var useAllColumns = false;
		if (typeof columns === 'undefined') {
			useAllColumns = true;
		}
		$(this.items).each(function(i) {
			var found = false;
			if (useAllColumns) {
				columns = this.values;
			}
			for(var j in columns) {
				var text = columns[j].toString().toLowerCase();
				if ((searchString !== "") && (text.search(searchString) > -1)) {
					found = true;
				}
			}
			if (found || (searchString === "")) {
				this.elm.show();
			} else {
				this.elm.hide();
			}
		});
	},
	filter: function(filterFunction) {
		$(this.items).each(function(i) {
			if (filterFunction(this.values)) {
				this.elm.show();
			} else {
				this.elm.hide();
			}
		});
	},
	reload: function() {
		$(this.items).each(function(i) {
			this.reload();
		});
	},
	Item: function(values, template) {
		this._init(values,template);
	}
};

List.prototype.Item.prototype = {
	_init: function(values, template) {
		this.template = template;
		this.setValues(values);
	},
	setValues: function(values) {
		this.values = values;
		this.reload();
	},
	reload: function() {
		this.elm = $('#'+this.template).tmpl(this.values);
	}
};

<commit_msg>Now it works<commit_after>function List(id, templates, values) {
	var self = this;
	this.listContainer = $('#'+id);
	this.templates = templates;
	this.items = [];
	this.list = null;

	var init = function(values) {
		$('#'+self.templates.list).tmpl({}).appendTo(self.listContainer);
		self.list = $('.list', self.listContainer);
		$('.search', self.listContainer).keyup($.proxy(self.search, self));
		$('.sort', self.listContainer).click($.proxy(self.sort, self));
		//self.addExisting();
		self.add(values);
	};

	this.addExisting = function() {
		//$('.item',this.list)
	};

	/* Add object to list
	 *
	 */
	this.add = function(values, effect, position) {
		var added = [];
		if (!$.isArray(values)){
			values = [values];
		}
		$(values).each(function(i) {
			var item = null;
			if (this instanceof Item) {
				item = this;
				item.reload();
			} else {
				item = new Item(this, self.templates.item);
			}
			if (effect) {
				item.elm.hide();
				self.list.append(item.elm);
				item.elm[effect]();
			} else {
				self.list.append(item.elm);
			}
			self.items.push(item);
			added.push(item);
		});
		return added;
	};

	/* Removes object from list.
	 * Loops through the list and removes objects where
	 * property "valuename" === value
	 */
	this.remove = function(valueName, value, effect) {
		$(this.items).each(function(i) {
			if (this.values[valueName] === value) {
				if (effect) {
					$(this.elm)[effect]($(this.elm).remove);
				} else {
					$(this.elm).remove();
				}
				self.items.splice(i,1);
				return true;
			}
		});
		return false;
	};

	/* Gets the objects in the list which
	 * property "valueName" === value
	 */
	this.get = function(valueName, value) {
		var item = [];
		$(this.items).each(function(i) {
			if (this.values[valueName] === value) {
				item.push(this);
			}
		});
		return item;
	};

	/* Sorts the list
	 *
	 */
	this.sort = function(event, sortFunction) {
		var length = this.items.length,
			value = null;
		if (event.target === 'undefined') {
			value = event;
		} else {
			value = $(event.target).attr('rel');
		}
		if (sortFunction) {
			sortFunction = sortFunction;
		} else {
			sortFunction = function(a, b) {
				return sorter.alphanum(a.values[value], b.values[value]);
			};
		}
		this.items.sort(sortFunction);
		this.list.html("");
		$(this.items).each(function(i) {
			self.list.append(this.elm);
		});
	};

	var sorter = {
		alphanum: function(a,b) {
			if (typeof a === 'undefined') {
				a = "";
			}
			if (typeof b === 'undefined') {
				b = "";
			}
			a = a.toString().replace(/&(lt|gt);/g, function (strMatch, p1){
	 		 	return (p1 == "lt")? "<" : ">";
	 		});
	 		a = a.replace(/<\/?[^>]+(>|$)/g, "");

			b = b.toString().replace(/&(lt|gt);/g, function (strMatch, p1){
	 		 	return (p1 == "lt")? "<" : ">";
	 		});
	 		b = b.replace(/<\/?[^>]+(>|$)/g, "");
			var aa = this.chunkify(a);
			var bb = this.chunkify(b);

			for (x = 0; aa[x] && bb[x]; x++) {
				if (aa[x] !== bb[x]) {
					var c = Number(aa[x]), d = Number(bb[x]);
					if (c == aa[x] && d == bb[x]) {
						return c - d;
					} else {
						return (aa[x] > bb[x]) ? 1 : -1;
					}
				}
			}
			return aa.length - bb.length;
		},
		chunkify: function(t) {
			var tz = [], x = 0, y = -1, n = 0, i, j;

			while (i = (j = t.charAt(x++)).charCodeAt(0)) {
				var m = (i == 46 || (i >=48 && i <= 57));
				if (m !== n) {
					tz[++y] = "";
					n = m;
				}
				tz[y] += j;
			}
			return tz;
		}
	};

	this.search = function(event, columns) {
		var searchString = '';
		if (typeof event.target !== 'undefined') {
			searchString = event.target.value.toLowerCase();
		} else {
			searchString = event;
		}
		var useAllColumns = false;
		if (typeof columns === 'undefined') {
			useAllColumns = true;
		}
		$(this.items).each(function(i) {
			var found = false;
			if (useAllColumns) {
				columns = this.values;
			}
			for(var j in columns) {
				var text = columns[j].toString().toLowerCase();
				if ((searchString !== "") && (text.search(searchString) > -1)) {
					found = true;
				}
			}
			if (found || (searchString === "")) {
				this.elm.show();
			} else {
				this.elm.hide();
			}
		});
	};

	this.filter = function(filterFunction) {
		$(this.items).each(function(i) {
			if (filterFunction(this.values)) {
				this.elm.show();
			} else {
				this.elm.hide();
			}
		});
	};

	this.reload = function() {
		$(this.items).each(function(i) {
			this.reload();
		});
	};

	var Item = function(values, template) {
		var item = this;

		var init = function(values, template) {
			item.template = template;
			item.setValues(values);
		};
		this.setValues = function(values) {
			item.values = values;
			item.reload();
		};
		this.reload = function() {
			item.elm = $('#'+this.template).tmpl(this.values);
		};
		init(values, template);
	};

	init(values);
};

<|endoftext|>


<commit_before>/*
 * Copyright (C) 2012 by CoNarrative
 */
/**
 * @class glu.List
 * An observable list of values or objects as a much lighter alternative to a store
 * The objects / values are not processed in any way (i.e. are not converted into Records or Models)
 * Notifications are raised when the list itself is manipulated.
 *
 */
glu.List = glu.extend(Object, {
    constructor:function (config) {
        config = config || {};
        glu.deepApply(this, config);
        this.length = 0;
        this._private = this._private || {};
        this._private.objs = [];
        this._private.observable = this._private.observable || new glu.GraphObservable({vm:this});
        config.items = config.items || config.data || [];
        for (var i = 0; i < config.items.length; i++) {
            var item = config.items[i];
            this.add(item, true);
        }
        delete this.items;
    },
    /**
     * Adds an item to the list
     * @param obj
     * @param silent
     */
    add:function (obj, silent) {
        this.insert(this.length,obj);
    },
    /**
     * Inserts an item at an ordinal position
     * @param index
     * @param obj
     */
    insert:function (index, obj) {
        if (obj.parentVM && obj.parentVM!==this.parentVM) {
            throw "View model already has a parent and needs to be removed from there first";
        }
        if (glu.isObject(obj) && obj.mtype ) {
            if (obj._private===undefined) {
                obj.ns = obj.ns || this.ns;
                obj.parentVM = this.parentVM;
                obj.parentList = this;
                obj = glu.model(obj);
            }
            obj.parentList = this;
            //            obj.referenceName = this.referenceName + '[x]';
            obj._ob.attach('parentVM');
            obj._ob.attach('rootVM')
        }
        this._private.objs.splice(index, 0, obj);
        this.length++;
        this.fireEvent('lengthchanged',this.length,this.length-1);
        this.fireEvent('added', obj, index);
    },
    /**
     * Removes an item by reference
     * @param Obj
     * @return {*}
     */
    remove:function (Obj) {
        return this.removeAt(this.indexOf(Obj));
    },
    /**
     * Removes an item by ordinal position
     * @param index
     * @return {*}
     */
    removeAt:function (index) {
        var obj = this.getAt(index);
        this._private.objs.splice(index, 1);
        if (obj._ob) {
            //remove from observation graph...since it can only go child-> parent don't worry about other direction
            obj._ob.detach('parentVM');
            obj._ob.detach('rootVM');
        }
        this.fireEvent('removed', obj, index);
        if (index < this.activeIndex) {
            this.setActiveIndex(this.getActiveIndex() - 1);
        }
        this.length--;
        this.fireEvent('lengthchanged',this.length,this.length+1);
        return obj;
    },
    /**
     * Removes all items
     */
    removeAll:function () {
        while (this.length > 0) {
            this.removeAt(0);
        }
    },

    /**
     * Returns the ordinal index of an item
     * @param obj
     * @return {Number}
     */
    indexOf:function (obj) {
        for (var i = 0; i < this._private.objs.length; i++) {
            if (obj === this._private.objs[i]) {
                return i;
            }
        }
        return -1;
    },

    /**
     * Whether or not the supplied item is in the container
     * @param Obj
     * @return {Boolean}
     */
    contains:function (Obj) {
        return this.indexOf(Obj) > -1;
    },
    /**
     * Fetches the item at a given ordinal position
     * @param index
     * @return {*}
     */
    getAt:function (index) {
        return this._private.objs[index];
    },
    /**
     * The total number of items in the container
     * @property {Number}
     */
    length : 0,
    /**
     * An alias for length()
     * @return {Number}
     */
    getCount:function () {
        return this._private.objs.length;
    },
    /**
     * Iterates through each item in the list and applies the function
     * @param operation
     * @param scope
     */
    foreach:function (operation, scope) {
        for (var i = 0; i < this.length; i++) {
            var item = this.getAt(i);
            var myScope = scope || item;
            operation.call(myScope, item, i);
        }
    },

    /**
     * Returns a single item using a custom finder
     * @param fn
     * @param scope
     * @return {*}
     */
    find:function (fn, scope) {
        for (var i = 0; i < this.length; i++) {
            var item = this.getAt(i);
            var myScope = scope || item;
            if (fn.call(myScope, item)) {
                return item;
            }
        }
    },

    //TODO: Put in glu observable mixin
    on:function (eventName, handler, scope) {
        if( Ext.isObject(eventName) ){
            scope = eventName.scope || this;
            for( var event in eventName ){
                this._private.observable.on(event, eventName[event], scope);
            }
        }
        else{
            scope = scope || this;
            this._private.observable.on(eventName, handler, scope);
        }
    },
    fireEvent:function () {
        glu.log.info('List "' + this.referenceName + '" is firing event "' + arguments[0] + '""');
        this._private.observable.fireEvent.apply(this._private.observable, arguments);
    }
});

glu.mreg('list', glu.List);

glu.mreg('keytracking',{
    initMixin:function(){
        this.keyMap ={};
        this.idProperty = this.idProperty || 'id';
        this.on('added',this.addKey)
        this.on('removed',this.removeKey)
    },
    addKey:function(item, idx){
        var key=item[this.idProperty];
        if (key===undefined) return;
        this.keyMap[key] = item;
    },
    removeKey:function(item){
        var key=item[this.idProperty];
        if (key===undefined) return;
        delete this.keyMap[key];
    },
    containsKey:function(key){
        return this.keyMap[key]!==undefined;
    }
})

glu.List.prototype.where = function(filter) {
    var f = [];
    for (var i = 0; i < this.length; i++) {
        var item = this.getAt(i);
        if (filter.call(this.parentVM, item)) f.push(item);
    }
    return f;
}

glu.List.prototype.count = function(filter) {
    return this.where(filter).length;
}

glu.List.prototype.any = function(filter) {
    return this.where(filter).length > 0;
}
<commit_msg>Added getById to keytracker mixin<commit_after>/*
 * Copyright (C) 2012 by CoNarrative
 */
/**
 * @class glu.List
 * An observable list of values or objects as a much lighter alternative to a store
 * The objects / values are not processed in any way (i.e. are not converted into Records or Models)
 * Notifications are raised when the list itself is manipulated.
 *
 */
glu.List = glu.extend(Object, {
    constructor:function (config) {
        config = config || {};
        glu.deepApply(this, config);
        this.length = 0;
        this._private = this._private || {};
        this._private.objs = [];
        this._private.observable = this._private.observable || new glu.GraphObservable({vm:this});
        config.items = config.items || config.data || [];
        for (var i = 0; i < config.items.length; i++) {
            var item = config.items[i];
            this.add(item, true);
        }
        delete this.items;
    },
    /**
     * Adds an item to the list
     * @param obj
     * @param silent
     */
    add:function (obj, silent) {
        this.insert(this.length,obj);
    },
    /**
     * Inserts an item at an ordinal position
     * @param index
     * @param obj
     */
    insert:function (index, obj) {
        if (obj.parentVM && obj.parentVM!==this.parentVM) {
            throw "View model already has a parent and needs to be removed from there first";
        }
        if (glu.isObject(obj) && obj.mtype ) {
            if (obj._private===undefined) {
                obj.ns = obj.ns || this.ns;
                obj.parentVM = this.parentVM;
                obj.parentList = this;
                obj = glu.model(obj);
            }
            obj.parentList = this;
            //            obj.referenceName = this.referenceName + '[x]';
            obj._ob.attach('parentVM');
            obj._ob.attach('rootVM')
        }
        this._private.objs.splice(index, 0, obj);
        this.length++;
        this.fireEvent('lengthchanged',this.length,this.length-1);
        this.fireEvent('added', obj, index);
    },
    /**
     * Removes an item by reference
     * @param Obj
     * @return {*}
     */
    remove:function (Obj) {
        return this.removeAt(this.indexOf(Obj));
    },
    /**
     * Removes an item by ordinal position
     * @param index
     * @return {*}
     */
    removeAt:function (index) {
        var obj = this.getAt(index);
        this._private.objs.splice(index, 1);
        if (obj._ob) {
            //remove from observation graph...since it can only go child-> parent don't worry about other direction
            obj._ob.detach('parentVM');
            obj._ob.detach('rootVM');
        }
        this.fireEvent('removed', obj, index);
        if (index < this.activeIndex) {
            this.setActiveIndex(this.getActiveIndex() - 1);
        }
        this.length--;
        this.fireEvent('lengthchanged',this.length,this.length+1);
        return obj;
    },
    /**
     * Removes all items
     */
    removeAll:function () {
        while (this.length > 0) {
            this.removeAt(0);
        }
    },

    /**
     * Returns the ordinal index of an item
     * @param obj
     * @return {Number}
     */
    indexOf:function (obj) {
        for (var i = 0; i < this._private.objs.length; i++) {
            if (obj === this._private.objs[i]) {
                return i;
            }
        }
        return -1;
    },

    /**
     * Whether or not the supplied item is in the container
     * @param Obj
     * @return {Boolean}
     */
    contains:function (Obj) {
        return this.indexOf(Obj) > -1;
    },
    /**
     * Fetches the item at a given ordinal position
     * @param index
     * @return {*}
     */
    getAt:function (index) {
        return this._private.objs[index];
    },
    /**
     * The total number of items in the container
     * @property {Number}
     */
    length : 0,
    /**
     * An alias for length()
     * @return {Number}
     */
    getCount:function () {
        return this._private.objs.length;
    },
    /**
     * Iterates through each item in the list and applies the function
     * @param operation
     * @param scope
     */
    foreach:function (operation, scope) {
        for (var i = 0; i < this.length; i++) {
            var item = this.getAt(i);
            var myScope = scope || item;
            operation.call(myScope, item, i);
        }
    },

    /**
     * Returns a single item using a custom finder
     * @param fn
     * @param scope
     * @return {*}
     */
    find:function (fn, scope) {
        for (var i = 0; i < this.length; i++) {
            var item = this.getAt(i);
            var myScope = scope || item;
            if (fn.call(myScope, item)) {
                return item;
            }
        }
    },

    //TODO: Put in glu observable mixin
    on:function (eventName, handler, scope) {
        if( Ext.isObject(eventName) ){
            scope = eventName.scope || this;
            for( var event in eventName ){
                this._private.observable.on(event, eventName[event], scope);
            }
        }
        else{
            scope = scope || this;
            this._private.observable.on(eventName, handler, scope);
        }
    },
    fireEvent:function () {
        glu.log.info('List "' + this.referenceName + '" is firing event "' + arguments[0] + '""');
        this._private.observable.fireEvent.apply(this._private.observable, arguments);
    }
});

glu.mreg('list', glu.List);

glu.mreg('keytracking',{
    initMixin:function(){
        this.keyMap ={};
        this.idProperty = this.idProperty || 'id';
        this.on('added',this.addKey)
        this.on('removed',this.removeKey)
    },
    addKey:function(item, idx){
        var key=item[this.idProperty];
        if (key===undefined) return;
        this.keyMap[key] = item;
    },
    removeKey:function(item){
        var key=item[this.idProperty];
        if (key===undefined) return;
        delete this.keyMap[key];
    },
    containsKey:function(key){
        return this.keyMap[key]!==undefined;
    },
    getById:function(key){
        return this.keyMap[key];
    }
})

glu.List.prototype.where = function(filter) {
    var f = [];
    for (var i = 0; i < this.length; i++) {
        var item = this.getAt(i);
        if (filter.call(this.parentVM, item)) f.push(item);
    }
    return f;
}

glu.List.prototype.count = function(filter) {
    return this.where(filter).length;
}

glu.List.prototype.any = function(filter) {
    return this.where(filter).length > 0;
}
<|endoftext|>


<commit_before>/**
 * Asynchronous read/write lock implementation for Node.
 *
 * The rules:
 * - there may be zero or more readers at the same time,
 * - there may be only one writer at a time,
 * - if there is a writer there may be no readers.
 *
 * ReadWriteLock also supports multiple independent locks identified by custom
 * user-defined strings called "keys".
 *
 * @class ReadWriteLock
 * @constructor
 */
module.exports = function () {
	'use strict';

	function Lock() {
		this.readers = 0;
		this.queue = [];
	}

	var defaultLock = new Lock();
	var table = {};

	/**
	 * Acquires a read lock and invokes a user-defined callback as soon as it is
	 * acquired.
	 *
	 * The operation might require some time as there may be a writer. You can
	 * optionally specify a timeout in milliseconds: if it expires before a read
	 * lock can be acquired, this request is canceled and no lock will be
	 * acquired.
	 *
	 * The `key` argument allows you to work on a specific lock; omitting it
	 * will request the default lock.
	 *
	 * @method readLock
	 * @param [key] {String} The name of the lock to read-acquire. The default
	 * lock will be requested if no key is specified.
	 * @param callback {Function} A user-defined function invoked as soon as a
	 * read lock is acquired.
	 * @param callback.release {Function} A function that releases the lock.
	 *
	 * This must be called by the ReadWriteLock user at some point, otherwise
	 * the read lock will remain and prevent any writers from operating. Anyway
	 * you do not necessarily need to call it inside the `callback` function:
	 * you can save a reference to the `release` function and call it later.
	 * @param [options] {Object} Further optional settings.
	 * @param [options.scope] {Object} An optional object to use as `this` when
	 * calling the `callback` function.
	 * @param [options.timeout] {Number} A timeout in milliseconds within which
	 * the lock must be acquired; if a writer is still operating and the timeout
	 * expires the request is canceled and no lock is acquired.
	 * @param [options.timeoutCallback] {Function} An optional user-defined
	 * callback function that gets invokes in case the timeout expires before
	 * the lock can be acquired.
	 */
	function readLock(key, callback, options) {
		var lock;
		if (typeof key !== 'function') {
			if (!table.hasOwnProperty(key)) {
				table[key] = new Lock();
			}
			lock = table[key];
		} else {
			options = callback;
			callback = key;
			lock = defaultLock;
		}
		if (!options) {
			options = {};
		}
		var scope = null;
		if (options.hasOwnProperty('scope')) {
			scope = options.scope;
		}
		var release = (function () {
			var released = false;
			return function () {
				if (!released) {
					released = true;
					lock.readers--;
					if (lock.queue.length) {
						lock.queue[0]();
					}
				}
			};
		}());
		if ((lock.readers < 0) || lock.queue.length) {
			var terminated = false;
			lock.queue.push(function () {
				if (!terminated && (lock.readers >= 0)) {
					terminated = true;
					lock.queue.shift();
					lock.readers++;
					callback.call(scope, release);
					if (lock.queue.length) {
						lock.queue[0]();
					}
				}
			});
			if (options.hasOwnProperty('timeout')) {
				var timeoutCallback = null;
				if (options.hasOwnProperty('timeoutCallback')) {
					timeoutCallback = options.timeoutCallback;
				}
				setTimeout(function () {
					if (!terminated) {
						terminated = true;
						lock.queue.shift();
						if (timeoutCallback) {
							timeoutCallback.call(options.scope);
						}
					}
				}, options.timeout);
			}
		} else {
			lock.readers++;
			callback.call(options.scope, release);
		}
	}

	/**
	 * Acquires a write lock and invokes a user-defined callback as soon as it
	 * is acquired.
	 *
	 * The operation might require some time as there may be one or more
	 * readers. You can optionally specify a timeout in milliseconds: if it
	 * expires before a read lock can be acquired, this request is canceled and
	 * no lock will be acquired.
	 *
	 * The `key` argument allows you to work on a specific lock; omitting it
	 * will request the default lock.
	 *
	 * @method writeLock
	 * @param [key] {String} The name of the lock to write-acquire. The default
	 * lock will be requested if no key is specified.
	 * @param callback {Function} A user-defined function invoked as soon as a
	 * write lock is acquired.
	 * @param callback.release {Function} A function that releases the lock.
	 *
	 * This must be called by the ReadWriteLock user at some point, otherwise
	 * the write lock will remain and prevent future readers from operating.
	 * Anyway you do not necessarily need to call it inside the `callback`
	 * function: you can save a reference to the `release` function and call it
	 * later.
	 * @param [options] {Object} Further optional settings.
	 * @param [options.scope] {Object} An optional object to use as `this` when
	 * calling the `callback` function.
	 * @param [options.timeout] {Number} A timeout in milliseconds within which
	 * the lock must be acquired; if one ore more readers are still operating
	 * and the timeout expires the request is canceled and no lock is acquired.
	 * @param [options.timeoutCallback] {Function} An optional user-defined
	 * callback function that gets invokes in case the timeout expires before
	 * the lock can be acquired.
	 */
	function writeLock(key, callback, options) {
		var lock;
		if (typeof key !== 'function') {
			if (!table.hasOwnProperty(key)) {
				table[key] = new Lock();
			}
			lock = table[key];
		} else {
			options = callback;
			callback = key;
			lock = defaultLock;
		}
		if (!options) {
			options = {};
		}
		var scope = null;
		if (options.hasOwnProperty('scope')) {
			scope = options.scope;
		}
		var release = (function () {
			var released = false;
			return function () {
				if (!released) {
					released = true;
					lock.readers = 0;
					if (lock.queue.length) {
						lock.queue[0]();
					}
				}
			};
		}());
		if (lock.readers || lock.queue.length) {
			var terminated = false;
			lock.queue.push(function () {
				if (!terminated && !lock.readers) {
					terminated = true;
					lock.queue.shift();
					lock.readers = -1;
					callback.call(options.scope, release);
				}
			});
			if (options.hasOwnProperty('timeout')) {
				var timeoutCallback = null;
				if (options.hasOwnProperty('timeoutCallback')) {
					timeoutCallback = options.timeoutCallback;
				}
				setTimeout(function () {
					if (!terminated) {
						terminated = true;
						lock.queue.shift();
						if (timeoutCallback) {
							timeoutCallback.call(scope);
						}
					}
				}, options.timeout);
			}
		} else {
			lock.readers = -1;
			callback.call(options.scope, release);
		}
	}

	this.readLock = readLock;
	this.writeLock = writeLock;

	this.async = {
		/**
		 * TODO
		 *
		 * @method async.readLock
		 * @param [key] {String} The name of the lock to read-acquire. The
		 * default lock will be requested if no key is specified.
		 * @param callback {Function} A user-defined function invoked as soon as
		 * a read lock is acquired.
		 * @param callback.release {Function} A function that releases the lock.
		 *
		 * This must be called by the ReadWriteLock user at some point,
		 * otherwise the read lock will remain and prevent any writers from
		 * operating. Anyway you do not necessarily need to call it inside the
		 * `callback` function: you can save a reference to the `release`
		 * function and call it later.
		 * @param [options] {Object} Further optional settings.
		 * @param [options.scope] {Object} An optional object to use as `this`
		 * when calling the `callback` function.
		 * @param [options.timeout] {Number} A timeout in milliseconds within
		 * which the lock must be acquired; if a writer is still operating and
		 * the timeout expires the request is canceled and no lock is acquired.
		 * @param [options.timeoutCallback] {Function} An optional user-defined
		 * callback function that gets invokes in case the timeout expires
		 * before the lock can be acquired.
		 */
		readLock: function (key, callback, options) {
			if (typeof key !== 'function') {
				readLock(key, function (release) {
					callback.call(this, null, release);
				}, options);
			} else {
				callback = key;
				options = callback;
				readLock(function (release) {
					callback.call(this, null, release);
				}, options);
			}
		},

		/**
		 * TODO
		 *
		 * @method async.writeLock
		 * @param [key] {String} The name of the lock to write-acquire. The
		 * default lock will be requested if no key is specified.
		 * @param callback {Function} A user-defined function invoked as soon as
		 * a write lock is acquired.
		 * @param callback.release {Function} A function that releases the lock.
		 *
		 * This must be called by the ReadWriteLock user at some point,
		 * otherwise the write lock will remain and prevent future readers from
		 * operating. Anyway you do not necessarily need to call it inside the
		 * `callback` function: you can save a reference to the `release`
		 * function and call it later.
		 * @param [options] {Object} Further optional settings.
		 * @param [options.scope] {Object} An optional object to use as `this`
		 * when calling the `callback` function.
		 * @param [options.timeout] {Number} A timeout in milliseconds within
		 * which the lock must be acquired; if one ore more readers are still
		 * operating and the timeout expires the request is canceled and no lock
		 * is acquired.
		 * @param [options.timeoutCallback] {Function} An optional user-defined
		 * callback function that gets invokes in case the timeout expires
		 * before the lock can be acquired.
		 */
		writeLock: function (key, callback, options) {
			if (typeof key !== 'function') {
				writeLock(key, function (release) {
					callback.call(this, null, release);
				}, options);
			} else {
				callback = key;
				options = callback;
				writeLock(function (release) {
					callback.call(this, null, release);
				}, options);
			}
		}
	};
};
<commit_msg>fixing typo in lock.js<commit_after>/**
 * Asynchronous read/write lock implementation for Node.
 *
 * The rules:
 * - there may be zero or more readers at the same time,
 * - there may be only one writer at a time,
 * - if there is a writer there may be no readers.
 *
 * ReadWriteLock also supports multiple independent locks identified by custom
 * user-defined strings called "keys".
 *
 * @class ReadWriteLock
 * @constructor
 */
module.exports = function () {
	'use strict';

	function Lock() {
		this.readers = 0;
		this.queue = [];
	}

	var defaultLock = new Lock();
	var table = {};

	/**
	 * Acquires a read lock and invokes a user-defined callback as soon as it is
	 * acquired.
	 *
	 * The operation might require some time as there may be a writer. You can
	 * optionally specify a timeout in milliseconds: if it expires before a read
	 * lock can be acquired, this request is canceled and no lock will be
	 * acquired.
	 *
	 * The `key` argument allows you to work on a specific lock; omitting it
	 * will request the default lock.
	 *
	 * @method readLock
	 * @param [key] {String} The name of the lock to read-acquire. The default
	 * lock will be requested if no key is specified.
	 * @param callback {Function} A user-defined function invoked as soon as a
	 * read lock is acquired.
	 * @param callback.release {Function} A function that releases the lock.
	 *
	 * This must be called by the ReadWriteLock user at some point, otherwise
	 * the read lock will remain and prevent any writers from operating. Anyway
	 * you do not necessarily need to call it inside the `callback` function:
	 * you can save a reference to the `release` function and call it later.
	 * @param [options] {Object} Further optional settings.
	 * @param [options.scope] {Object} An optional object to use as `this` when
	 * calling the `callback` function.
	 * @param [options.timeout] {Number} A timeout in milliseconds within which
	 * the lock must be acquired; if a writer is still operating and the timeout
	 * expires the request is canceled and no lock is acquired.
	 * @param [options.timeoutCallback] {Function} An optional user-defined
	 * callback function that gets invokes in case the timeout expires before
	 * the lock can be acquired.
	 */
	function readLock(key, callback, options) {
		var lock;
		if (typeof key !== 'function') {
			if (!table.hasOwnProperty(key)) {
				table[key] = new Lock();
			}
			lock = table[key];
		} else {
			options = callback;
			callback = key;
			lock = defaultLock;
		}
		if (!options) {
			options = {};
		}
		var scope = null;
		if (options.hasOwnProperty('scope')) {
			scope = options.scope;
		}
		var release = (function () {
			var released = false;
			return function () {
				if (!released) {
					released = true;
					lock.readers--;
					if (lock.queue.length) {
						lock.queue[0]();
					}
				}
			};
		}());
		if ((lock.readers < 0) || lock.queue.length) {
			var terminated = false;
			lock.queue.push(function () {
				if (!terminated && (lock.readers >= 0)) {
					terminated = true;
					lock.queue.shift();
					lock.readers++;
					callback.call(scope, release);
					if (lock.queue.length) {
						lock.queue[0]();
					}
				}
			});
			if (options.hasOwnProperty('timeout')) {
				var timeoutCallback = null;
				if (options.hasOwnProperty('timeoutCallback')) {
					timeoutCallback = options.timeoutCallback;
				}
				setTimeout(function () {
					if (!terminated) {
						terminated = true;
						lock.queue.shift();
						if (timeoutCallback) {
							timeoutCallback.call(options.scope);
						}
					}
				}, options.timeout);
			}
		} else {
			lock.readers++;
			callback.call(options.scope, release);
		}
	}

	/**
	 * Acquires a write lock and invokes a user-defined callback as soon as it
	 * is acquired.
	 *
	 * The operation might require some time as there may be one or more
	 * readers. You can optionally specify a timeout in milliseconds: if it
	 * expires before a read lock can be acquired, this request is canceled and
	 * no lock will be acquired.
	 *
	 * The `key` argument allows you to work on a specific lock; omitting it
	 * will request the default lock.
	 *
	 * @method writeLock
	 * @param [key] {String} The name of the lock to write-acquire. The default
	 * lock will be requested if no key is specified.
	 * @param callback {Function} A user-defined function invoked as soon as a
	 * write lock is acquired.
	 * @param callback.release {Function} A function that releases the lock.
	 *
	 * This must be called by the ReadWriteLock user at some point, otherwise
	 * the write lock will remain and prevent future readers from operating.
	 * Anyway you do not necessarily need to call it inside the `callback`
	 * function: you can save a reference to the `release` function and call it
	 * later.
	 * @param [options] {Object} Further optional settings.
	 * @param [options.scope] {Object} An optional object to use as `this` when
	 * calling the `callback` function.
	 * @param [options.timeout] {Number} A timeout in milliseconds within which
	 * the lock must be acquired; if one or more readers are still operating
	 * and the timeout expires the request is canceled and no lock is acquired.
	 * @param [options.timeoutCallback] {Function} An optional user-defined
	 * callback function that gets invokes in case the timeout expires before
	 * the lock can be acquired.
	 */
	function writeLock(key, callback, options) {
		var lock;
		if (typeof key !== 'function') {
			if (!table.hasOwnProperty(key)) {
				table[key] = new Lock();
			}
			lock = table[key];
		} else {
			options = callback;
			callback = key;
			lock = defaultLock;
		}
		if (!options) {
			options = {};
		}
		var scope = null;
		if (options.hasOwnProperty('scope')) {
			scope = options.scope;
		}
		var release = (function () {
			var released = false;
			return function () {
				if (!released) {
					released = true;
					lock.readers = 0;
					if (lock.queue.length) {
						lock.queue[0]();
					}
				}
			};
		}());
		if (lock.readers || lock.queue.length) {
			var terminated = false;
			lock.queue.push(function () {
				if (!terminated && !lock.readers) {
					terminated = true;
					lock.queue.shift();
					lock.readers = -1;
					callback.call(options.scope, release);
				}
			});
			if (options.hasOwnProperty('timeout')) {
				var timeoutCallback = null;
				if (options.hasOwnProperty('timeoutCallback')) {
					timeoutCallback = options.timeoutCallback;
				}
				setTimeout(function () {
					if (!terminated) {
						terminated = true;
						lock.queue.shift();
						if (timeoutCallback) {
							timeoutCallback.call(scope);
						}
					}
				}, options.timeout);
			}
		} else {
			lock.readers = -1;
			callback.call(options.scope, release);
		}
	}

	this.readLock = readLock;
	this.writeLock = writeLock;

	this.async = {
		/**
		 * TODO
		 *
		 * @method async.readLock
		 * @param [key] {String} The name of the lock to read-acquire. The
		 * default lock will be requested if no key is specified.
		 * @param callback {Function} A user-defined function invoked as soon as
		 * a read lock is acquired.
		 * @param callback.release {Function} A function that releases the lock.
		 *
		 * This must be called by the ReadWriteLock user at some point,
		 * otherwise the read lock will remain and prevent any writers from
		 * operating. Anyway you do not necessarily need to call it inside the
		 * `callback` function: you can save a reference to the `release`
		 * function and call it later.
		 * @param [options] {Object} Further optional settings.
		 * @param [options.scope] {Object} An optional object to use as `this`
		 * when calling the `callback` function.
		 * @param [options.timeout] {Number} A timeout in milliseconds within
		 * which the lock must be acquired; if a writer is still operating and
		 * the timeout expires the request is canceled and no lock is acquired.
		 * @param [options.timeoutCallback] {Function} An optional user-defined
		 * callback function that gets invokes in case the timeout expires
		 * before the lock can be acquired.
		 */
		readLock: function (key, callback, options) {
			if (typeof key !== 'function') {
				readLock(key, function (release) {
					callback.call(this, null, release);
				}, options);
			} else {
				callback = key;
				options = callback;
				readLock(function (release) {
					callback.call(this, null, release);
				}, options);
			}
		},

		/**
		 * TODO
		 *
		 * @method async.writeLock
		 * @param [key] {String} The name of the lock to write-acquire. The
		 * default lock will be requested if no key is specified.
		 * @param callback {Function} A user-defined function invoked as soon as
		 * a write lock is acquired.
		 * @param callback.release {Function} A function that releases the lock.
		 *
		 * This must be called by the ReadWriteLock user at some point,
		 * otherwise the write lock will remain and prevent future readers from
		 * operating. Anyway you do not necessarily need to call it inside the
		 * `callback` function: you can save a reference to the `release`
		 * function and call it later.
		 * @param [options] {Object} Further optional settings.
		 * @param [options.scope] {Object} An optional object to use as `this`
		 * when calling the `callback` function.
		 * @param [options.timeout] {Number} A timeout in milliseconds within
		 * which the lock must be acquired; if one or more readers are still
		 * operating and the timeout expires the request is canceled and no lock
		 * is acquired.
		 * @param [options.timeoutCallback] {Function} An optional user-defined
		 * callback function that gets invokes in case the timeout expires
		 * before the lock can be acquired.
		 */
		writeLock: function (key, callback, options) {
			if (typeof key !== 'function') {
				writeLock(key, function (release) {
					callback.call(this, null, release);
				}, options);
			} else {
				callback = key;
				options = callback;
				writeLock(function (release) {
					callback.call(this, null, release);
				}, options);
			}
		}
	};
};
<|endoftext|>


<commit_before>import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { RocketChat } from 'meteor/rocketchat:lib';
import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat';

RocketChat.API.v1.addRoute('livechat/agent.info/:rid/:token', {
	get() {
		try {
			check(this.urlParams, {
				rid: String,
				token: String,
			});

			const visitor = findGuest(this.urlParams.token);
			if (!visitor) {
				throw new Meteor.Error('invalid-token');
			}

			const room = findRoom(this.urlParams.token, this.urlParams.rid);
			if (!room) {
				throw new Meteor.Error('invalid-room');
			}

			const agent = room && room.servedBy && findAgent(room.servedBy._id);
			if (!agent) {
				throw new Meteor.Error('invalid-agent');
			}

			return RocketChat.API.v1.success({ agent });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});

RocketChat.API.v1.addRoute('livechat/agent.next/:token', {
	get() {
		try {
			check(this.urlParams, {
				token: String,
			});

			check(this.queryParams, {
				department: Match.Maybe(String),
			});

			const { token } = this.urlParams;
			const room = findOpenRoom(token);
			if (room) {
				throw new Meteor.Error('invalid-token');
			}

			let { department } = this.queryParams;
			if (!department) {
				const requireDeparment = RocketChat.Livechat.getRequiredDepartment();
				if (requireDeparment) {
					department = requireDeparment._id;
				}
			}

			const agentData = RocketChat.Livechat.getNextAgent(department);
			if (!agentData) {
				throw new Meteor.Error('agent-not-found');
			}

			const agent = findAgent(agentData.agentId);
			if (!agent) {
				throw new Meteor.Error('invalid-agent');
			}

			return RocketChat.API.v1.success({ agent });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});
<commit_msg>[FIX] Invalid condition on getting next livechat agent over REST API endpoint (#13360)<commit_after>import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { RocketChat } from 'meteor/rocketchat:lib';
import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat';

RocketChat.API.v1.addRoute('livechat/agent.info/:rid/:token', {
	get() {
		try {
			check(this.urlParams, {
				rid: String,
				token: String,
			});

			const visitor = findGuest(this.urlParams.token);
			if (!visitor) {
				throw new Meteor.Error('invalid-token');
			}

			const room = findRoom(this.urlParams.token, this.urlParams.rid);
			if (!room) {
				throw new Meteor.Error('invalid-room');
			}

			const agent = room && room.servedBy && findAgent(room.servedBy._id);
			if (!agent) {
				throw new Meteor.Error('invalid-agent');
			}

			return RocketChat.API.v1.success({ agent });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});

RocketChat.API.v1.addRoute('livechat/agent.next/:token', {
	get() {
		try {
			check(this.urlParams, {
				token: String,
			});

			check(this.queryParams, {
				department: Match.Maybe(String),
			});

			const { token } = this.urlParams;
			const room = findOpenRoom(token);
			if (!room) {
				throw new Meteor.Error('invalid-token');
			}

			let { department } = this.queryParams;
			if (!department) {
				const requireDeparment = RocketChat.Livechat.getRequiredDepartment();
				if (requireDeparment) {
					department = requireDeparment._id;
				}
			}

			const agentData = RocketChat.Livechat.getNextAgent(department);
			if (!agentData) {
				throw new Meteor.Error('agent-not-found');
			}

			const agent = findAgent(agentData.agentId);
			if (!agent) {
				throw new Meteor.Error('invalid-agent');
			}

			return RocketChat.API.v1.success({ agent });
		} catch (e) {
			return RocketChat.API.v1.failure(e);
		}
	},
});
<|endoftext|>


<commit_before>'use strict';

// Array of 5-trit tryte memory cells backed by an Int8Array
class Memory {
  constructor(opts={}) {
    this.tryteCount = opts.tryteCount;
    if (this.tryteCount === undefined) throw new Error('memory reqires tryteCount option');
    this.arrayType = Int8Array; // Int8Array is 8-bit signed -129 to +128, fits 5-trit -121 to +121
    this._array = opts._array || new this.arrayType(new ArrayBuffer(this.tryteCount));
    this.map = opts.map || {};
  }

  getTraps(address) {
    for (let name of Object.keys(this.map)) { // TODO: switch map to ES6 Map
      let info = this.map[name];

      if (address >= info.start && address <= info.end) {
        return {read:info.read, write:info.write};
      }
    }
    return {};
  }

  // Read one tryte
  read(address) {
    const traps = this.getTraps(address);
    if (traps.read) {
      const value = traps.read(address);
      if (value !== undefined) {
        this._array[address] = value;
      }
    }

    return this._array[address];
  }

  // Write one tryte
  write(address, value) {
    this._array[address] = value;

    const traps = this.getTraps(address);
    if (traps.write) {
      traps.write(address, this._array[address]);
    }
  }

  // Write an _array of trytes
  writeArray(address, data) {
    let i = address;
    for(let tryte of data) {
      this.write(i++, tryte);
    }
  }

  // Get a subarray view of memory starting at address, to end
  subarray(address, end) {
    return this._array.subarray(address, end);
  }

  // TODO: write individual trits, tritmapped-canvas
}

module.exports = function(opts) {
  return new Memory(opts);
};
<commit_msg>Calculate balanced memory addresses<commit_after>'use strict';

// Array of 5-trit tryte memory cells backed by an Int8Array
class Memory {
  constructor(opts={}) {
    this.tryteCount = opts.tryteCount;
    if (this.tryteCount === undefined) throw new Error('memory reqires tryteCount option');

    // Addresses are balanced ± TODO
    if (!Number.isInteger((this.tryteCount - 1) / 2)) throw new Error('memory (tryteCount-1)/2 must be integral: '+tryteCount);
    this.maxAddress = (this.tryteCount - 1) / 2;
    this.midAddress = 0;
    this.minAddress = -this.maxAddress;

    this.arrayType = Int8Array; // Int8Array is 8-bit signed -129 to +128, fits 5-trit -121 to +121
    this._array = opts._array || new this.arrayType(new ArrayBuffer(this.tryteCount));
    this.map = opts.map || {};
  }

  getTraps(address) {
    for (let name of Object.keys(this.map)) { // TODO: switch map to ES6 Map
      let info = this.map[name];

      if (address >= info.start && address <= info.end) {
        return {read:info.read, write:info.write};
      }
    }
    return {};
  }

  // Read one tryte
  read(address) {
    const traps = this.getTraps(address);
    if (traps.read) {
      const value = traps.read(address);
      if (value !== undefined) {
        this._array[address] = value;
      }
    }

    return this._array[address];
  }

  // Write one tryte
  write(address, value) {
    this._array[address] = value;

    const traps = this.getTraps(address);
    if (traps.write) {
      traps.write(address, this._array[address]);
    }
  }

  // Write an _array of trytes
  writeArray(address, data) {
    let i = address;
    for(let tryte of data) {
      this.write(i++, tryte);
    }
  }

  // Get a subarray view of memory starting at address, to end
  subarray(address, end) {
    return this._array.subarray(address, end);
  }

  // TODO: write individual trits, tritmapped-canvas
}

module.exports = function(opts) {
  return new Memory(opts);
};
<|endoftext|>


<commit_before>/**
 * Module dependencies
 */

// Public node modules.
const _ = require('lodash');
const mongoose = require('mongoose');

// Utils
const {
  models: { getValuePrimaryKey },
} = require('strapi-utils');

const getModel = function(model, plugin) {
  return (
    _.get(strapi.plugins, [plugin, 'models', model]) ||
    _.get(strapi, ['models', model]) ||
    undefined
  );
};

const transformToArrayID = (array, pk) => {
  if (_.isArray(array)) {
    return array
      .map(value => value && (getValuePrimaryKey(value, pk) || value))
      .filter(n => n)
      .map(val => _.toString(val));
  }

  return transformToArrayID([array]);
};

const removeUndefinedKeys = (obj = {}) => _.pickBy(obj, _.negate(_.isUndefined));

const addRelationMorph = async (model, params) => {
  const { id, alias, refId, ref, field, filter } = params;

  await model.updateMany(
    {
      [model.primaryKey]: id,
    },
    {
      $push: {
        [alias]: {
          ref: new mongoose.Types.ObjectId(refId),
          kind: ref,
          [filter]: field,
        },
      },
    }
  );
};

const removeRelationMorph = async (model, params) => {
  const { alias } = params;

  let opts;
  // if entry id is provided simply query it
  if (params.id) {
    opts = {
      _id: params.id,
    };
  } else {
    opts = {
      [alias]: {
        $elemMatch: {
          ref: params.refId,
          kind: params.ref,
          [params.filter]: params.field,
        },
      },
    };
  }

  await model.updateMany(opts, {
    $pull: {
      [alias]: {
        ref: params.refId,
        kind: params.ref,
        [params.filter]: params.field,
      },
    },
  });
};

module.exports = {
  async update(params) {
    const relationUpdates = [];
    const populate = this.associations.map(x => x.alias);
    const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey);

    const entry = await this.findOne({ [this.primaryKey]: primaryKeyValue })
      .populate(populate)
      .lean();

    // Only update fields which are on this document.
    const values = Object.keys(removeUndefinedKeys(params.values)).reduce((acc, attribute) => {
      const currentValue = entry[attribute];
      const newValue = params.values[attribute];

      const association = this.associations.find(x => x.alias === attribute);

      const details = this._attributes[attribute];

      // set simple attributes
      if (!association && _.get(details, 'isVirtual') !== true) {
        return _.set(acc, attribute, newValue);
      }

      const assocModel = getModel(details.model || details.collection, details.plugin);

      switch (association.nature) {
        case 'oneWay': {
          return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
        }
        case 'oneToOne': {
          // if value is the same don't do anything
          if (currentValue === newValue) return acc;

          // if the value is null, set field to null on both sides
          if (_.isNull(newValue)) {
            const updatePromise = assocModel.updateOne(
              {
                [assocModel.primaryKey]: getValuePrimaryKey(currentValue, assocModel.primaryKey),
              },
              { [details.via]: null }
            );

            relationUpdates.push(updatePromise);
            return _.set(acc, attribute, null);
          }

          // set old relations to null
          const updateLink = this.updateOne(
            { [attribute]: new mongoose.Types.ObjectId(newValue) },
            { [attribute]: null }
          ).then(() => {
            return assocModel.updateOne(
              {
                [this.primaryKey]: new mongoose.Types.ObjectId(newValue),
              },
              { [details.via]: primaryKeyValue }
            );
          });

          // set new relation
          relationUpdates.push(updateLink);
          return _.set(acc, attribute, newValue);
        }
        case 'oneToMany': {
          // set relation to null for all the ids not in the list
          const attributeIds = currentValue;
          const toRemove = _.differenceWith(attributeIds, newValue, (a, b) => {
            return `${a[assocModel.primaryKey] || a}` === `${b[assocModel.primaryKey] || b}`;
          });

          const updatePromise = assocModel
            .updateMany(
              {
                [assocModel.primaryKey]: {
                  $in: toRemove.map(
                    val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                  ),
                },
              },
              { [details.via]: null }
            )
            .then(() => {
              return assocModel.updateMany(
                {
                  [assocModel.primaryKey]: {
                    $in: newValue.map(
                      val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                    ),
                  },
                },
                { [details.via]: primaryKeyValue }
              );
            });

          relationUpdates.push(updatePromise);
          return acc;
        }
        case 'manyToOne': {
          return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
        }
        case 'manyWay':
        case 'manyToMany': {
          if (association.dominant) {
            return _.set(
              acc,
              attribute,
              newValue ? newValue.map(val => val[assocModel.primaryKey] || val) : newValue
            );
          }

          const updatePomise = assocModel
            .updateMany(
              {
                [assocModel.primaryKey]: {
                  $in: currentValue.map(
                    val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                  ),
                },
              },
              {
                $pull: {
                  [association.via]: new mongoose.Types.ObjectId(primaryKeyValue),
                },
              }
            )
            .then(() => {
              return assocModel.updateMany(
                {
                  [assocModel.primaryKey]: {
                    $in: newValue
                      ? newValue.map(
                          val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                        )
                      : newValue,
                  },
                },
                {
                  $addToSet: { [association.via]: [primaryKeyValue] },
                }
              );
            });

          relationUpdates.push(updatePomise);
          return acc;
        }
        // media -> model
        case 'manyMorphToMany':
        case 'manyMorphToOne': {
          newValue.forEach(obj => {
            const refModel = strapi.getModel(obj.ref, obj.source);

            const createRelation = () => {
              return addRelationMorph(this, {
                id: entry[this.primaryKey],
                alias: association.alias,
                ref: obj.kind || refModel.globalId,
                refId: new mongoose.Types.ObjectId(obj.refId),
                field: obj.field,
                filter: association.filter,
              });
            };

            // Clear relations to refModel
            const reverseAssoc = refModel.associations.find(assoc => assoc.alias === obj.field);
            if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') {
              relationUpdates.push(
                removeRelationMorph(this, {
                  alias: association.alias,
                  ref: obj.kind || refModel.globalId,
                  refId: new mongoose.Types.ObjectId(obj.refId),
                  field: obj.field,
                  filter: association.filter,
                })
                  .then(createRelation)
                  .then(() => {
                    // set field inside refModel
                    return refModel.updateMany(
                      {
                        [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
                      },
                      {
                        [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]),
                      }
                    );
                  })
              );
            } else {
              relationUpdates.push(
                createRelation().then(() => {
                  // push to field inside refModel
                  return refModel.updateMany(
                    {
                      [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
                    },
                    {
                      $push: { [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]) },
                    }
                  );
                })
              );
            }
          });
          break;
        }
        // model -> media
        case 'oneToManyMorph':
        case 'manyToManyMorph': {
          // Compare array of ID to find deleted files.
          const currentIds = transformToArrayID(currentValue, this.primaryKey);
          const newIds = transformToArrayID(newValue, this.primaryKey);

          const toAdd = _.difference(newIds, currentIds);
          const toRemove = _.difference(currentIds, newIds);

          const model = getModel(details.model || details.collection, details.plugin);

          _.set(acc, attribute, newIds);

          const addPromise = Promise.all(
            toAdd.map(id => {
              return addRelationMorph(model, {
                id,
                alias: association.via,
                ref: this.globalId,
                refId: entry._id,
                field: association.alias,
                filter: association.filter,
              });
            })
          );

          relationUpdates.push(addPromise);

          toRemove.forEach(id => {
            relationUpdates.push(
              removeRelationMorph(model, {
                id,
                alias: association.via,
                ref: this.globalId,
                refId: entry._id,
                field: association.alias,
                filter: association.filter,
              })
            );
          });
          break;
        }
        case 'oneMorphToOne':
        case 'oneMorphToMany':
          break;
        default:
      }

      return acc;
    }, {});

    // Update virtuals fields.
    await Promise.all(relationUpdates).then(() =>
      this.updateOne({ [this.primaryKey]: primaryKeyValue }, values, {
        strict: false,
      })
    );

    const updatedEntity = await this.findOne({
      [this.primaryKey]: primaryKeyValue,
    }).populate(populate);

    return updatedEntity && updatedEntity.toObject ? updatedEntity.toObject() : updatedEntity;
  },

  deleteRelations(entry) {
    const primaryKeyValue = entry[this.primaryKey];

    return Promise.all(
      this.associations.map(async association => {
        const { nature, via, dominant } = association;

        switch (nature) {
          case 'oneWay':
          case 'manyWay': {
            return;
          }
          case 'oneToMany':
          case 'oneToOne': {
            if (!via || dominant) {
              return;
            }

            const targetModel = strapi.db.getModel(
              association.model || association.collection,
              association.plugin
            );

            return targetModel.updateMany({ [via]: primaryKeyValue }, { [via]: null });
          }
          case 'manyToMany':
          case 'manyToOne': {
            if (!via || dominant) {
              return;
            }

            const targetModel = strapi.db.getModel(
              association.model || association.collection,
              association.plugin
            );

            return targetModel.updateMany(
              { [via]: primaryKeyValue },
              { $pull: { [via]: primaryKeyValue } }
            );
          }
          case 'oneToManyMorph':
          case 'manyToManyMorph': {
            // delete relation inside of the ref model

            const targetModel = strapi.db.getModel(
              association.model || association.collection,
              association.plugin
            );

            // ignore them ghost relations
            if (!targetModel) return;

            const element = {
              ref: primaryKeyValue,
              kind: this.globalId,
              [association.filter]: association.alias,
            };

            return targetModel.updateMany(
              { [via]: { $elemMatch: element } },
              { $pull: { [via]: element } }
            );
          }
          case 'manyMorphToMany':
          case 'manyMorphToOne': {
            // delete relation inside of the ref model
            // console.log(entry[association.alias]);

            if (Array.isArray(entry[association.alias])) {
              return Promise.all(
                entry[association.alias].map(val => {
                  const targetModel = strapi.db.getModelByGlobalId(val.kind);

                  // ignore them ghost relations
                  if (!targetModel) return;

                  const field = val[association.filter];
                  const reverseAssoc = targetModel.associations.find(
                    assoc => assoc.alias === field
                  );

                  if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') {
                    return targetModel.updateMany(
                      {
                        [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref),
                      },
                      {
                        [field]: null,
                      }
                    );
                  }

                  return targetModel.updateMany(
                    {
                      [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref),
                    },
                    {
                      $pull: { [field]: primaryKeyValue },
                    }
                  );
                })
              );
            }

            return;
          }

          case 'oneMorphToOne':
          case 'oneMorphToMany': {
            // TODO: to implement
            return;
          }
        }
      })
    );
  },
};
<commit_msg>Delete fk for oneToOne and oneToMany mongo<commit_after>/**
 * Module dependencies
 */

// Public node modules.
const _ = require('lodash');
const mongoose = require('mongoose');

// Utils
const {
  models: { getValuePrimaryKey },
} = require('strapi-utils');

const getModel = function(model, plugin) {
  return (
    _.get(strapi.plugins, [plugin, 'models', model]) ||
    _.get(strapi, ['models', model]) ||
    undefined
  );
};

const transformToArrayID = (array, pk) => {
  if (_.isArray(array)) {
    return array
      .map(value => value && (getValuePrimaryKey(value, pk) || value))
      .filter(n => n)
      .map(val => _.toString(val));
  }

  return transformToArrayID([array]);
};

const removeUndefinedKeys = (obj = {}) => _.pickBy(obj, _.negate(_.isUndefined));

const addRelationMorph = async (model, params) => {
  const { id, alias, refId, ref, field, filter } = params;

  await model.updateMany(
    {
      [model.primaryKey]: id,
    },
    {
      $push: {
        [alias]: {
          ref: new mongoose.Types.ObjectId(refId),
          kind: ref,
          [filter]: field,
        },
      },
    }
  );
};

const removeRelationMorph = async (model, params) => {
  const { alias } = params;

  let opts;
  // if entry id is provided simply query it
  if (params.id) {
    opts = {
      _id: params.id,
    };
  } else {
    opts = {
      [alias]: {
        $elemMatch: {
          ref: params.refId,
          kind: params.ref,
          [params.filter]: params.field,
        },
      },
    };
  }

  await model.updateMany(opts, {
    $pull: {
      [alias]: {
        ref: params.refId,
        kind: params.ref,
        [params.filter]: params.field,
      },
    },
  });
};

module.exports = {
  async update(params) {
    const relationUpdates = [];
    const populate = this.associations.map(x => x.alias);
    const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey);

    const entry = await this.findOne({ [this.primaryKey]: primaryKeyValue })
      .populate(populate)
      .lean();

    // Only update fields which are on this document.
    const values = Object.keys(removeUndefinedKeys(params.values)).reduce((acc, attribute) => {
      const currentValue = entry[attribute];
      const newValue = params.values[attribute];

      const association = this.associations.find(x => x.alias === attribute);

      const details = this._attributes[attribute];

      // set simple attributes
      if (!association && _.get(details, 'isVirtual') !== true) {
        return _.set(acc, attribute, newValue);
      }

      const assocModel = getModel(details.model || details.collection, details.plugin);

      switch (association.nature) {
        case 'oneWay': {
          return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
        }
        case 'oneToOne': {
          // if value is the same don't do anything
          if (currentValue === newValue) return acc;

          // if the value is null, set field to null on both sides
          if (_.isNull(newValue)) {
            const updatePromise = assocModel.updateOne(
              {
                [assocModel.primaryKey]: getValuePrimaryKey(currentValue, assocModel.primaryKey),
              },
              { [details.via]: null }
            );

            relationUpdates.push(updatePromise);
            return _.set(acc, attribute, null);
          }

          // set old relations to null
          const updateLink = this.updateOne(
            { [attribute]: new mongoose.Types.ObjectId(newValue) },
            { [attribute]: null }
          ).then(() => {
            return assocModel.updateOne(
              {
                [this.primaryKey]: new mongoose.Types.ObjectId(newValue),
              },
              { [details.via]: primaryKeyValue }
            );
          });

          // set new relation
          relationUpdates.push(updateLink);
          return _.set(acc, attribute, newValue);
        }
        case 'oneToMany': {
          // set relation to null for all the ids not in the list
          const attributeIds = currentValue;
          const toRemove = _.differenceWith(attributeIds, newValue, (a, b) => {
            return `${a[assocModel.primaryKey] || a}` === `${b[assocModel.primaryKey] || b}`;
          });

          const updatePromise = assocModel
            .updateMany(
              {
                [assocModel.primaryKey]: {
                  $in: toRemove.map(
                    val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                  ),
                },
              },
              { [details.via]: null }
            )
            .then(() => {
              return assocModel.updateMany(
                {
                  [assocModel.primaryKey]: {
                    $in: newValue.map(
                      val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                    ),
                  },
                },
                { [details.via]: primaryKeyValue }
              );
            });

          relationUpdates.push(updatePromise);
          return acc;
        }
        case 'manyToOne': {
          return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
        }
        case 'manyWay':
        case 'manyToMany': {
          if (association.dominant) {
            return _.set(
              acc,
              attribute,
              newValue ? newValue.map(val => val[assocModel.primaryKey] || val) : newValue
            );
          }

          const updatePomise = assocModel
            .updateMany(
              {
                [assocModel.primaryKey]: {
                  $in: currentValue.map(
                    val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                  ),
                },
              },
              {
                $pull: {
                  [association.via]: new mongoose.Types.ObjectId(primaryKeyValue),
                },
              }
            )
            .then(() => {
              return assocModel.updateMany(
                {
                  [assocModel.primaryKey]: {
                    $in: newValue
                      ? newValue.map(
                          val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
                        )
                      : newValue,
                  },
                },
                {
                  $addToSet: { [association.via]: [primaryKeyValue] },
                }
              );
            });

          relationUpdates.push(updatePomise);
          return acc;
        }
        // media -> model
        case 'manyMorphToMany':
        case 'manyMorphToOne': {
          newValue.forEach(obj => {
            const refModel = strapi.getModel(obj.ref, obj.source);

            const createRelation = () => {
              return addRelationMorph(this, {
                id: entry[this.primaryKey],
                alias: association.alias,
                ref: obj.kind || refModel.globalId,
                refId: new mongoose.Types.ObjectId(obj.refId),
                field: obj.field,
                filter: association.filter,
              });
            };

            // Clear relations to refModel
            const reverseAssoc = refModel.associations.find(assoc => assoc.alias === obj.field);
            if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') {
              relationUpdates.push(
                removeRelationMorph(this, {
                  alias: association.alias,
                  ref: obj.kind || refModel.globalId,
                  refId: new mongoose.Types.ObjectId(obj.refId),
                  field: obj.field,
                  filter: association.filter,
                })
                  .then(createRelation)
                  .then(() => {
                    // set field inside refModel
                    return refModel.updateMany(
                      {
                        [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
                      },
                      {
                        [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]),
                      }
                    );
                  })
              );
            } else {
              relationUpdates.push(
                createRelation().then(() => {
                  // push to field inside refModel
                  return refModel.updateMany(
                    {
                      [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
                    },
                    {
                      $push: { [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]) },
                    }
                  );
                })
              );
            }
          });
          break;
        }
        // model -> media
        case 'oneToManyMorph':
        case 'manyToManyMorph': {
          // Compare array of ID to find deleted files.
          const currentIds = transformToArrayID(currentValue, this.primaryKey);
          const newIds = transformToArrayID(newValue, this.primaryKey);

          const toAdd = _.difference(newIds, currentIds);
          const toRemove = _.difference(currentIds, newIds);

          const model = getModel(details.model || details.collection, details.plugin);

          _.set(acc, attribute, newIds);

          const addPromise = Promise.all(
            toAdd.map(id => {
              return addRelationMorph(model, {
                id,
                alias: association.via,
                ref: this.globalId,
                refId: entry._id,
                field: association.alias,
                filter: association.filter,
              });
            })
          );

          relationUpdates.push(addPromise);

          toRemove.forEach(id => {
            relationUpdates.push(
              removeRelationMorph(model, {
                id,
                alias: association.via,
                ref: this.globalId,
                refId: entry._id,
                field: association.alias,
                filter: association.filter,
              })
            );
          });
          break;
        }
        case 'oneMorphToOne':
        case 'oneMorphToMany':
          break;
        default:
      }

      return acc;
    }, {});

    // Update virtuals fields.
    await Promise.all(relationUpdates).then(() =>
      this.updateOne({ [this.primaryKey]: primaryKeyValue }, values, {
        strict: false,
      })
    );

    const updatedEntity = await this.findOne({
      [this.primaryKey]: primaryKeyValue,
    }).populate(populate);

    return updatedEntity && updatedEntity.toObject ? updatedEntity.toObject() : updatedEntity;
  },

  deleteRelations(entry) {
    const primaryKeyValue = entry[this.primaryKey];

    return Promise.all(
      this.associations.map(async association => {
        const { nature, via, dominant } = association;

        // TODO: delete all the ref to the model

        switch (nature) {
          case 'oneWay':
          case 'manyWay': {
            return;
          }
          case 'oneToMany':
          case 'oneToOne': {
            if (!via) {
              return;
            }

            const targetModel = strapi.db.getModel(
              association.model || association.collection,
              association.plugin
            );

            return targetModel.updateMany({ [via]: primaryKeyValue }, { [via]: null });
          }
          case 'manyToMany':
          case 'manyToOne': {
            if (!via || dominant) {
              return;
            }

            const targetModel = strapi.db.getModel(
              association.model || association.collection,
              association.plugin
            );

            return targetModel.updateMany(
              { [via]: primaryKeyValue },
              { $pull: { [via]: primaryKeyValue } }
            );
          }
          case 'oneToManyMorph':
          case 'manyToManyMorph': {
            // delete relation inside of the ref model

            const targetModel = strapi.db.getModel(
              association.model || association.collection,
              association.plugin
            );

            // ignore them ghost relations
            if (!targetModel) return;

            const element = {
              ref: primaryKeyValue,
              kind: this.globalId,
              [association.filter]: association.alias,
            };

            return targetModel.updateMany(
              { [via]: { $elemMatch: element } },
              { $pull: { [via]: element } }
            );
          }
          case 'manyMorphToMany':
          case 'manyMorphToOne': {
            // delete relation inside of the ref model
            // console.log(entry[association.alias]);

            if (Array.isArray(entry[association.alias])) {
              return Promise.all(
                entry[association.alias].map(val => {
                  const targetModel = strapi.db.getModelByGlobalId(val.kind);

                  // ignore them ghost relations
                  if (!targetModel) return;

                  const field = val[association.filter];
                  const reverseAssoc = targetModel.associations.find(
                    assoc => assoc.alias === field
                  );

                  if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') {
                    return targetModel.updateMany(
                      {
                        [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref),
                      },
                      {
                        [field]: null,
                      }
                    );
                  }

                  return targetModel.updateMany(
                    {
                      [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref),
                    },
                    {
                      $pull: { [field]: primaryKeyValue },
                    }
                  );
                })
              );
            }

            return;
          }

          case 'oneMorphToOne':
          case 'oneMorphToMany': {
            // TODO: to implement
            return;
          }
        }
      })
    );
  },
};
<|endoftext|>