"use strict";
var qs = require("qs");
var Errors = require("./errors");
/**
* オブジェクトの検索を行うモジュールです。
*
* DataStore, User, Role, Fileクラスから呼び出し、それぞれのクラスメソッドとして利用します。
* 検索条件を設定するメソッドに続けてfetch/fetchAllをメソッドチェーンで実行して利用します。
*
* @class Query<T>
*/
var Query = module.exports = (function(){
function Query(ncmb, className){
var tempPrototype = Object.getPrototypeOf(this)
tempPrototype.ncmb = ncmb;
Object.setPrototypeOf(this, tempPrototype);
this._className = className;
this._where = {};
this._limit = 0;
this._skip = 0;
};
/**
* クエリを直接記述して設定します。
*
* @method Query<T>#where
* @param {Object} where JSON形式のクエリオブジェクト
* @return {this}
*/
Query.prototype.where = function(where){
if(typeof where !== "object")
throw new Errors.InvalidWhereError("First argument must object.");
for(var key in where){
if(where.hasOwnProperty(key)){
this._where[key] = where[key];
}
}
return this;
};
/**
* 指定したkeyの値がvalueと等しいオブジェクトを検索します。
*
* @method Query<T>#equalTo
* @param {string} key 値を比較するキー
* @param value 比較する値
* @return {this}
*/
Query.prototype.equalTo = function(key, value){
return setOperand(this, key, value);
};
/**
* 指定したkeyの値がvalueと等しくないオブジェクトを検索します。
*
* @method Query<T>#notEqualTo
* @param {string} key 値を比較するキー
* @param value 比較する値
* @return {this}
*/
Query.prototype.notEqualTo = function(key, value){
return setOperand(this, key, value, "$ne");
};
/**
* 指定したkeyの値がvalueより小さいオブジェクトを検索します。
*
* @method Query<T>#lessThan
* @param {string} key 値を比較するキー
* @param value 比較する値
* @return {this}
*/
Query.prototype.lessThan = function(key, value){
return setOperand(this, key, value, "$lt");
};
/**
* 指定したkeyの値がvalue以下のオブジェクトを検索します。
*
* @method Query<T>#lessThanOrEqualTo
* @param {string} key 値を比較するキー
* @param value 比較する値
* @return {this}
*/
Query.prototype.lessThanOrEqualTo = function(key, value){
return setOperand(this, key, value, "$lte");
};
/**
* 指定したkeyの値がvalueより大きいオブジェクトを検索します。
*
* @method Query<T>#greaterThan
* @param {string} key 値を比較するキー
* @param value 比較する値
* @return {this}
*/
Query.prototype.greaterThan = function(key, value){
return setOperand(this, key, value, "$gt");
};
/**
* 指定したkeyの値がvalue以上のオブジェクトを検索します。
*
* @method Query<T>#greaterThanOrEqualTo
* @param {string} key 値を比較するキー
* @param value 比較する値
* @return {this}
*/
Query.prototype.greaterThanOrEqualTo = function(key, value){
return setOperand(this, key, value, "$gte");
};
/**
* 指定したkeyの値が、配列values内のいずれかと等しいオブジェクトを検索します
*
* @method Query<T>#in
* @param {string} key 値を比較するキー
* @param {Array} values 比較する値
* @return {this}
*/
Query.prototype.in = function(key, values){
if(!Array.isArray(values)) throw new Errors.InvalidArgumentError();
return setOperand(this, key, values, "$in");
};
/**
* 指定したkeyの値が、配列values内のいずれとも等しくないオブジェクトを検索します。
*
* @method Query<T>#notIn
* @param {string} key 値を比較するキー
* @param {Array} values 比較する値
* @return {this}
*/
Query.prototype.notIn = function(key, values){
if(!Array.isArray(values)) throw new Errors.InvalidArgumentError();
return setOperand(this, key, values, "$nin");
};
/**
* 指定したキーに値が存在するオブジェクトを検索します。
* existがtrue(false)の場合、指定したkeyに値が存在する(しない)オブジェクトを検索します。
* 第二引数は省略可。省略時はtrueを指定した場合と同意となります。
*
* @method Query<T>#exists
* @param {string} key 値を比較するキー
* @param {boolean} exist true(false)を設定した場合、値が存在する(しない)オブジェクトを検索する。省略可能。
* @return {this}
*/
Query.prototype.exists = function(key, exist){
if(typeof exist === "undefined" ) exist = true;
if(typeof exist !== "boolean") throw new Errors.InvalidArgumentError();
return setOperand(this, key, exist, "$exists");
};
/**
* 指定したkeyの値が正規表現regexに合致するオブジェクトを検索します。
*
* @method Query<T>#regularExpressionTo
* @param {string} key 値を比較するキー
* @param {string} regex 検索する正規表現
* @return {this}
*/
Query.prototype.regularExpressionTo = function(key, regex){
if(typeof regex !== "string") throw new Errors.InvalidArgumentError();
return setOperand(this, key, regex, "$regex");
};
/**
* 指定したkeyの値が、配列values内のいずれかと等しいオブジェクトを検索します
*
* @method Query<T>#inArray
* @param {string} key 値を比較するキー
* @param {Array} values 比較する値
* @return {this}
*/
Query.prototype.inArray = function(key, values){
if(!Array.isArray(values)) values = [values];
return setOperand(this, key, values, "$inArray");
};
/**
* 指定したkeyの値が、配列values内のいずれとも等しくないオブジェクトを検索します。
*
* @method Query<T>#notInArray
* @param {string} key 値を比較するキー
* @param {Array} values 比較する値
* @return {this}
*/
Query.prototype.notInArray = function(key, values){
if(!Array.isArray(values)) values = [values];
return setOperand(this, key, values, "$ninArray");
};
/**
* 指定したkeyの値が、配列values内のすべての値を含むオブジェクトを検索します。
*
* @method Query<T>#allInArray
* @param {string} key 値を比較するキー
* @param {Array} values 比較する値
* @return {this}
*/
Query.prototype.allInArray = function(key, values){
if(!Array.isArray(values)) values = [values];
return setOperand(this, key, values, "$all");
};
/**
* 指定したキーの位置情報が指定した位置に近い順でオブジェクトを検索します。
*
* @method Query<T>#near
* @param {string} key 値を比較するキー
* @param {NCMB.GeoPoint} location 原点とする位置情報
* @return {this}
*/
Query.prototype.near = function(key, location){
if(!(location instanceof this.ncmb.GeoPoint)){
throw new this.ncmb.Errors.InvalidArgumentError("Second argument must be instance of ncmb.GeoPoint.");
}
return setOperand(this, key, location.toJSON(), "$nearSphere");
};
/**
* 検索範囲内(Km)で、指定したキーの位置情報が指定した位置に近い順でオブジェクトを検索します。
*
* @method Query<T>#withinKilometers
* @param {string} key 値を比較するキー
* @param {NCMB.GeoPoint} location 原点とする位置情報
* @param {number} maxDistance 原点からの検索範囲(Km)
* @return {this}
*/
Query.prototype.withinKilometers = function(key, location, maxDistance){
if(!(location instanceof this.ncmb.GeoPoint)){
throw new Errors.InvalidArgumentError("Location must be instance of ncmb.GeoPoint.");
}
setOperand(this, key, location.toJSON(), "$nearSphere");
this._where[key]["$maxDistanceInKilometers"] = maxDistance;
return this;
};
/**
* 検索範囲内(ml)で、指定したキーの位置情報が指定した位置に近い順でオブジェクトを検索します。
*
* @method Query<T>#withinMiles
* @param {string} key 値を比較するキー
* @param {NCMB.GeoPoint} location 原点とする位置情報
* @param {number} maxDistance 原点からの検索範囲(ml)
* @return {this}
*/
Query.prototype.withinMiles = function(key, location, maxDistance){
if(!(location instanceof this.ncmb.GeoPoint)){
throw new Errors.InvalidArgumentError("Location must be instance of ncmb.GeoPoint.");
}
setOperand(this, key, location.toJSON(), "$nearSphere");
this._where[key]["$maxDistanceInMiles"] = maxDistance;
return this;
};
/**
* 検索範囲内(rad)で、指定したキーの位置情報が指定した位置に近い順でオブジェクトを検索します。
*
* @method Query<T>#withinRadians
* @param {string} key 値を比較するキー
* @param {NCMB.GeoPoint} location 原点とする位置情報
* @param {number} maxDistance 原点からの検索範囲(rad)
* @return {this}
*/
Query.prototype.withinRadians = function(key, location, maxDistance){
if(!(location instanceof this.ncmb.GeoPoint)){
throw new Errors.InvalidArgumentError("Location must be instance of ncmb.GeoPoint.");
}
setOperand(this, key, location.toJSON(), "$nearSphere");
this._where[key]["$maxDistanceInRadians"] = maxDistance;
return this;
};
/**
* 指定したキーの位置情報で、左下(southWestVertex)と右上(northEastVertex)の2地点からなる矩形(長方形)で設定された検索範囲の内部にあるオブジェクトを検索します。
*
* @method Query<T>#withinSquare
* @param {string} key 値を比較するキー
* @param {NCMB.GeoPoint} southWestVertex 検索矩形の左下の頂点
* @param {NCMB.GeoPoint} northEastVertex 検索矩形の右下の頂点
* @return {this}
*/
Query.prototype.withinSquare = function(key, southWestVertex, northEastVertex){
if(!(southWestVertex instanceof this.ncmb.GeoPoint) || !(northEastVertex instanceof this.ncmb.GeoPoint)){
throw new Errors.InvalidArgumentError("Location must be instance of ncmb.GeoPoint.");
}
var box = {"$box":[southWestVertex.toJSON(), northEastVertex.toJSON()]};
setOperand(this, key, box, "$within");
return this;
};
/**
* 複数の検索条件subqueriesを設定し、その検索結果のいずれかに合致するオブジェクトを検索します
* 配列で複数の条件を一度に設定でき、複数回実行することで検索条件を追加できます。
*
* @method Query<T>#or
* @param {Array<Query<T>>|Query<T>} subqueries 検索条件
* @return {this}
*/
Query.prototype.or = function(subqueries){
if(!Array.isArray(subqueries)){
subqueries = [subqueries];
}
this._where = this._where || {};
this._where["$or"] = this._where["$or"] || [];
for(var i = 0; i < subqueries.length; i++){
if(!subqueries[i]._where) throw new Errors.InvalidArgumentError("Argument is invalid. Input query or array of query.");
this._where["$or"].push(subqueries[i]._where);
}
return this;
};
/**
* subqueriesの検索結果のうち、指定したsubkeyとkeyの値が一致するオブジェクトを検索します。
*
* @method Query<T>#select
* @param {string} key メインクエリのクラスで値を比較するキー
* @param {string} subkey サブクエリの検索結果で値を比較するキー
* @param {Query} subquery 検索条件
* @return {this}
*/
Query.prototype.select = function(key, subkey, subquery){
if(typeof key !== "string" || typeof subkey !== "string"){
throw new Errors.InvalidArgumentError("Key and subkey must be string");
}
if(!subquery._className) throw new Errors.InvalidArgumentError("Third argument is invalid. Input query.");
var className = null;
if(subquery._className === "/users"){
className = "user";
}else if(subquery._className === "/roles"){
className = "role";
}else if(subquery._className === "/installations"){
className = "installation";
}else if(subquery._className === "/files"){
className = "file";
}else{
className = subquery._className.slice(9);
}
this._where = this._where || {};
this._where[key] = this._where[key] || {};
if(subquery._limit > 0 && subquery._skip > 0){
this._where[key]["$select"] = {query:{className: className, where: subquery._where, limit:subquery._limit, skip:subquery._skip} , key: subkey};
}else if(subquery._limit > 0){
this._where[key]["$select"] = {query:{className: className, where: subquery._where, limit:subquery._limit} , key: subkey};
}else if(subquery._skip > 0){
this._where[key]["$select"] = {query:{className: className, where: subquery._where, skip:subquery._skip} , key: subkey};
}else{
this._where[key]["$select"] = {query:{className: className, where: subquery._where} , key: subkey};
}
return this;
};
/**
* objectのkeyのプロパティに関連づけられているリレーションの実態(オブジェクト)を検索します。
* objectはmobile backend に保存済みである必要があります。
*
* @method Query<T>#relatedTo
* @param object
* @param {string} key オブジェクトが関連づけられているキー
* @return {this}
*/
Query.prototype.relatedTo = function(object, key){
var className = null;
if(typeof key !== "string") throw new Errors.InvalidArgumentError("Key must be string.");
if(!object.className) throw new Errors.InvalidArgumentError("First argument requires saved object.");
if(!object.objectId){
throw new Errors.NoObjectIdError("First argument requires saved object.");
}
if(object instanceof this.ncmb.User){
className = "user";
}else if(object instanceof this.ncmb.Role){
className = "role";
}else if(object instanceof this.ncmb.Installation){
className = "installation";
}else{
className = object.className.slice(9);
}
this._where = this._where || {};
this._where["$relatedTo"] = {object: {__type: "Pointer", className: className, objectId: object.objectId}, key: key};
return this;
};
/**
* subqueriesの検索結果のうち、指定したkeyに設定されているポインタの実態(オブジェクト)を検索します。
* objectはmobile backend に保存済みである必要がある。
*
* @method Query<T>#inQuery
* @param {string} key ポインタを保存したキー
* @param {Query<T>} subquery 検索条件
* @return {this}
*/
Query.prototype.inQuery = function(key, subquery){
if(typeof key !== "string") throw new Errors.InvalidArgumentError("Key must be string.");
if(!subquery._className) throw new Errors.InvalidArgumentError("Second argument is invalid. Input query.");
var className = null;
if(subquery._className === "/users"){
className = "user";
}else if(subquery._className === "/roles"){
className = "role";
}else if(subquery._className === "/installations"){
className = "installation";
}else if(subquery._className === "/files"){
className = "file";
}else{
className = subquery._className.slice(9);
}
this._where = this._where || {};
this._where[key] = this._where[key] ||{};
if(subquery._limit > 0 && subquery._skip > 0){
this._where[key]["$inQuery"]= {where: subquery._where, className: className, limit:subquery._limit, skip:subquery._skip};
}else if(subquery._limit > 0){
this._where[key]["$inQuery"]= {where: subquery._where, className: className, limit:subquery._limit};
}else if(subquery._skip > 0){
this._where[key]["$inQuery"]= {where: subquery._where, className: className, skip:subquery._skip};
}else{
this._where[key]["$inQuery"]= {where: subquery._where, className: className};
}
return this;
};
/**
* 指定したkeyに設定されているポインタの実態(オブジェクト)を検索し、返却値として返します。
* 複数回実行した場合、最後に設定したキーが反映されます。複数のキーを指定することはできません。
*
* @method Query<T>#include
* @param {string} key ポインタの中身を取得するキー
* @return {this}
*/
Query.prototype.include = function(key){
if(typeof key !== "string") throw new Errors.InvalidArgumentError("Key must be string.");
this._include = key;
return this;
};
/**
* 検索結果の配列と共に、検索結果の総件数を取得するよう設定します。
* 検索結果の配列は最大100件までしか取得しませんが、countは検索結果の総件数を表示します。
* 検索結果配列にcountプロパティとして付加されます。
*
* @method Query<T>#count
* @return {this}
*/
Query.prototype.count = function(){
this._count = 1;
return this;
};
/**
* 指定したkeyをソートして検索結果を取得するよう設定します。
*(複数設定可能。先に指定したkeyが優先ソートされる。)
* フラグによって降順ソートも可能です。降順フラグはキーごとに設定できます。
*
* @method Query<T>#order
* @param {string} key ソートするキー
* @param descending trueを指定した場合、降順でソートされる。省略可能。
* @return {this}
*/
Query.prototype.order = function(key, descending){
var symbol = "";
if(typeof key !== "string") throw new Errors.InvalidArgumentError("Key must be string.");
if(descending && typeof descending !== "boolean"){
throw new Errors.InvalidArgumentError("Second argument must be boolean.");
}
if(descending === true) symbol = "-";
if(!this._order){
this._order = symbol + key;
}else{
this._order = this._order + "," + symbol + key;
}
return this;
};
/**
* 検索結果の最大取得数を設定します。設定値は1から1000まで、デフォルト値は100です。
*
* @method Query<T>#limit
* @param {number} limit 最大取得件数
* @return {this}
*/
Query.prototype.limit = function(limit){
if(typeof limit !== "number"){
throw new Errors.InvalidLimitError("Limit must be number.");
}
if(limit < 1 || limit >1000){
throw new Errors.InvalidLimitError("Limit must be renge of 1~1000.");
}
this._limit = limit;
return this;
};
/**
* 指定したskipの件数だけ頭から除いた検索結果を取得するよう設定します。
*
* @method Query<T>#skip
* @param {number} skip 検索結果から除く件数
* @return {this}
*/
Query.prototype.skip = function(skip){
if(typeof skip !== "number") throw new Errors.InvalidskipError("Skip must be number.");
if(skip < 0) throw new Errors.InvalidskipError("Skip must be greater than 0.");
this._skip = skip;
return this;
};
/**
* objectIdから一意のオブジェクトを取得します。
*
* @method Query<T>#fetchById
* @param {string} id 取得したいオブジェクトのobjectId
* @param {function} [callback] コールバック関数
* @return {Promise<T>} オブジェクト
*/
Query.prototype.fetchById = function(id, callback){
var path = "/" + this.ncmb.version + this._className + "/" + id;
var Klass = this.ncmb.collections[this._className];
if(typeof Klass === "undefined"){
return Promise.reject(new Error("no class definition `" + this._className +"`"));
}
return this.ncmb.request({
path: path,
method: "GET"
}).then(function(data){
var obj = null;
try{
obj = JSON.parse(data);
}catch(err){
throw err;
}
var object = new Klass(obj);
if(object.acl) object.acl = new this.ncmb.Acl(object.acl);
if(callback) return callback(null, object);
return object;
}.bind(this)).catch(function(err){
if(callback) return callback(err, null);
throw err;
});
};
/**
* 検索条件に合致するオブジェクトのうち、先頭の一つだけを取得します。
*
* @method Query<T>#fetch
* @param {function} [callback] コールバック関数
* @return {Promise<T>} 検索結果に合致したオブジェクト
*/
Query.prototype.fetch = function(callback){
this._limit = 1;
return this.fetchAll().then(function(objects){
if(!objects[0]){
if(callback) return callback(null, {});
return {};
}
if(callback) return callback(null, objects[0]);
return objects[0];
}).catch(function(err){
if(callback) return callback(err, null);
throw err;
});
};
/**
* 検索条件に合致するオブジェクトをすべて取得します。
*
* @method Query<T>#fetchAll
* @param {function} [callback] コールバック関数
* @return {Promise<Array<T>>} 検索結果に合致したオブジェクトの配列
*/
Query.prototype.fetchAll = function(callback){
var path = "/" + this.ncmb.version + this._className;
var opts = [];
if(Object.keys(this._where).length !== 0) opts.push("where=" + JSON.stringify(this._where));
if(this._limit) opts.push("limit=" + this._limit);
if(this._skip) opts.push("skip=" + this._skip);
if(this._count) opts.push("count=" + this._count);
if(this._include) opts.push("include=" + this._include);
if(this._order) opts.push("order=" + this._order);
var Klass = this.ncmb.collections[this._className];
if(typeof Klass === "undefined"){
return Promise.reject(new Error("no class definition `" + this._className +"`"));
}
return this.ncmb.request({
path: path,
method: "GET",
query: qs.parse(opts.join("&"))
}).then(function(data){
var objects = null;
try{
objects = JSON.parse(data).results;
objects = objects.map(function(obj){
if(Klass.className === "/files") return obj;
var object = new Klass(obj);
if(object.acl) object.acl = new this.ncmb.Acl(object.acl);
return object;
}.bind(this));
var parsedData = JSON.parse(data)
if("count" in parsedData){
objects.count = parsedData.count;
}
}catch(err){
if(callback) return callback(err, null);
throw err;
}
if(callback) return callback(null, objects);
return objects;
}.bind(this)).catch(function(err){
if(callback) return callback(err, null);
throw err;
});
};
var setOperand = function(query, key, value, operand){
if(typeof key !== "string"){
throw new Errors.InvalidArgumentError("Key must be string.");
}
if(value instanceof Date) {
value = {__type: "Date", iso: value.toJSON()};
}
if(operand === undefined){
query._where = query._where || {};
query._where[key] = value;
return query;
}
query._where = query._where || {};
query._where[key] = query._where[key] || {};
query._where[key][operand] = value;
return query;
};
return Query;
})();