Commit e6c460b4 authored by Romain Courteaud's avatar Romain Courteaud

Release version 3.6.0

Include new google drive storage and performance improvement for QueryStorage's allDocs.
parent d005d845
...@@ -3889,229 +3889,6 @@ if (typeof define === 'function' && define.amd) { ...@@ -3889,229 +3889,6 @@ if (typeof define === 'function' && define.amd) {
makeGlobal(); makeGlobal();
} }
}).call(this); }).call(this);
;/*jslint indent: 2, maxlen: 80, sloppy: true */
var query_class_dict = {};
;/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global parseStringToObject: true, emptyFunction: true, sortOn: true, limit:
true, select: true, window, stringEscapeRegexpCharacters: true,
deepClone, RSVP*/
/**
* The query to use to filter a list of objects.
* This is an abstract class.
*
* @class Query
* @constructor
*/
function Query() {
/**
* Called before parsing the query. Must be overridden!
*
* @method onParseStart
* @param {Object} object The object shared in the parse process
* @param {Object} option Some option gave in parse()
*/
this.onParseStart = emptyFunction;
/**
* Called when parsing a simple query. Must be overridden!
*
* @method onParseSimpleQuery
* @param {Object} object The object shared in the parse process
* @param {Object} option Some option gave in parse()
*/
this.onParseSimpleQuery = emptyFunction;
/**
* Called when parsing a complex query. Must be overridden!
*
* @method onParseComplexQuery
* @param {Object} object The object shared in the parse process
* @param {Object} option Some option gave in parse()
*/
this.onParseComplexQuery = emptyFunction;
/**
* Called after parsing the query. Must be overridden!
*
* @method onParseEnd
* @param {Object} object The object shared in the parse process
* @param {Object} option Some option gave in parse()
*/
this.onParseEnd = emptyFunction;
}
/**
* Filter the item list with matching item only
*
* @method exec
* @param {Array} item_list The list of object
* @param {Object} [option] Some operation option
* @param {Array} [option.select_list] A object keys to retrieve
* @param {Array} [option.sort_on] Couples of object keys and "ascending"
* or "descending"
* @param {Array} [option.limit] Couple of integer, first is an index and
* second is the length.
*/
Query.prototype.exec = function (item_list, option) {
var i, promises = [];
if (!Array.isArray(item_list)) {
throw new TypeError("Query().exec(): Argument 1 is not of type 'array'");
}
if (option === undefined) {
option = {};
}
if (typeof option !== 'object') {
throw new TypeError("Query().exec(): " +
"Optional argument 2 is not of type 'object'");
}
for (i = 0; i < item_list.length; i += 1) {
if (!item_list[i]) {
promises.push(RSVP.resolve(false));
} else {
promises.push(this.match(item_list[i]));
}
}
return new RSVP.Queue()
.push(function () {
return RSVP.all(promises);
})
.push(function (answers) {
var j;
for (j = answers.length - 1; j >= 0; j -= 1) {
if (!answers[j]) {
item_list.splice(j, 1);
}
}
if (option.sort_on) {
return sortOn(option.sort_on, item_list);
}
})
.push(function () {
if (option.limit) {
return limit(option.limit, item_list);
}
})
.push(function () {
return select(option.select_list || [], item_list);
})
.push(function () {
return item_list;
});
};
/**
* Test if an item matches this query
*
* @method match
* @param {Object} item The object to test
* @return {Boolean} true if match, false otherwise
*/
Query.prototype.match = function () {
return RSVP.resolve(true);
};
/**
* Browse the Query in deep calling parser method in each step.
*
* `onParseStart` is called first, on end `onParseEnd` is called.
* It starts from the simple queries at the bottom of the tree calling the
* parser method `onParseSimpleQuery`, and go up calling the
* `onParseComplexQuery` method.
*
* @method parse
* @param {Object} option Any options you want (except 'parsed')
* @return {Any} The parse result
*/
Query.prototype.parse = function (option) {
var that = this,
object;
/**
* The recursive parser.
*
* @param {Object} object The object shared in the parse process
* @param {Object} options Some options usable in the parseMethods
* @return {Any} The parser result
*/
function recParse(object, option) {
var query = object.parsed,
queue = new RSVP.Queue(),
i;
function enqueue(j) {
queue
.push(function () {
object.parsed = query.query_list[j];
return recParse(object, option);
})
.push(function () {
query.query_list[j] = object.parsed;
});
}
if (query.type === "complex") {
for (i = 0; i < query.query_list.length; i += 1) {
enqueue(i);
}
return queue
.push(function () {
object.parsed = query;
return that.onParseComplexQuery(object, option);
});
}
if (query.type === "simple") {
return that.onParseSimpleQuery(object, option);
}
}
object = {
parsed: JSON.parse(JSON.stringify(that.serialized()))
};
return new RSVP.Queue()
.push(function () {
return that.onParseStart(object, option);
})
.push(function () {
return recParse(object, option);
})
.push(function () {
return that.onParseEnd(object, option);
})
.push(function () {
return object.parsed;
});
};
/**
* Convert this query to a parsable string.
*
* @method toString
* @return {String} The string version of this query
*/
Query.prototype.toString = function () {
return "";
};
/**
* Convert this query to an jsonable object in order to be remake thanks to
* QueryFactory class.
*
* @method serialized
* @return {Object} The jsonable object
*/
Query.prototype.serialized = function () {
return undefined;
};
window.Query = Query;
;/** ;/**
* Parse a text request to a json query object tree * Parse a text request to a json query object tree
* *
...@@ -4815,896 +4592,1010 @@ if ((error_count = __NODEJS_parse(string, error_offsets, error_lookaheads)) > 0) ...@@ -4815,896 +4592,1010 @@ if ((error_count = __NODEJS_parse(string, error_offsets, error_lookaheads)) > 0)
; return result; ; return result;
} // parseStringToObject } // parseStringToObject
Query.parseStringToObject = parseStringToObject; ;/*global RSVP, window, parseStringToObject*/
;/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*jslint nomen: true, maxlen: 90*/
/*global Query: true, query_class_dict: true, inherits: true, (function (RSVP, window, parseStringToObject) {
window, QueryFactory, RSVP */ "use strict";
/** var query_class_dict = {},
* The ComplexQuery inherits from Query, and compares one or several metadata regexp_escape = /[\-\[\]{}()*+?.,\\\^$|#\s]/g,
* values. regexp_percent = /%/g,
* regexp_underscore = /_/g,
* @class ComplexQuery regexp_operator = /^(?:AND|OR|NOT)$/i,
* @extends Query regexp_comparaison = /^(?:!?=|<=?|>=?)$/i;
* @param {Object} [spec={}] The specifications
* @param {String} [spec.operator="AND"] The compare method to use
* @param {String} spec.key The metadata key
* @param {String} spec.value The value of the metadata to compare
*/
function ComplexQuery(spec, key_schema) {
Query.call(this);
/** /**
* Logical operator to use to compare object values * Convert metadata values to array of strings. ex:
*
* "a" -> ["a"],
* {"content": "a"} -> ["a"]
* *
* @attribute operator * @param {Any} value The metadata value
* @type String * @return {Array} The value in string array format
* @default "AND"
* @optional
*/ */
this.operator = spec.operator; function metadataValueToStringArray(value) {
var i, new_value = [];
if (value === undefined) {
return undefined;
}
if (!Array.isArray(value)) {
value = [value];
}
for (i = 0; i < value.length; i += 1) {
if (typeof value[i] === 'object') {
new_value[i] = value[i].content;
} else {
new_value[i] = value[i];
}
}
return new_value;
}
/** /**
* The sub Query list which are used to query an item. * A sort function to sort items by key
* *
* @attribute query_list * @param {String} key The key to sort on
* @type Array * @param {String} [way="ascending"] 'ascending' or 'descending'
* @default [] * @return {Function} The sort function
* @optional
*/ */
this.query_list = spec.query_list || []; function sortFunction(key, way) {
/*jslint unparam: true*/ var result;
this.query_list = this.query_list.map( if (way === 'descending') {
// decorate the map to avoid sending the index as key_schema argument result = 1;
function (o, i) { return QueryFactory.create(o, key_schema); } } else if (way === 'ascending') {
); result = -1;
/*jslint unparam: false*/ } else {
throw new TypeError("Query.sortFunction(): " +
} "Argument 2 must be 'ascending' or 'descending'");
inherits(ComplexQuery, Query); }
return function (a, b) {
ComplexQuery.prototype.operator = "AND"; // this comparison is 5 times faster than json comparison
ComplexQuery.prototype.type = "complex"; var i, l;
a = metadataValueToStringArray(a[key]) || [];
/** b = metadataValueToStringArray(b[key]) || [];
* #crossLink "Query/match:method" l = a.length > b.length ? a.length : b.length;
*/ for (i = 0; i < l; i += 1) {
ComplexQuery.prototype.match = function (item) { if (a[i] === undefined) {
var operator = this.operator; return result;
if (!(/^(?:AND|OR|NOT)$/i.test(operator))) { }
operator = "AND"; if (b[i] === undefined) {
return -result;
}
if (a[i] > b[i]) {
return -result;
}
if (a[i] < b[i]) {
return result;
}
}
return 0;
};
} }
return this[operator.toUpperCase()](item);
};
/** /**
* #crossLink "Query/toString:method" * Sort a list of items, according to keys and directions.
*/ *
ComplexQuery.prototype.toString = function () { * @param {Array} sort_on_option List of couples [key, direction]
var str_list = [], this_operator = this.operator; * @param {Array} list The item list to sort
if (this.operator === "NOT") { * @return {Array} The filtered list
str_list.push("NOT ("); */
str_list.push(this.query_list[0].toString()); function sortOn(sort_on_option, list) {
str_list.push(")"); var sort_index;
return str_list.join(" "); if (!Array.isArray(sort_on_option)) {
throw new TypeError("jioquery.sortOn(): " +
"Argument 1 is not of type 'array'");
}
for (sort_index = sort_on_option.length - 1; sort_index >= 0;
sort_index -= 1) {
list.sort(sortFunction(
sort_on_option[sort_index][0],
sort_on_option[sort_index][1]
));
}
return list;
} }
this.query_list.forEach(function (query) {
str_list.push("(");
str_list.push(query.toString());
str_list.push(")");
str_list.push(this_operator);
});
str_list.length -= 1;
return str_list.join(" ");
};
/**
* #crossLink "Query/serialized:method"
*/
ComplexQuery.prototype.serialized = function () {
var s = {
"type": "complex",
"operator": this.operator,
"query_list": []
};
this.query_list.forEach(function (query) {
s.query_list.push(
typeof query.toJSON === "function" ? query.toJSON() : query
);
});
return s;
};
ComplexQuery.prototype.toJSON = ComplexQuery.prototype.serialized;
/** /**
* Comparison operator, test if all sub queries match the * Limit a list of items, according to index and length.
* item value *
* * @param {Array} limit_option A couple [from, length]
* @method AND * @param {Array} list The item list to limit
* @param {Object} item The item to match * @return {Array} The filtered list
* @return {Boolean} true if all match, false otherwise */
*/ function limit(limit_option, list) {
ComplexQuery.prototype.AND = function (item) { if (!Array.isArray(limit_option)) {
var queue = new RSVP.Queue(), throw new TypeError("jioquery.limit(): " +
context = this, "Argument 1 is not of type 'array'");
i = 0;
function executeNextIfNotFalse(result) {
if (result === false) {
// No need to evaluate the other elements, as one is false
return result;
} }
if (context.query_list.length === i) { if (!Array.isArray(list)) {
// No new element to loop on throw new TypeError("jioquery.limit(): " +
return true; "Argument 2 is not of type 'array'");
} }
queue list.splice(0, limit_option[0]);
.push(function () { if (limit_option[1]) {
var sub_result = context.query_list[i].match(item); list.splice(limit_option[1]);
i += 1; }
return sub_result; return list;
})
.push(executeNextIfNotFalse);
} }
executeNextIfNotFalse(true); /**
return queue; * Filter a list of items, modifying them to select only wanted keys.
}; *
* @param {Array} select_option Key list to keep
/** * @param {Array} list The item list to filter
* Comparison operator, test if one of the sub queries matches the * @return {Array} The filtered list
* item value */
* function select(select_option, list) {
* @method OR var i, j, new_item;
* @param {Object} item The item to match if (!Array.isArray(select_option)) {
* @return {Boolean} true if one match, false otherwise throw new TypeError("jioquery.select(): " +
*/ "Argument 1 is not of type Array");
ComplexQuery.prototype.OR = function (item) { }
var queue = new RSVP.Queue(), if (!Array.isArray(list)) {
context = this, throw new TypeError("jioquery.select(): " +
i = 0; "Argument 2 is not of type Array");
}
function executeNextIfNotTrue(result) { for (i = 0; i < list.length; i += 1) {
if (result === true) { new_item = {};
// No need to evaluate the other elements, as one is true for (j = 0; j < select_option.length; j += 1) {
return result; if (list[i].hasOwnProperty([select_option[j]])) {
} new_item[select_option[j]] = list[i][select_option[j]];
if (context.query_list.length === i) { }
// No new element to loop on }
return false; for (j in new_item) {
if (new_item.hasOwnProperty(j)) {
list[i] = new_item;
break;
}
}
} }
queue return list;
.push(function () {
var sub_result = context.query_list[i].match(item);
i += 1;
return sub_result;
})
.push(executeNextIfNotTrue);
} }
executeNextIfNotTrue(false); /**
return queue; * The query to use to filter a list of objects.
}; * This is an abstract class.
*
* @class Query
* @constructor
*/
function Query() {
/** /**
* Comparison operator, test if the sub query does not match the * Called before parsing the query. Must be overridden!
* item value *
* * @method onParseStart
* @method NOT * @param {Object} object The object shared in the parse process
* @param {Object} item The item to match * @param {Object} option Some option gave in parse()
* @return {Boolean} true if one match, false otherwise */
*/ // this.onParseStart = emptyFunction;
ComplexQuery.prototype.NOT = function (item) {
return new RSVP.Queue()
.push(function () {
return this.query_list[0].match(item);
})
.push(function (answer) {
return !answer;
});
};
query_class_dict.complex = ComplexQuery; /**
* Called when parsing a simple query. Must be overridden!
*
* @method onParseSimpleQuery
* @param {Object} object The object shared in the parse process
* @param {Object} option Some option gave in parse()
*/
// this.onParseSimpleQuery = emptyFunction;
window.ComplexQuery = ComplexQuery; /**
;/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ * Called when parsing a complex query. Must be overridden!
/*global window, ComplexQuery, SimpleQuery, Query, parseStringToObject, *
query_class_dict */ * @method onParseComplexQuery
* @param {Object} object The object shared in the parse process
* @param {Object} option Some option gave in parse()
*/
// this.onParseComplexQuery = emptyFunction;
/** /**
* Provides static methods to create Query object * Called after parsing the query. Must be overridden!
* *
* @class QueryFactory * @method onParseEnd
*/ * @param {Object} object The object shared in the parse process
function QueryFactory() { * @param {Object} option Some option gave in parse()
return; */
} // this.onParseEnd = emptyFunction;
/** return;
* Creates Query object from a search text string or a serialized version
* of a Query.
*
* @method create
* @static
* @param {Object,String} object The search text or the serialized version
* of a Query
* @return {Query} A Query object
*/
QueryFactory.create = function (object, key_schema) {
if (object === "") {
return new Query();
} }
if (typeof object === "string") {
object = parseStringToObject(object);
}
if (typeof (object || {}).type === "string" &&
query_class_dict[object.type]) {
return new query_class_dict[object.type](object, key_schema);
}
throw new TypeError("QueryFactory.create(): " +
"Argument 1 is not a search text or a parsable object");
};
window.QueryFactory = QueryFactory;
;/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global Query, exports */
function objectToSearchText(query) { /**
var str_list = []; * Filter the item list with matching item only
if (query.type === "complex") { *
str_list.push("("); * @method exec
(query.query_list || []).forEach(function (sub_query) { * @param {Array} item_list The list of object
str_list.push(objectToSearchText(sub_query)); * @param {Object} [option] Some operation option
str_list.push(query.operator); * @param {Array} [option.select_list] A object keys to retrieve
}); * @param {Array} [option.sort_on] Couples of object keys and "ascending"
str_list.length -= 1; * or "descending"
str_list.push(")"); * @param {Array} [option.limit] Couple of integer, first is an index and
return str_list.join(" "); * second is the length.
} */
if (query.type === "simple") { Query.prototype.exec = function (item_list, option) {
return (query.key ? query.key + ": " : "") + if (!Array.isArray(item_list)) {
(query.operator || "") + ' "' + query.value + '"'; throw new TypeError("Query().exec(): Argument 1 is not of type 'array'");
} }
throw new TypeError("This object is not a query"); if (option === undefined) {
} option = {};
Query.objectToSearchText = objectToSearchText; }
;/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ if (typeof option !== 'object') {
/*global Query, inherits, query_class_dict, window, throw new TypeError("Query().exec(): " +
searchTextToRegExp, RSVP */ "Optional argument 2 is not of type 'object'");
}
var checkKeySchema = function (key_schema) { var context = this,
var prop; i;
for (i = item_list.length - 1; i >= 0; i -= 1) {
if (key_schema !== undefined) { if (!context.match(item_list[i])) {
if (typeof key_schema !== 'object') { item_list.splice(i, 1);
throw new TypeError("SimpleQuery().create(): " +
"key_schema is not of type 'object'");
}
// key_set is mandatory
if (key_schema.key_set === undefined) {
throw new TypeError("SimpleQuery().create(): " +
"key_schema has no 'key_set' property");
}
for (prop in key_schema) {
if (key_schema.hasOwnProperty(prop)) {
switch (prop) {
case 'key_set':
case 'cast_lookup':
case 'match_lookup':
break;
default:
throw new TypeError("SimpleQuery().create(): " +
"key_schema has unknown property '" + prop + "'");
}
} }
} }
}
};
if (option.sort_on) {
sortOn(option.sort_on, item_list);
}
/** if (option.limit) {
* The SimpleQuery inherits from Query, and compares one metadata value limit(option.limit, item_list);
* }
* @class SimpleQuery
* @extends Query
* @param {Object} [spec={}] The specifications
* @param {String} [spec.operator="="] The compare method to use
* @param {String} spec.key The metadata key
* @param {String} spec.value The value of the metadata to compare
*/
function SimpleQuery(spec, key_schema) {
Query.call(this);
checkKeySchema(key_schema); select(option.select_list || [], item_list);
this._key_schema = key_schema || {}; return new RSVP.Queue()
.push(function () {
return item_list;
});
};
/** /**
* Operator to use to compare object values * Test if an item matches this query
* *
* @attribute operator * @method match
* @type String * @param {Object} item The object to test
* @optional * @return {Boolean} true if match, false otherwise
*/ */
this.operator = spec.operator; Query.prototype.match = function () {
return true;
};
/** /**
* Key of the object which refers to the value to compare * Browse the Query in deep calling parser method in each step.
*
* `onParseStart` is called first, on end `onParseEnd` is called.
* It starts from the simple queries at the bottom of the tree calling the
* parser method `onParseSimpleQuery`, and go up calling the
* `onParseComplexQuery` method.
* *
* @attribute key * @method parse
* @type String * @param {Object} option Any options you want (except 'parsed')
* @return {Any} The parse result
*/ */
this.key = spec.key; Query.prototype.parse = function (option) {
var that = this,
object;
/**
* The recursive parser.
*
* @param {Object} object The object shared in the parse process
* @param {Object} options Some options usable in the parseMethods
* @return {Any} The parser result
*/
function recParse(object, option) {
var query = object.parsed,
queue = new RSVP.Queue(),
i;
function enqueue(j) {
queue
.push(function () {
object.parsed = query.query_list[j];
return recParse(object, option);
})
.push(function () {
query.query_list[j] = object.parsed;
});
}
if (query.type === "complex") {
for (i = 0; i < query.query_list.length; i += 1) {
enqueue(i);
}
return queue
.push(function () {
object.parsed = query;
return that.onParseComplexQuery(object, option);
});
}
if (query.type === "simple") {
return that.onParseSimpleQuery(object, option);
}
}
object = {
parsed: JSON.parse(JSON.stringify(that.serialized()))
};
return new RSVP.Queue()
.push(function () {
return that.onParseStart(object, option);
})
.push(function () {
return recParse(object, option);
})
.push(function () {
return that.onParseEnd(object, option);
})
.push(function () {
return object.parsed;
});
};
/** /**
* Value is used to do the comparison with the object value * Convert this query to a parsable string.
* *
* @attribute value * @method toString
* @type String * @return {String} The string version of this query
*/ */
this.value = spec.value; Query.prototype.toString = function () {
return "";
} };
inherits(SimpleQuery, Query);
SimpleQuery.prototype.type = "simple"; /**
* Convert this query to an jsonable object in order to be remake thanks to
* QueryFactory class.
*
* @method serialized
* @return {Object} The jsonable object
*/
Query.prototype.serialized = function () {
return undefined;
};
var checkKey = function (key) { /**
var prop; * Provides static methods to create Query object
*
* @class QueryFactory
*/
function QueryFactory() {
return;
}
if (key.read_from === undefined) { /**
throw new TypeError("Custom key is missing the read_from property"); * Escapes regexp special chars from a string.
*
* @param {String} string The string to escape
* @return {String} The escaped string
*/
function stringEscapeRegexpCharacters(string) {
return string.replace(regexp_escape, "\\$&");
} }
for (prop in key) { /**
if (key.hasOwnProperty(prop)) { * Inherits the prototype methods from one constructor into another. The
switch (prop) { * prototype of `constructor` will be set to a new object created from
case 'read_from': * `superConstructor`.
case 'cast_to': *
case 'equal_match': * @param {Function} constructor The constructor which inherits the super one
break; * @param {Function} superConstructor The super constructor
default: */
throw new TypeError("Custom key has unknown property '" + function inherits(constructor, superConstructor) {
prop + "'"); constructor.super_ = superConstructor;
constructor.prototype = Object.create(superConstructor.prototype, {
"constructor": {
"configurable": true,
"enumerable": false,
"writable": true,
"value": constructor
} }
} });
} }
};
/**
/** * Convert a search text to a regexp.
* #crossLink "Query/match:method" *
*/ * @param {String} string The string to convert
SimpleQuery.prototype.match = function (item) { * @param {Boolean} [use_wildcard_character=true] Use wildcard "%" and "_"
var object_value = null, * @return {RegExp} The search text regexp
equal_match = null, */
cast_to = null, function searchTextToRegExp(string, use_wildcard_characters) {
matchMethod = null, if (typeof string !== 'string') {
operator = this.operator, throw new TypeError("jioquery.searchTextToRegExp(): " +
value = null, "Argument 1 is not of type 'string'");
key = this.key; }
if (use_wildcard_characters === false) {
/*jslint regexp: true */ return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$");
if (!(/^(?:!?=|<=?|>=?)$/i.test(operator))) {
// `operator` is not correct, we have to change it to "like" or "="
if (/%/.test(this.value)) {
// `value` contains a non escaped `%`
operator = "like";
} else {
// `value` does not contain non escaped `%`
operator = "=";
} }
return new RegExp("^" + stringEscapeRegexpCharacters(string)
.replace(regexp_percent, '.*')
.replace(regexp_underscore, '.') + "$");
} }
matchMethod = this[operator]; /**
* The ComplexQuery inherits from Query, and compares one or several metadata
* values.
*
* @class ComplexQuery
* @extends Query
* @param {Object} [spec={}] The specifications
* @param {String} [spec.operator="AND"] The compare method to use
* @param {String} spec.key The metadata key
* @param {String} spec.value The value of the metadata to compare
*/
function ComplexQuery(spec, key_schema) {
Query.call(this);
if (this._key_schema.key_set && this._key_schema.key_set[key] !== undefined) { /**
key = this._key_schema.key_set[key]; * Logical operator to use to compare object values
} *
* @attribute operator
* @type String
* @default "AND"
* @optional
*/
this.operator = spec.operator;
/**
* The sub Query list which are used to query an item.
*
* @attribute query_list
* @type Array
* @default []
* @optional
*/
this.query_list = spec.query_list || [];
this.query_list = this.query_list.map(
// decorate the map to avoid sending the index as key_schema argument
function (o) { return QueryFactory.create(o, key_schema); }
);
if (typeof key === 'object') { }
checkKey(key); inherits(ComplexQuery, Query);
object_value = item[key.read_from];
equal_match = key.equal_match; ComplexQuery.prototype.operator = "AND";
ComplexQuery.prototype.type = "complex";
// equal_match can be a string /**
if (typeof equal_match === 'string') { * #crossLink "Query/match:method"
// XXX raise error if equal_match not in match_lookup */
equal_match = this._key_schema.match_lookup[equal_match]; ComplexQuery.prototype.match = function (item) {
var operator = this.operator;
if (!(regexp_operator.test(operator))) {
operator = "AND";
} }
return this[operator.toUpperCase()](item);
};
/**
* #crossLink "Query/toString:method"
*/
ComplexQuery.prototype.toString = function () {
var str_list = [], this_operator = this.operator;
if (this.operator === "NOT") {
str_list.push("NOT (");
str_list.push(this.query_list[0].toString());
str_list.push(")");
return str_list.join(" ");
}
this.query_list.forEach(function (query) {
str_list.push("(");
str_list.push(query.toString());
str_list.push(")");
str_list.push(this_operator);
});
str_list.length -= 1;
return str_list.join(" ");
};
/**
* #crossLink "Query/serialized:method"
*/
ComplexQuery.prototype.serialized = function () {
var s = {
"type": "complex",
"operator": this.operator,
"query_list": []
};
this.query_list.forEach(function (query) {
s.query_list.push(
typeof query.toJSON === "function" ? query.toJSON() : query
);
});
return s;
};
ComplexQuery.prototype.toJSON = ComplexQuery.prototype.serialized;
// equal_match overrides the default '=' operator /**
if (equal_match !== undefined) { * Comparison operator, test if all sub queries match the
matchMethod = (operator === "=" || operator === "like" ? * item value
equal_match : matchMethod); *
* @method AND
* @param {Object} item The item to match
* @return {Boolean} true if all match, false otherwise
*/
ComplexQuery.prototype.AND = function (item) {
var result = true,
i = 0;
while (result && (i !== this.query_list.length)) {
result = this.query_list[i].match(item);
i += 1;
} }
return result;
value = this.value; };
cast_to = key.cast_to;
if (cast_to) {
// cast_to can be a string
if (typeof cast_to === 'string') {
// XXX raise error if cast_to not in cast_lookup
cast_to = this._key_schema.cast_lookup[cast_to];
}
try { /**
value = cast_to(value); * Comparison operator, test if one of the sub queries matches the
} catch (e) { * item value
value = undefined; *
} * @method OR
* @param {Object} item The item to match
* @return {Boolean} true if one match, false otherwise
*/
ComplexQuery.prototype.OR = function (item) {
var result = false,
i = 0;
try { while ((!result) && (i !== this.query_list.length)) {
object_value = cast_to(object_value); result = this.query_list[i].match(item);
} catch (e) { i += 1;
object_value = undefined;
}
} }
} else {
object_value = item[key];
value = this.value;
}
if (object_value === undefined || value === undefined) {
return RSVP.resolve(false);
}
return matchMethod(object_value, value);
};
/** return result;
* #crossLink "Query/toString:method" };
*/
SimpleQuery.prototype.toString = function () {
return (this.key ? this.key + ":" : "") +
(this.operator ? " " + this.operator : "") + ' "' + this.value + '"';
};
/** /**
* #crossLink "Query/serialized:method" * Comparison operator, test if the sub query does not match the
*/ * item value
SimpleQuery.prototype.serialized = function () { *
var object = { * @method NOT
"type": "simple", * @param {Object} item The item to match
"key": this.key, * @return {Boolean} true if one match, false otherwise
"value": this.value */
}; ComplexQuery.prototype.NOT = function (item) {
if (this.operator !== undefined) { return !this.query_list[0].match(item);
object.operator = this.operator; };
}
return object;
};
SimpleQuery.prototype.toJSON = SimpleQuery.prototype.serialized;
/** /**
* Comparison operator, test if this query value matches the item value * Creates Query object from a search text string or a serialized version
* * of a Query.
* @method = *
* @param {String} object_value The value to compare * @method create
* @param {String} comparison_value The comparison value * @static
* @return {Boolean} true if match, false otherwise * @param {Object,String} object The search text or the serialized version
*/ * of a Query
SimpleQuery.prototype["="] = function (object_value, comparison_value) { * @return {Query} A Query object
var value, i; */
if (!Array.isArray(object_value)) { QueryFactory.create = function (object, key_schema) {
object_value = [object_value]; if (object === "") {
} return new Query();
for (i = 0; i < object_value.length; i += 1) {
value = object_value[i];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
} }
if (typeof value.cmp === "function") { if (typeof object === "string") {
return RSVP.resolve(value.cmp(comparison_value) === 0); object = parseStringToObject(object);
} }
if ( if (typeof (object || {}).type === "string" &&
searchTextToRegExp(comparison_value.toString(), false). query_class_dict[object.type]) {
test(value.toString()) return new query_class_dict[object.type](object, key_schema);
) {
return RSVP.resolve(true);
} }
} throw new TypeError("QueryFactory.create(): " +
return RSVP.resolve(false); "Argument 1 is not a search text or a parsable object");
}; };
/** function objectToSearchText(query) {
* Comparison operator, test if this query value matches the item value var str_list = [];
* if (query.type === "complex") {
* @method like str_list.push("(");
* @param {String} object_value The value to compare (query.query_list || []).forEach(function (sub_query) {
* @param {String} comparison_value The comparison value str_list.push(objectToSearchText(sub_query));
* @return {Boolean} true if match, false otherwise str_list.push(query.operator);
*/ });
SimpleQuery.prototype.like = function (object_value, comparison_value) { str_list.length -= 1;
var value, i; str_list.push(")");
if (!Array.isArray(object_value)) { return str_list.join(" ");
object_value = [object_value];
}
for (i = 0; i < object_value.length; i += 1) {
value = object_value[i];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
} }
if (typeof value.cmp === "function") { if (query.type === "simple") {
return RSVP.resolve(value.cmp(comparison_value) === 0); return (query.key ? query.key + ": " : "") +
(query.operator || "") + ' "' + query.value + '"';
} }
if ( throw new TypeError("This object is not a query");
searchTextToRegExp(comparison_value.toString()).test(value.toString()) }
) {
return RSVP.resolve(true); function checkKeySchema(key_schema) {
var prop;
if (key_schema !== undefined) {
if (typeof key_schema !== 'object') {
throw new TypeError("SimpleQuery().create(): " +
"key_schema is not of type 'object'");
}
// key_set is mandatory
if (key_schema.key_set === undefined) {
throw new TypeError("SimpleQuery().create(): " +
"key_schema has no 'key_set' property");
}
for (prop in key_schema) {
if (key_schema.hasOwnProperty(prop)) {
switch (prop) {
case 'key_set':
case 'cast_lookup':
case 'match_lookup':
break;
default:
throw new TypeError("SimpleQuery().create(): " +
"key_schema has unknown property '" + prop + "'");
}
}
}
} }
} }
return RSVP.resolve(false);
};
/** /**
* Comparison operator, test if this query value does not match the item value * The SimpleQuery inherits from Query, and compares one metadata value
* *
* @method != * @class SimpleQuery
* @param {String} object_value The value to compare * @extends Query
* @param {String} comparison_value The comparison value * @param {Object} [spec={}] The specifications
* @return {Boolean} true if not match, false otherwise * @param {String} [spec.operator="="] The compare method to use
*/ * @param {String} spec.key The metadata key
SimpleQuery.prototype["!="] = function (object_value, comparison_value) { * @param {String} spec.value The value of the metadata to compare
var value, i; */
if (!Array.isArray(object_value)) { function SimpleQuery(spec, key_schema) {
object_value = [object_value]; Query.call(this);
checkKeySchema(key_schema);
this._key_schema = key_schema || {};
/**
* Operator to use to compare object values
*
* @attribute operator
* @type String
* @optional
*/
this.operator = spec.operator;
/**
* Key of the object which refers to the value to compare
*
* @attribute key
* @type String
*/
this.key = spec.key;
/**
* Value is used to do the comparison with the object value
*
* @attribute value
* @type String
*/
this.value = spec.value;
} }
for (i = 0; i < object_value.length; i += 1) { inherits(SimpleQuery, Query);
value = object_value[i];
if (typeof value === 'object' && value.hasOwnProperty('content')) { SimpleQuery.prototype.type = "simple";
value = value.content;
} function checkKey(key) {
if (typeof value.cmp === "function") { var prop;
return RSVP.resolve(value.cmp(comparison_value) !== 0);
if (key.read_from === undefined) {
throw new TypeError("Custom key is missing the read_from property");
} }
if (
searchTextToRegExp(comparison_value.toString(), false). for (prop in key) {
test(value.toString()) if (key.hasOwnProperty(prop)) {
) { switch (prop) {
return RSVP.resolve(false); case 'read_from':
case 'cast_to':
case 'equal_match':
break;
default:
throw new TypeError("Custom key has unknown property '" +
prop + "'");
}
}
} }
} }
return RSVP.resolve(true);
};
/** /**
* Comparison operator, test if this query value is lower than the item value * #crossLink "Query/match:method"
* */
* @method < SimpleQuery.prototype.match = function (item) {
* @param {Number, String} object_value The value to compare var object_value = null,
* @param {Number, String} comparison_value The comparison value equal_match = null,
* @return {Boolean} true if lower, false otherwise cast_to = null,
*/ matchMethod = null,
SimpleQuery.prototype["<"] = function (object_value, comparison_value) { operator = this.operator,
var value; value = null,
if (!Array.isArray(object_value)) { key = this.key;
object_value = [object_value];
} if (!(regexp_comparaison.test(operator))) {
value = object_value[0]; // `operator` is not correct, we have to change it to "like" or "="
if (typeof value === 'object' && value.hasOwnProperty('content')) { if (regexp_percent.test(this.value)) {
value = value.content; // `value` contains a non escaped `%`
} operator = "like";
if (typeof value.cmp === "function") { } else {
return RSVP.resolve(value.cmp(comparison_value) < 0); // `value` does not contain non escaped `%`
} operator = "=";
return RSVP.resolve(value < comparison_value); }
}; }
/** matchMethod = this[operator];
* Comparison operator, test if this query value is equal or lower than the
* item value
*
* @method <=
* @param {Number, String} object_value The value to compare
* @param {Number, String} comparison_value The comparison value
* @return {Boolean} true if equal or lower, false otherwise
*/
SimpleQuery.prototype["<="] = function (object_value, comparison_value) {
var value;
if (!Array.isArray(object_value)) {
object_value = [object_value];
}
value = object_value[0];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return RSVP.resolve(value.cmp(comparison_value) <= 0);
}
return RSVP.resolve(value <= comparison_value);
};
/** if (this._key_schema.key_set && this._key_schema.key_set[key] !== undefined) {
* Comparison operator, test if this query value is greater than the item key = this._key_schema.key_set[key];
* value }
*
* @method >
* @param {Number, String} object_value The value to compare
* @param {Number, String} comparison_value The comparison value
* @return {Boolean} true if greater, false otherwise
*/
SimpleQuery.prototype[">"] = function (object_value, comparison_value) {
var value;
if (!Array.isArray(object_value)) {
object_value = [object_value];
}
value = object_value[0];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return RSVP.resolve(value.cmp(comparison_value) > 0);
}
return RSVP.resolve(value > comparison_value);
};
/** if (typeof key === 'object') {
* Comparison operator, test if this query value is equal or greater than the checkKey(key);
* item value object_value = item[key.read_from];
*
* @method >=
* @param {Number, String} object_value The value to compare
* @param {Number, String} comparison_value The comparison value
* @return {Boolean} true if equal or greater, false otherwise
*/
SimpleQuery.prototype[">="] = function (object_value, comparison_value) {
var value;
if (!Array.isArray(object_value)) {
object_value = [object_value];
}
value = object_value[0];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return RSVP.resolve(value.cmp(comparison_value) >= 0);
}
return RSVP.resolve(value >= comparison_value);
};
query_class_dict.simple = SimpleQuery; equal_match = key.equal_match;
window.SimpleQuery = SimpleQuery; // equal_match can be a string
;/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ if (typeof equal_match === 'string') {
/*global Query, RSVP, deepClone */ // XXX raise error if equal_match not in match_lookup
equal_match = this._key_schema.match_lookup[equal_match];
}
/** // equal_match overrides the default '=' operator
* Escapes regexp special chars from a string. if (equal_match !== undefined) {
* matchMethod = (operator === "=" || operator === "like" ?
* @param {String} string The string to escape equal_match : matchMethod);
* @return {String} The escaped string }
*/
function stringEscapeRegexpCharacters(string) {
if (typeof string === "string") {
return string.replace(/([\\\.\$\[\]\(\)\{\}\^\?\*\+\-])/g, "\\$1");
}
throw new TypeError("Query.stringEscapeRegexpCharacters(): " +
"Argument no 1 is not of type 'string'");
}
Query.stringEscapeRegexpCharacters = stringEscapeRegexpCharacters; value = this.value;
cast_to = key.cast_to;
if (cast_to) {
// cast_to can be a string
if (typeof cast_to === 'string') {
// XXX raise error if cast_to not in cast_lookup
cast_to = this._key_schema.cast_lookup[cast_to];
}
/** try {
* Convert metadata values to array of strings. ex: value = cast_to(value);
* } catch (e) {
* "a" -> ["a"], value = undefined;
* {"content": "a"} -> ["a"] }
*
* @param {Any} value The metadata value try {
* @return {Array} The value in string array format object_value = cast_to(object_value);
*/ } catch (e) {
function metadataValueToStringArray(value) { object_value = undefined;
var i, new_value = []; }
if (value === undefined) { }
return undefined;
}
if (!Array.isArray(value)) {
value = [value];
}
for (i = 0; i < value.length; i += 1) {
if (typeof value[i] === 'object') {
new_value[i] = value[i].content;
} else { } else {
new_value[i] = value[i]; object_value = item[key];
value = this.value;
}
if (object_value === undefined || value === undefined) {
return false;
}
return matchMethod(object_value, value);
};
/**
* #crossLink "Query/toString:method"
*/
SimpleQuery.prototype.toString = function () {
return (this.key ? this.key + ":" : "") +
(this.operator ? " " + this.operator : "") + ' "' + this.value + '"';
};
/**
* #crossLink "Query/serialized:method"
*/
SimpleQuery.prototype.serialized = function () {
var object = {
"type": "simple",
"key": this.key,
"value": this.value
};
if (this.operator !== undefined) {
object.operator = this.operator;
}
return object;
};
SimpleQuery.prototype.toJSON = SimpleQuery.prototype.serialized;
/**
* Comparison operator, test if this query value matches the item value
*
* @method =
* @param {String} object_value The value to compare
* @param {String} comparison_value The comparison value
* @return {Boolean} true if match, false otherwise
*/
SimpleQuery.prototype["="] = function (object_value, comparison_value) {
var value, i;
if (!Array.isArray(object_value)) {
object_value = [object_value];
}
for (i = 0; i < object_value.length; i += 1) {
value = object_value[i];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return (value.cmp(comparison_value) === 0);
}
if (comparison_value.toString() === value.toString()) {
return true;
}
} }
} return false;
return new_value; };
}
/** /**
* A sort function to sort items by key * Comparison operator, test if this query value matches the item value
* *
* @param {String} key The key to sort on * @method like
* @param {String} [way="ascending"] 'ascending' or 'descending' * @param {String} object_value The value to compare
* @return {Function} The sort function * @param {String} comparison_value The comparison value
*/ * @return {Boolean} true if match, false otherwise
function sortFunction(key, way) { */
var result; SimpleQuery.prototype.like = function (object_value, comparison_value) {
if (way === 'descending') { var value, i;
result = 1; if (!Array.isArray(object_value)) {
} else if (way === 'ascending') { object_value = [object_value];
result = -1; }
} else { for (i = 0; i < object_value.length; i += 1) {
throw new TypeError("Query.sortFunction(): " + value = object_value[i];
"Argument 2 must be 'ascending' or 'descending'"); if (typeof value === 'object' && value.hasOwnProperty('content')) {
} value = value.content;
return function (a, b) {
// this comparison is 5 times faster than json comparison
var i, l;
a = metadataValueToStringArray(a[key]) || [];
b = metadataValueToStringArray(b[key]) || [];
l = a.length > b.length ? a.length : b.length;
for (i = 0; i < l; i += 1) {
if (a[i] === undefined) {
return result;
}
if (b[i] === undefined) {
return -result;
} }
if (a[i] > b[i]) { if (typeof value.cmp === "function") {
return -result; return (value.cmp(comparison_value) === 0);
} }
if (a[i] < b[i]) { if (
return result; searchTextToRegExp(comparison_value.toString()).test(value.toString())
) {
return true;
} }
} }
return 0; return false;
}; };
}
/** /**
* Inherits the prototype methods from one constructor into another. The * Comparison operator, test if this query value does not match the item value
* prototype of `constructor` will be set to a new object created from *
* `superConstructor`. * @method !=
* * @param {String} object_value The value to compare
* @param {Function} constructor The constructor which inherits the super one * @param {String} comparison_value The comparison value
* @param {Function} superConstructor The super constructor * @return {Boolean} true if not match, false otherwise
*/ */
function inherits(constructor, superConstructor) { SimpleQuery.prototype["!="] = function (object_value, comparison_value) {
constructor.super_ = superConstructor; var value, i;
constructor.prototype = Object.create(superConstructor.prototype, { if (!Array.isArray(object_value)) {
"constructor": { object_value = [object_value];
"configurable": true, }
"enumerable": false, for (i = 0; i < object_value.length; i += 1) {
"writable": true, value = object_value[i];
"value": constructor if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return (value.cmp(comparison_value) !== 0);
}
if (comparison_value.toString() === value.toString()) {
return false;
}
} }
}); return true;
} };
/**
* Does nothing
*/
function emptyFunction() {
return;
}
/** /**
* Filter a list of items, modifying them to select only wanted keys. If * Comparison operator, test if this query value is lower than the item value
* `clone` is true, then the method will act on a cloned list. *
* * @method <
* @param {Array} select_option Key list to keep * @param {Number, String} object_value The value to compare
* @param {Array} list The item list to filter * @param {Number, String} comparison_value The comparison value
* @param {Boolean} [clone=false] If true, modifies a clone of the list * @return {Boolean} true if lower, false otherwise
* @return {Array} The filtered list */
*/ SimpleQuery.prototype["<"] = function (object_value, comparison_value) {
function select(select_option, list, clone) { var value;
var i, j, new_item; if (!Array.isArray(object_value)) {
if (!Array.isArray(select_option)) { object_value = [object_value];
throw new TypeError("jioquery.select(): " +
"Argument 1 is not of type Array");
}
if (!Array.isArray(list)) {
throw new TypeError("jioquery.select(): " +
"Argument 2 is not of type Array");
}
if (clone === true) {
list = deepClone(list);
}
for (i = 0; i < list.length; i += 1) {
new_item = {};
for (j = 0; j < select_option.length; j += 1) {
if (list[i].hasOwnProperty([select_option[j]])) {
new_item[select_option[j]] = list[i][select_option[j]];
}
} }
for (j in new_item) { value = object_value[0];
if (new_item.hasOwnProperty(j)) { if (typeof value === 'object' && value.hasOwnProperty('content')) {
list[i] = new_item; value = value.content;
break;
}
} }
} if (typeof value.cmp === "function") {
return list; return (value.cmp(comparison_value) < 0);
} }
return (value < comparison_value);
};
Query.select = select; /**
* Comparison operator, test if this query value is equal or lower than the
* item value
*
* @method <=
* @param {Number, String} object_value The value to compare
* @param {Number, String} comparison_value The comparison value
* @return {Boolean} true if equal or lower, false otherwise
*/
SimpleQuery.prototype["<="] = function (object_value, comparison_value) {
var value;
if (!Array.isArray(object_value)) {
object_value = [object_value];
}
value = object_value[0];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return (value.cmp(comparison_value) <= 0);
}
return (value <= comparison_value);
};
/** /**
* Sort a list of items, according to keys and directions. If `clone` is true, * Comparison operator, test if this query value is greater than the item
* then the method will act on a cloned list. * value
* *
* @param {Array} sort_on_option List of couples [key, direction] * @method >
* @param {Array} list The item list to sort * @param {Number, String} object_value The value to compare
* @param {Boolean} [clone=false] If true, modifies a clone of the list * @param {Number, String} comparison_value The comparison value
* @return {Array} The filtered list * @return {Boolean} true if greater, false otherwise
*/ */
function sortOn(sort_on_option, list, clone) { SimpleQuery.prototype[">"] = function (object_value, comparison_value) {
var sort_index; var value;
if (!Array.isArray(sort_on_option)) { if (!Array.isArray(object_value)) {
throw new TypeError("jioquery.sortOn(): " + object_value = [object_value];
"Argument 1 is not of type 'array'"); }
} value = object_value[0];
if (clone) { if (typeof value === 'object' && value.hasOwnProperty('content')) {
list = deepClone(list); value = value.content;
} }
for (sort_index = sort_on_option.length - 1; sort_index >= 0; if (typeof value.cmp === "function") {
sort_index -= 1) { return (value.cmp(comparison_value) > 0);
list.sort(sortFunction( }
sort_on_option[sort_index][0], return (value > comparison_value);
sort_on_option[sort_index][1] };
));
}
return list;
}
Query.sortOn = sortOn; /**
* Comparison operator, test if this query value is equal or greater than the
* item value
*
* @method >=
* @param {Number, String} object_value The value to compare
* @param {Number, String} comparison_value The comparison value
* @return {Boolean} true if equal or greater, false otherwise
*/
SimpleQuery.prototype[">="] = function (object_value, comparison_value) {
var value;
if (!Array.isArray(object_value)) {
object_value = [object_value];
}
value = object_value[0];
if (typeof value === 'object' && value.hasOwnProperty('content')) {
value = value.content;
}
if (typeof value.cmp === "function") {
return (value.cmp(comparison_value) >= 0);
}
return (value >= comparison_value);
};
/** query_class_dict.simple = SimpleQuery;
* Limit a list of items, according to index and length. If `clone` is true, query_class_dict.complex = ComplexQuery;
* then the method will act on a cloned list.
*
* @param {Array} limit_option A couple [from, length]
* @param {Array} list The item list to limit
* @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Array} The filtered list
*/
function limit(limit_option, list, clone) {
if (!Array.isArray(limit_option)) {
throw new TypeError("jioquery.limit(): " +
"Argument 1 is not of type 'array'");
}
if (!Array.isArray(list)) {
throw new TypeError("jioquery.limit(): " +
"Argument 2 is not of type 'array'");
}
if (clone) {
list = deepClone(list);
}
list.splice(0, limit_option[0]);
if (limit_option[1]) {
list.splice(limit_option[1]);
}
return list;
}
Query.limit = limit; Query.parseStringToObject = parseStringToObject;
Query.objectToSearchText = objectToSearchText;
/** window.Query = Query;
* Convert a search text to a regexp. window.SimpleQuery = SimpleQuery;
* window.ComplexQuery = ComplexQuery;
* @param {String} string The string to convert window.QueryFactory = QueryFactory;
* @param {Boolean} [use_wildcard_character=true] Use wildcard "%" and "_"
* @return {RegExp} The search text regexp
*/
function searchTextToRegExp(string, use_wildcard_characters) {
if (typeof string !== 'string') {
throw new TypeError("jioquery.searchTextToRegExp(): " +
"Argument 1 is not of type 'string'");
}
if (use_wildcard_characters === false) {
return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$");
}
return new RegExp("^" + stringEscapeRegexpCharacters(string).replace(
/%/g,
".*"
).replace(
/_/g,
"."
) + "$");
}
Query.searchTextToRegExp = searchTextToRegExp; }(RSVP, window, parseStringToObject));
;/*global window, moment */ ;/*global window, moment */
/*jslint nomen: true, maxlen: 200*/ /*jslint nomen: true, maxlen: 200*/
(function (window, moment) { (function (window, moment) {
...@@ -5950,70 +5841,6 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -5950,70 +5841,6 @@ Query.searchTextToRegExp = searchTextToRegExp;
} }
util.ajax = ajax; util.ajax = ajax;
/**
* Clones all native object in deep. Managed types: Object, Array, String,
* Number, Boolean, Function, null.
*
* It can also clone object which are serializable, like Date.
*
* To make a class serializable, you need to implement the `toJSON` function
* which returns a JSON representation of the object. The returned value is
* used as first parameter of the object constructor.
*
* @param {A} object The object to clone
* @return {A} The cloned object
*/
function deepClone(object) {
var i, cloned;
if (Array.isArray(object)) {
cloned = [];
for (i = 0; i < object.length; i += 1) {
cloned[i] = deepClone(object[i]);
}
return cloned;
}
if (object === null) {
return null;
}
if (typeof object === 'object') {
if (Object.getPrototypeOf(object) === Object.prototype) {
cloned = {};
for (i in object) {
if (object.hasOwnProperty(i)) {
cloned[i] = deepClone(object[i]);
}
}
return cloned;
}
if (object instanceof Date) {
// XXX this block is to enable phantomjs and browsers compatibility with
// Date.prototype.toJSON when it is an invalid date. In phantomjs, it
// returns `"Invalid Date"` but in browsers it returns `null`. In
// browsers, giving `null` as parameter to `new Date()` doesn't return
// an invalid date.
// Cloning a date with `return new Date(object)` has problems on
// Firefox.
// I don't know why... (Tested on Firefox 23)
if (isFinite(object.getTime())) {
return new Date(object.toJSON());
}
return new Date("Invalid Date");
}
// clone serializable objects
if (typeof object.toJSON === 'function') {
return new (Object.getPrototypeOf(object).constructor)(object.toJSON());
}
// cannot clone
return object;
}
return object;
}
util.deepClone = deepClone;
function readBlobAsText(blob, encoding) { function readBlobAsText(blob, encoding) {
var fr = new FileReader(); var fr = new FileReader();
return new RSVP.Promise(function (resolve, reject, notify) { return new RSVP.Promise(function (resolve, reject, notify) {
...@@ -7525,13 +7352,13 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -7525,13 +7352,13 @@ Query.searchTextToRegExp = searchTextToRegExp;
attachments: {} attachments: {}
}; };
} }
this._database[id].doc = JSON.stringify(metadata); this._database[id].doc = metadata;
return id; return id;
}; };
MemoryStorage.prototype.get = function (id) { MemoryStorage.prototype.get = function (id) {
try { try {
return JSON.parse(this._database[id].doc); return this._database[id].doc;
} catch (error) { } catch (error) {
if (error instanceof TypeError) { if (error instanceof TypeError) {
throw new jIO.util.jIOError( throw new jIO.util.jIOError(
...@@ -7619,18 +7446,26 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -7619,18 +7446,26 @@ Query.searchTextToRegExp = searchTextToRegExp;
MemoryStorage.prototype.hasCapacity = function (name) { MemoryStorage.prototype.hasCapacity = function (name) {
return (name === "list"); return ((name === "list") || (name === "include"));
}; };
MemoryStorage.prototype.buildQuery = function () { MemoryStorage.prototype.buildQuery = function (options) {
var rows = [], var rows = [],
i; i;
for (i in this._database) { for (i in this._database) {
if (this._database.hasOwnProperty(i)) { if (this._database.hasOwnProperty(i)) {
rows.push({ if (options.include_docs === true) {
id: i, rows.push({
value: {} id: i,
}); value: {},
doc: this._database[i]
});
} else {
rows.push({
id: i,
value: {}
});
}
} }
} }
...@@ -8622,6 +8457,250 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -8622,6 +8457,250 @@ Query.searchTextToRegExp = searchTextToRegExp;
jIO.addStorage('dav', DavStorage); jIO.addStorage('dav', DavStorage);
}(jIO, RSVP, DOMParser, Blob)); }(jIO, RSVP, DOMParser, Blob));
;/*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/**
* JIO Google Drive Storage. Type = "gdrive".
* Google Drive "database" storage.
*/
/*global jIO, Blob, RSVP, UriTemplate, JSON*/
/*jslint nomen: true*/
(function (jIO, Blob, RSVP, UriTemplate, JSON) {
"use strict";
var UPLOAD_URL = "https://www.googleapis.com{/upload}/drive/v2/files{/id}" +
"{?uploadType,access_token}",
upload_template = UriTemplate.parse(UPLOAD_URL),
REMOVE_URL = "https://www.googleapis.com/drive/v2/" +
"files{/id,trash}{?access_token}",
remove_template = UriTemplate.parse(REMOVE_URL),
LIST_URL = "https://www.googleapis.com/drive/v2/files" +
"?prettyPrint=false{&pageToken}&q=trashed=false" +
"&fields=nextPageToken,items(id){&access_token}",
list_template = UriTemplate.parse(LIST_URL),
GET_URL = "https://www.googleapis.com/drive/v2/files{/id}{?alt}",
get_template = UriTemplate.parse(GET_URL);
function handleError(error, id) {
if (error.target.status === 404) {
throw new jIO.util.jIOError(
"Cannot find document: " + id,
404
);
}
throw error;
}
function listPage(result, token) {
var i,
obj;
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": "GET",
"url": list_template.expand({
pageToken : (result.nextPageToken || ""),
access_token: token
})
});
})
.push(function (data) {
obj = JSON.parse(data.target.response || data.target.responseText);
for (i = 0; i < obj.items.length; i += 1) {
obj.items[i].value = {};
result.push(obj.items[i]);
}
result.nextPageToken = obj.nextPageToken;
return result;
}, handleError);
}
function checkName(name) {
if (name !== "enclosure") {
throw new jIO.util.jIOError("Only support 'enclosure' attachment", 400);
}
}
/**
* The JIO Google Drive Storage extension
*
* @class GdriveStorage
* @constructor
*/
function GdriveStorage(spec) {
if (spec === undefined || spec.access_token === undefined ||
typeof spec.access_token !== 'string') {
throw new TypeError("Access Token must be a string " +
"which contains more than one character.");
}
if (spec.trashing !== undefined &&
(spec.trashing !== true && spec.trashing !== false)) {
throw new TypeError("trashing parameter" +
" must be a boolean (true or false)");
}
this._trashing = spec.trashing || true;
this._access_token = spec.access_token;
return;
}
function recursiveAllDocs(result, accessToken) {
return new RSVP.Queue()
.push(function () {
return listPage(result, accessToken);
})
.push(function () {
if (result.nextPageToken) {
return recursiveAllDocs(result, accessToken);
}
return result;
});
}
GdriveStorage.prototype.hasCapacity = function (name) {
return (name === "list");
};
GdriveStorage.prototype.buildQuery = function () {
return recursiveAllDocs([], this._access_token);
};
function sendMetaData(id, param, token) {
var boundary = "-------314159265358979323846";
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": id ? "PUT" : "POST",
"url": upload_template.expand({
access_token: token,
id: id || [],
upload: id ? [] : "upload",
uploadType: "multipart"
}),
headers: {
"Content-Type" : 'multipart/related; boundary="' + boundary + '"'
},
data: '--' + boundary + '\n' +
'Content-Type: application/json; charset=UTF-8\n\n' +
JSON.stringify(param) + '\n\n--' + boundary + "--"
});
})
.push(function (result) {
var obj = JSON.parse(result.target.responseText);
return obj.id;
},
function (error) {handleError(error, id); });
}
GdriveStorage.prototype.put = function (id, param) {
return sendMetaData(id, param, this._access_token);
};
GdriveStorage.prototype.post = function (param) {
return sendMetaData(undefined, param, this._access_token);
};
function sendData(id, blob, token) {
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": "PUT",
"url": upload_template.expand({
access_token: token,
upload: "upload",
id: id,
uploadType: "media"
}),
data: blob
});
})
.push(function (data) {
data = JSON.parse(data.target.responseText);
if (data.mimeType === "application/vnd.google-apps.folder") {
throw new jIO.util.jIOError("cannot put attachments to folder", 400);
}
return data;
}, function (error) {handleError(error, id); });
}
GdriveStorage.prototype.putAttachment = function (id, name, blob) {
checkName(name);
return sendData(id, blob, this._access_token);
};
GdriveStorage.prototype.remove = function (id) {
var that = this;
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: that._trashing ? "POST" : "DELETE",
url: remove_template.expand({
id : id,
access_token : that._access_token,
trash : that._trashing ? "trash" : []
})
});
})
.push(undefined, function (error) {handleError(error, id); });
};
function getData(id, attach, token) {
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "GET",
dataType: attach ? "blob" : "json",
url: get_template.expand({
id: id,
alt: attach ? "media" : [],
access_token: token
}),
headers: {
"Authorization" : "Bearer " + token
}
});
})
.push(function (evt) {
return evt.target.response ||
(attach ? new Blob([evt.target.responseText],
{"type" :
evt.target.responseHeaders["Content-Type"]}) :
JSON.parse(evt.target.responseText));
}, function (error) {handleError(error, id); });
}
GdriveStorage.prototype.get = function (id) {
return getData(id, false, this._access_token);
};
GdriveStorage.prototype.getAttachment = function (id, name) {
checkName(name);
return getData(id, true, this._access_token);
};
GdriveStorage.prototype.allAttachments = function (id) {
var token = this._access_token;
return new RSVP.Queue()
.push(function () {
return getData(id, false, token);
})
.push(function (data) {
if (data.mimeType === "application/vnd.google-apps.folder") {
return {};
}
return {"enclosure": {}};
});
};
jIO.addStorage('gdrive', GdriveStorage);
}(jIO, Blob, RSVP, UriTemplate, JSON));
;/*jslint nomen: true */ ;/*jslint nomen: true */
/*global RSVP*/ /*global RSVP*/
...@@ -8880,10 +8959,10 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -8880,10 +8959,10 @@ Query.searchTextToRegExp = searchTextToRegExp;
// } // }
/*jslint nomen: true, unparam: true */ /*jslint nomen: true, unparam: true */
/*global jIO, UriTemplate, FormData, RSVP, URI, Blob, objectToSearchText, /*global jIO, UriTemplate, FormData, RSVP, URI, Blob,
SimpleQuery, ComplexQuery*/ SimpleQuery, ComplexQuery*/
(function (jIO, UriTemplate, FormData, RSVP, URI, Blob, objectToSearchText, (function (jIO, UriTemplate, FormData, RSVP, URI, Blob,
SimpleQuery, ComplexQuery) { SimpleQuery, ComplexQuery) {
"use strict"; "use strict";
...@@ -9340,14 +9419,14 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -9340,14 +9419,14 @@ Query.searchTextToRegExp = searchTextToRegExp;
if (result_list) { if (result_list) {
local_roles = result_list; local_roles = result_list;
parsed_query.query_list.splice(i, 1); parsed_query.query_list.splice(i, 1);
query = objectToSearchText(parsed_query); query = jIO.Query.objectToSearchText(parsed_query);
i = parsed_query.query_list.length; i = parsed_query.query_list.length;
} else { } else {
result_list = isMultipleLocalRoles(sub_query); result_list = isMultipleLocalRoles(sub_query);
if (result_list) { if (result_list) {
local_roles = result_list; local_roles = result_list;
parsed_query.query_list.splice(i, 1); parsed_query.query_list.splice(i, 1);
query = objectToSearchText(parsed_query); query = jIO.Query.objectToSearchText(parsed_query);
i = parsed_query.query_list.length; i = parsed_query.query_list.length;
} }
} }
...@@ -9398,7 +9477,7 @@ Query.searchTextToRegExp = searchTextToRegExp; ...@@ -9398,7 +9477,7 @@ Query.searchTextToRegExp = searchTextToRegExp;
jIO.addStorage("erp5", ERP5Storage); jIO.addStorage("erp5", ERP5Storage);
}(jIO, UriTemplate, FormData, RSVP, URI, Blob, objectToSearchText, }(jIO, UriTemplate, FormData, RSVP, URI, Blob,
SimpleQuery, ComplexQuery)); SimpleQuery, ComplexQuery));
;/*jslint nomen: true*/ ;/*jslint nomen: true*/
/*global RSVP*/ /*global RSVP*/
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "jio", "name": "jio",
"version": "v3.5.0", "version": "v3.6.0",
"license": "LGPLv3", "license": "LGPLv3",
"author": "Nexedi SA", "author": "Nexedi SA",
"contributors": [ "contributors": [
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment