|
|
/*!
|
|
|
* Bmob WeChat applet SDK
|
|
|
* http://www.bmob.cn
|
|
|
* Copyright Bmob, Inc.
|
|
|
* The Bmob WeChat applet SDK is freely distributable under the MIT license.
|
|
|
* (c) 2016-2050 Magic
|
|
|
*/
|
|
|
|
|
|
(function (root) {
|
|
|
var _ = require('underscore.js');
|
|
|
var Bmob = {};
|
|
|
Bmob.VERSION = "js0.0.1";
|
|
|
Bmob._ = _;
|
|
|
|
|
|
var EmptyConstructor = function () { };
|
|
|
|
|
|
if (typeof exports !== 'undefined') {
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
|
exports = module.exports = Bmob;
|
|
|
}
|
|
|
exports.Bmob = Bmob;
|
|
|
} else {
|
|
|
root.Bmob = Bmob;
|
|
|
}
|
|
|
|
|
|
// Helper function to correctly set up the prototype chain, for subclasses.
|
|
|
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
|
|
// class properties to be extended.
|
|
|
var inherits = function (parent, protoProps, staticProps) {
|
|
|
var child;
|
|
|
|
|
|
// The constructor function for the new subclass is either defined by you
|
|
|
// (the "constructor" property in your `extend` definition), or defaulted
|
|
|
// by us to simply call the parent's constructor.
|
|
|
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
|
|
child = protoProps.constructor;
|
|
|
} else {
|
|
|
/** @ignore */
|
|
|
child = function () {
|
|
|
parent.apply(this, arguments);
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// Inherit class (static) properties from parent.
|
|
|
Bmob._.extend(child, parent);
|
|
|
|
|
|
// Set the prototype chain to inherit from `parent`, without calling
|
|
|
// `parent`'s constructor function.
|
|
|
EmptyConstructor.prototype = parent.prototype;
|
|
|
child.prototype = new EmptyConstructor();
|
|
|
|
|
|
// Add prototype properties (instance properties) to the subclass,
|
|
|
// if supplied.
|
|
|
if (protoProps) {
|
|
|
Bmob._.extend(child.prototype, protoProps);
|
|
|
}
|
|
|
|
|
|
// Add static properties to the constructor function, if supplied.
|
|
|
if (staticProps) {
|
|
|
Bmob._.extend(child, staticProps);
|
|
|
}
|
|
|
|
|
|
// Correctly set child's `prototype.constructor`.
|
|
|
child.prototype.constructor = child;
|
|
|
|
|
|
// Set a convenience property in case the parent's prototype is
|
|
|
// needed later.
|
|
|
child.__super__ = parent.prototype;
|
|
|
|
|
|
return child;
|
|
|
};
|
|
|
|
|
|
// Set the server for Bmob to talk to.
|
|
|
Bmob.serverURL = "";
|
|
|
Bmob.fileURL = "http://file.bmob.cn";
|
|
|
|
|
|
// Check whether we are running in Node.js.
|
|
|
if (typeof (process) !== "undefined" && process.versions && process.versions.node) {
|
|
|
Bmob._isNode = true;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 初始化时需要调用这个函数。可以从bmob中获取所需的key
|
|
|
*
|
|
|
* @param {String} applicationId 你的 Application ID.
|
|
|
* @param {String} applicationKey 你的 restful api Key.
|
|
|
* @param {String} masterKey (optional) 你的 bmob Master Key.
|
|
|
*/
|
|
|
Bmob.initialize = function (applicationId, applicationKey, masterKey) {
|
|
|
Bmob._initialize(applicationId, applicationKey, masterKey);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Call this method first to set up authentication tokens for Bmob.
|
|
|
* This method is for Bmob's own private use.
|
|
|
* @param {String} applicationId Your Bmob Application ID.
|
|
|
* @param {String} applicationKey Your Bmob Application Key
|
|
|
|
|
|
*/
|
|
|
Bmob._initialize = function (applicationId, applicationKey, masterKey) {
|
|
|
Bmob.applicationId = applicationId;
|
|
|
Bmob.applicationKey = applicationKey;
|
|
|
Bmob.masterKey = masterKey;
|
|
|
Bmob._useMasterKey = true;
|
|
|
Bmob.serverURL = "https://" + applicationId + ".bmobcloud.com";
|
|
|
};
|
|
|
|
|
|
if (Bmob._isNode) {
|
|
|
Bmob.initialize = Bmob._initialize;
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Returns prefix for localStorage keys used by this instance of Bmob.
|
|
|
* @param {String} path The relative suffix to append to it.
|
|
|
* null or undefined is treated as the empty string.
|
|
|
* @return {String} The full key name.
|
|
|
*/
|
|
|
Bmob._getBmobPath = function (path) {
|
|
|
if (!Bmob.applicationId) {
|
|
|
throw "You need to call Bmob.initialize before using Bmob.";
|
|
|
}
|
|
|
if (!path) {
|
|
|
path = "";
|
|
|
}
|
|
|
if (!Bmob._.isString(path)) {
|
|
|
throw "Tried to get a localStorage path that wasn't a String.";
|
|
|
}
|
|
|
if (path[0] === "/") {
|
|
|
path = path.substring(1);
|
|
|
}
|
|
|
return "Bmob/" + Bmob.applicationId + "/" + path;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Returns prefix for localStorage keys used by this instance of Bmob.
|
|
|
* @param {String} path The relative suffix to append to it.
|
|
|
* null or undefined is treated as the empty string.
|
|
|
* @return {String} The full key name.
|
|
|
*/
|
|
|
Bmob._getBmobPath = function (path) {
|
|
|
if (!Bmob.applicationId) {
|
|
|
throw "You need to call Bmob.initialize before using Bmob.";
|
|
|
}
|
|
|
if (!path) {
|
|
|
path = "";
|
|
|
}
|
|
|
if (!Bmob._.isString(path)) {
|
|
|
throw "Tried to get a localStorage path that wasn't a String.";
|
|
|
}
|
|
|
if (path[0] === "/") {
|
|
|
path = path.substring(1);
|
|
|
}
|
|
|
return "Bmob/" + Bmob.applicationId + "/" + path;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Returns the unique string for this app on this machine.
|
|
|
* Gets reset when localStorage is cleared.
|
|
|
*/
|
|
|
Bmob._installationId = null;
|
|
|
Bmob._getInstallationId = function () {
|
|
|
// See if it's cached in RAM.
|
|
|
if (Bmob._installationId) {
|
|
|
return Bmob._installationId;
|
|
|
}
|
|
|
|
|
|
// Try to get it from localStorage.
|
|
|
var path = Bmob._getBmobPath("installationId");
|
|
|
// Bmob._installationId = Bmob.localStorage.getItem(path);
|
|
|
wx.getStorage({
|
|
|
key: 'key',
|
|
|
success: function (res) {
|
|
|
Bmob._installationId = res.data;
|
|
|
console.log(res.data)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
if (!Bmob._installationId || Bmob._installationId === "") {
|
|
|
// It wasn't in localStorage, so create a new one.
|
|
|
var hexOctet = function () {
|
|
|
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
|
|
};
|
|
|
Bmob._installationId = (hexOctet() + hexOctet() + "-" + hexOctet() + "-" + hexOctet() + "-" + hexOctet() + "-" + hexOctet() + hexOctet() + hexOctet());
|
|
|
|
|
|
wx.setStorage({
|
|
|
key: path,
|
|
|
data: Bmob._installationId
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return Bmob._installationId;
|
|
|
};
|
|
|
|
|
|
Bmob._parseDate = function (iso8601) {
|
|
|
var regexp = new RegExp("^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})" + "T" + "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})" + "(.([0-9]+))?" + "Z$");
|
|
|
var match = regexp.exec(iso8601);
|
|
|
if (!match) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
var year = match[1] || 0;
|
|
|
var month = (match[2] || 1) - 1;
|
|
|
var day = match[3] || 0;
|
|
|
var hour = match[4] || 0;
|
|
|
var minute = match[5] || 0;
|
|
|
var second = match[6] || 0;
|
|
|
var milli = match[8] || 0;
|
|
|
|
|
|
return new Date(Date.UTC(year, month, day, hour, minute, second, milli));
|
|
|
};
|
|
|
|
|
|
Bmob._ajax = function (method, url, data, success, error) {
|
|
|
var options = {
|
|
|
success: success,
|
|
|
error: error
|
|
|
};
|
|
|
var promise = new Bmob.Promise();
|
|
|
|
|
|
var dataObject = JSON.parse(data);
|
|
|
|
|
|
var error;
|
|
|
wx.showNavigationBarLoading()
|
|
|
if (dataObject.category == "wechatApp") {
|
|
|
|
|
|
wx.uploadFile({
|
|
|
url: url,
|
|
|
filePath: dataObject.base64,
|
|
|
name: 'file',
|
|
|
header: {
|
|
|
"X-Bmob-SDK-Type": "wechatApp"
|
|
|
},
|
|
|
formData: dataObject,
|
|
|
success: function (res) {
|
|
|
console.log(res);
|
|
|
var data = JSON.parse(res.data);
|
|
|
promise.resolve(data, res.statusCode, res);
|
|
|
wx.hideNavigationBarLoading()
|
|
|
},
|
|
|
fail: function (e) {
|
|
|
console.log(e);
|
|
|
promise.reject(e);
|
|
|
wx.hideNavigationBarLoading()
|
|
|
}
|
|
|
});
|
|
|
} else {
|
|
|
|
|
|
wx.request({
|
|
|
method: method,
|
|
|
url: url,
|
|
|
data: data,
|
|
|
header: {
|
|
|
'content-type': 'text/plain'
|
|
|
},
|
|
|
success: function (res) {
|
|
|
if (res.data && res.data.code) {
|
|
|
promise.reject(res);
|
|
|
} else if (res.statusCode != 200) {
|
|
|
|
|
|
promise.reject(res);
|
|
|
} else {
|
|
|
promise.resolve(res.data, res.statusCode, res);
|
|
|
}
|
|
|
wx.hideNavigationBarLoading()
|
|
|
},
|
|
|
fail: function (e) {
|
|
|
promise.reject(e);
|
|
|
wx.hideNavigationBarLoading()
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (error) {
|
|
|
return Bmob.Promise.error(error);
|
|
|
}
|
|
|
|
|
|
return promise._thenRunCallbacks(options);
|
|
|
}
|
|
|
|
|
|
// A self-propagating extend function.
|
|
|
Bmob._extend = function (protoProps, classProps) {
|
|
|
var child = inherits(this, protoProps, classProps);
|
|
|
child.extend = this.extend;
|
|
|
return child;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* route is classes, users, login, etc.
|
|
|
* objectId is null if there is no associated objectId.
|
|
|
* method is the http method for the REST API.
|
|
|
* dataObject is the payload as an object, or null if there is none.
|
|
|
* @ignore
|
|
|
*/
|
|
|
Bmob._request = function (route, className, objectId, method, dataObject) {
|
|
|
if (!Bmob.applicationId) {
|
|
|
throw "You must specify your applicationId using Bmob.initialize";
|
|
|
}
|
|
|
|
|
|
if (!Bmob.applicationKey && !Bmob.masterKey) {
|
|
|
throw "You must specify a key using Bmob.initialize";
|
|
|
}
|
|
|
|
|
|
var url = Bmob.serverURL;
|
|
|
if (url.charAt(url.length - 1) !== "/") {
|
|
|
url += "/";
|
|
|
}
|
|
|
if (route.indexOf("2/") < 0) { //如果是使用了2版接口,则不需要加上路由
|
|
|
url += "1/" + route;
|
|
|
} else {
|
|
|
url += route;
|
|
|
}
|
|
|
if (className) {
|
|
|
url += "/" + className;
|
|
|
}
|
|
|
if (objectId) {
|
|
|
url += "/" + objectId;
|
|
|
}
|
|
|
if ((route === 'users' || route === 'classes') && method === 'PUT' && dataObject._fetchWhenSave) {
|
|
|
delete dataObject._fetchWhenSave;
|
|
|
url += '?new=true';
|
|
|
}
|
|
|
|
|
|
dataObject = Bmob._.clone(dataObject || {});
|
|
|
if (method !== "POST") {
|
|
|
dataObject._Method = method;
|
|
|
method = "POST";
|
|
|
}
|
|
|
|
|
|
dataObject._ApplicationId = Bmob.applicationId;
|
|
|
dataObject._RestKey = Bmob.applicationKey;
|
|
|
if (Bmob._useMasterKey && Bmob.masterKey != undefined) {
|
|
|
dataObject._MasterKey = Bmob.masterKey;
|
|
|
}
|
|
|
|
|
|
dataObject._ClientVersion = Bmob.VERSION;
|
|
|
dataObject._InstallationId = Bmob._getInstallationId();
|
|
|
|
|
|
// Pass the session token on every request.
|
|
|
var currentUser = Bmob.User.current();
|
|
|
if (currentUser && currentUser._sessionToken) {
|
|
|
dataObject._SessionToken = currentUser._sessionToken;
|
|
|
}
|
|
|
var data = JSON.stringify(dataObject);
|
|
|
|
|
|
return Bmob._ajax(method, url, data).then(null,
|
|
|
function (response) {
|
|
|
// Transform the error into an instance of Bmob.Error by trying to parse
|
|
|
// the error string as JSON.
|
|
|
var error;
|
|
|
try {
|
|
|
if (response.data.code) {
|
|
|
error = new Bmob.Error(response.data.code, response.data.error);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// If we fail to parse the error text, that's okay.
|
|
|
}
|
|
|
error = error || new Bmob.Error(- 1, response.data);
|
|
|
// By explicitly returning a rejected Promise, this will work with
|
|
|
// either jQuery or Promises/A semantics.
|
|
|
return Bmob.Promise.error(error);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// Helper function to get a value from a Backbone object as a property
|
|
|
// or as a function.
|
|
|
Bmob._getValue = function (object, prop) {
|
|
|
if (!(object && object[prop])) {
|
|
|
return null;
|
|
|
}
|
|
|
return Bmob._.isFunction(object[prop]) ? object[prop]() : object[prop];
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Converts a value in a Bmob Object into the appropriate representation.
|
|
|
* This is the JS equivalent of Java's Bmob.maybeReferenceAndEncode(Object)
|
|
|
* if seenObjects is falsey. Otherwise any Bmob.Objects not in
|
|
|
* seenObjects will be fully embedded rather than encoded
|
|
|
* as a pointer. This array will be used to prevent going into an infinite
|
|
|
* loop because we have circular references. If <seenObjects>
|
|
|
* is set, then none of the Bmob Objects that are serialized can be dirty.
|
|
|
*/
|
|
|
Bmob._encode = function (value, seenObjects, disallowObjects) {
|
|
|
var _ = Bmob._;
|
|
|
if (value instanceof Bmob.Object) {
|
|
|
if (disallowObjects) {
|
|
|
throw "Bmob.Objects not allowed here";
|
|
|
}
|
|
|
if (!seenObjects || _.include(seenObjects, value) || !value._hasData) {
|
|
|
return value._toPointer();
|
|
|
}
|
|
|
if (!value.dirty()) {
|
|
|
seenObjects = seenObjects.concat(value);
|
|
|
return Bmob._encode(value._toFullJSON(seenObjects), seenObjects, disallowObjects);
|
|
|
}
|
|
|
throw "Tried to save an object with a pointer to a new, unsaved object.";
|
|
|
}
|
|
|
if (value instanceof Bmob.ACL) {
|
|
|
return value.toJSON();
|
|
|
}
|
|
|
if (_.isDate(value)) {
|
|
|
return {
|
|
|
"__type": "Date",
|
|
|
"iso": value.toJSON()
|
|
|
};
|
|
|
}
|
|
|
if (value instanceof Bmob.GeoPoint) {
|
|
|
return value.toJSON();
|
|
|
}
|
|
|
if (_.isArray(value)) {
|
|
|
return _.map(value,
|
|
|
function (x) {
|
|
|
return Bmob._encode(x, seenObjects, disallowObjects);
|
|
|
});
|
|
|
}
|
|
|
if (_.isRegExp(value)) {
|
|
|
return value.source;
|
|
|
}
|
|
|
if (value instanceof Bmob.Relation) {
|
|
|
return value.toJSON();
|
|
|
}
|
|
|
if (value instanceof Bmob.Op) {
|
|
|
return value.toJSON();
|
|
|
}
|
|
|
|
|
|
if (value instanceof Bmob.File) {
|
|
|
if (!value.url()) {
|
|
|
throw "Tried to save an object containing an unsaved file.";
|
|
|
}
|
|
|
return {
|
|
|
"__type": "File",
|
|
|
"cdn": value.cdn(),
|
|
|
"filename": value.name(),
|
|
|
"url": value.url()
|
|
|
};
|
|
|
}
|
|
|
if (_.isObject(value)) {
|
|
|
var output = {};
|
|
|
Bmob._objectEach(value,
|
|
|
function (v, k) {
|
|
|
output[k] = Bmob._encode(v, seenObjects, disallowObjects);
|
|
|
});
|
|
|
return output;
|
|
|
}
|
|
|
return value;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* The inverse function of Bmob._encode.
|
|
|
* TODO: make decode not mutate value.
|
|
|
*/
|
|
|
Bmob._decode = function (key, value) {
|
|
|
var _ = Bmob._;
|
|
|
if (!_.isObject(value)) {
|
|
|
return value;
|
|
|
}
|
|
|
if (_.isArray(value)) {
|
|
|
Bmob._arrayEach(value,
|
|
|
function (v, k) {
|
|
|
value[k] = Bmob._decode(k, v);
|
|
|
});
|
|
|
return value;
|
|
|
}
|
|
|
if (value instanceof Bmob.Object) {
|
|
|
return value;
|
|
|
}
|
|
|
if (value instanceof Bmob.File) {
|
|
|
return value;
|
|
|
}
|
|
|
if (value instanceof Bmob.Op) {
|
|
|
return value;
|
|
|
}
|
|
|
if (value.__op) {
|
|
|
return Bmob.Op._decode(value);
|
|
|
}
|
|
|
if (value.__type === "Pointer") {
|
|
|
var className = value.className;
|
|
|
var pointer = Bmob.Object._create(className);
|
|
|
if (value.createdAt) {
|
|
|
delete value.__type;
|
|
|
delete value.className;
|
|
|
pointer._finishFetch(value, true);
|
|
|
} else {
|
|
|
pointer._finishFetch({
|
|
|
objectId: value.objectId
|
|
|
},
|
|
|
false);
|
|
|
}
|
|
|
return pointer;
|
|
|
}
|
|
|
if (value.__type === "Object") {
|
|
|
// It's an Object included in a query result.
|
|
|
var className = value.className;
|
|
|
delete value.__type;
|
|
|
delete value.className;
|
|
|
var object = Bmob.Object._create(className);
|
|
|
object._finishFetch(value, true);
|
|
|
return object;
|
|
|
}
|
|
|
if (value.__type === "Date") {
|
|
|
return value.iso;
|
|
|
}
|
|
|
if (value.__type === "GeoPoint") {
|
|
|
return new Bmob.GeoPoint({
|
|
|
latitude: value.latitude,
|
|
|
longitude: value.longitude
|
|
|
});
|
|
|
}
|
|
|
if (key === "ACL") {
|
|
|
if (value instanceof Bmob.ACL) {
|
|
|
return value;
|
|
|
}
|
|
|
return new Bmob.ACL(value);
|
|
|
}
|
|
|
if (value.__type === "Relation") {
|
|
|
var relation = new Bmob.Relation(null, key);
|
|
|
relation.targetClassName = value.className;
|
|
|
return relation;
|
|
|
}
|
|
|
if (value.__type === "File") {
|
|
|
// var file = new Bmob.File(value.name);
|
|
|
// file._metaData = value.metaData || {};
|
|
|
// file._url = value.url;
|
|
|
// file.id = value.objectId;
|
|
|
if (value.url != undefined && value.url != null) {
|
|
|
|
|
|
if (value.url.indexOf("http") >= 0) {
|
|
|
var file = {
|
|
|
"_name": value.filename,
|
|
|
"_url": value.url,
|
|
|
"_group": value.group
|
|
|
};
|
|
|
} else {
|
|
|
var file = {
|
|
|
"_name": value.filename,
|
|
|
"_url": Bmob.fileURL + "/" + value.url,
|
|
|
"_group": value.group
|
|
|
};
|
|
|
}
|
|
|
} else { //用cdn上传的文件
|
|
|
var file = {
|
|
|
"_name": value.filename,
|
|
|
"_url": value.url,
|
|
|
"_group": value.group
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return file;
|
|
|
}
|
|
|
Bmob._objectEach(value,
|
|
|
function (v, k) {
|
|
|
value[k] = Bmob._decode(k, v);
|
|
|
});
|
|
|
return value;
|
|
|
};
|
|
|
|
|
|
Bmob._arrayEach = Bmob._.each;
|
|
|
|
|
|
/**
|
|
|
* Does a deep traversal of every item in object, calling func on every one.
|
|
|
* @param {Object} object The object or array to traverse deeply.
|
|
|
* @param {Function} func The function to call for every item. It will
|
|
|
* be passed the item as an argument. If it returns a truthy value, that
|
|
|
* value will replace the item in its parent container.
|
|
|
* @returns {} the result of calling func on the top-level object itself.
|
|
|
*/
|
|
|
Bmob._traverse = function (object, func, seen) {
|
|
|
if (object instanceof Bmob.Object) {
|
|
|
seen = seen || [];
|
|
|
if (Bmob._.indexOf(seen, object) >= 0) {
|
|
|
// We've already visited this object in this call.
|
|
|
return;
|
|
|
}
|
|
|
seen.push(object);
|
|
|
Bmob._traverse(object.attributes, func, seen);
|
|
|
return func(object);
|
|
|
}
|
|
|
if (object instanceof Bmob.Relation || object instanceof Bmob.File) {
|
|
|
// Nothing needs to be done, but we don't want to recurse into the
|
|
|
// object's parent infinitely, so we catch this case.
|
|
|
return func(object);
|
|
|
}
|
|
|
if (Bmob._.isArray(object)) {
|
|
|
Bmob._.each(object,
|
|
|
function (child, index) {
|
|
|
var newChild = Bmob._traverse(child, func, seen);
|
|
|
if (newChild) {
|
|
|
object[index] = newChild;
|
|
|
}
|
|
|
});
|
|
|
return func(object);
|
|
|
}
|
|
|
if (Bmob._.isObject(object)) {
|
|
|
Bmob._each(object,
|
|
|
function (child, key) {
|
|
|
var newChild = Bmob._traverse(child, func, seen);
|
|
|
if (newChild) {
|
|
|
object[key] = newChild;
|
|
|
}
|
|
|
});
|
|
|
return func(object);
|
|
|
}
|
|
|
return func(object);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* This is like _.each, except:
|
|
|
* * it doesn't work for so-called array-like objects,
|
|
|
* * it does work for dictionaries with a "length" attribute.
|
|
|
*/
|
|
|
Bmob._objectEach = Bmob._each = function (obj, callback) {
|
|
|
var _ = Bmob._;
|
|
|
if (_.isObject(obj)) {
|
|
|
_.each(_.keys(obj),
|
|
|
function (key) {
|
|
|
callback(obj[key], key);
|
|
|
});
|
|
|
} else {
|
|
|
_.each(obj, callback);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// Helper function to check null or undefined.
|
|
|
Bmob._isNullOrUndefined = function (x) {
|
|
|
return Bmob._.isNull(x) || Bmob._.isUndefined(x);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Constructs a new Bmob.Error object with the given code and message.
|
|
|
* @param {Number} code An error code constant from <code>Bmob.Error</code>.
|
|
|
* @param {String} message A detailed description of the error.
|
|
|
*
|
|
|
* <p>Class used for all objects passed to error callbacks.</p>
|
|
|
*/
|
|
|
Bmob.Error = function (code, message) {
|
|
|
this.code = code;
|
|
|
this.message = message;
|
|
|
};
|
|
|
|
|
|
_.extend(Bmob.Error,
|
|
|
/** @lends Bmob.Error */
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* Error code indicating some error other than those enumerated here.
|
|
|
* @constant
|
|
|
*/
|
|
|
OTHER_CAUSE: -1,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating the specified object doesn't exist.
|
|
|
* @constant
|
|
|
*/
|
|
|
OBJECT_NOT_FOUND: 101,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating you tried to query with a datatype that doesn't
|
|
|
* support it, like exact matching an array or object.
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_QUERY: 102,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating a missing or invalid classname. Classnames are
|
|
|
* case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the
|
|
|
* only valid characters.
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_CLASS_NAME: 103,
|
|
|
|
|
|
/**
|
|
|
* Error code relation className not exists
|
|
|
* @constant
|
|
|
*/
|
|
|
RELATIONDOCNOTEXISTS: 104,
|
|
|
|
|
|
/**
|
|
|
* Error code invalid field name
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_KEY_NAME: 105,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating a malformed pointer. You should not see this unless
|
|
|
* you have been mucking about changing internal Bmob code.
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_POINTER: 106,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating that badly formed JSON was received upstream. This
|
|
|
* either indicates you have done something unusual with modifying how
|
|
|
* things encode to JSON, or the network is failing badly.
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_JSON: 107,
|
|
|
|
|
|
/**
|
|
|
* Error code username and password required
|
|
|
* @constant
|
|
|
*/
|
|
|
USERNAME_PASSWORD_REQUIRED: 108,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating that a field was set to an inconsistent type.
|
|
|
* @constant
|
|
|
*/
|
|
|
INCORRECT_TYPE: 111,
|
|
|
|
|
|
/**
|
|
|
* Error code requests must be an array
|
|
|
* @constant
|
|
|
*/
|
|
|
REQUEST_MUST_ARRAY: 112,
|
|
|
|
|
|
/**
|
|
|
* Error code requests must be LIKE OBJECT
|
|
|
* @constant
|
|
|
*/
|
|
|
REQUEST_MUST_OBJECT: 113,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating that the object is too large.
|
|
|
* @constant
|
|
|
*/
|
|
|
OBJECT_TOO_LARGE: 114,
|
|
|
|
|
|
/**
|
|
|
* Error code geo error
|
|
|
* @constant
|
|
|
*/
|
|
|
GEO_ERROR: 117,
|
|
|
|
|
|
/**
|
|
|
* Error code Email verify should be opened in your app setup page of bmob
|
|
|
* @constant
|
|
|
*/
|
|
|
EMAIL_VERIFY_MUST_OPEN: 120,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating the result was not found in the cache.
|
|
|
* @constant
|
|
|
*/
|
|
|
CACHE_MISS: 120,
|
|
|
|
|
|
/**
|
|
|
* Error code Invalid device token
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_DEVICE_TOKEN: 131,
|
|
|
|
|
|
/**
|
|
|
* Error code Invalid installation ID
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_INSTALLID: 132,
|
|
|
|
|
|
/**
|
|
|
* Error code Invalid device type
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_DEVICE_TYPE: 133,
|
|
|
|
|
|
/**
|
|
|
* Error code device token EXIST
|
|
|
* @constant
|
|
|
*/
|
|
|
DEVICE_TOKEN_EXIST: 134,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating that the email address was invalid.
|
|
|
* @constant
|
|
|
*/
|
|
|
INSTALLID_EXIST: 135,
|
|
|
|
|
|
/**
|
|
|
* Error code DEVICE_TOKEN_NOT_FOR_ANDROID
|
|
|
* @constant
|
|
|
*/
|
|
|
DEVICE_TOKEN_NOT_FOR_ANDROID: 136,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating a missing content length.
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_INSTALL_OPERATE: 137,
|
|
|
|
|
|
/**
|
|
|
* Error code READ_ONLY
|
|
|
* @constant
|
|
|
*/
|
|
|
READ_ONLY: 138,
|
|
|
|
|
|
/**
|
|
|
* Error code Role names must be restricted to alphanumeric characters, dashes(-), underscores(_), and spaces
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_ROLE_NAME: 139,
|
|
|
|
|
|
/**
|
|
|
* Error code MISS_PUSH_DATA
|
|
|
* @constant
|
|
|
*/
|
|
|
MISS_PUSH_DATA: 141,
|
|
|
|
|
|
/**
|
|
|
* Error code INVALID_PUSH_TIME
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_PUSH_TIME: 142,
|
|
|
|
|
|
/**
|
|
|
* Error code INVALID_PUSH_EXPIRE
|
|
|
* @constant
|
|
|
*/
|
|
|
INVALID_PUSH_EXPIRE: 143,
|
|
|
|
|
|
/**
|
|
|
* Error code PUSHTIME cannot before now
|
|
|
* @constant
|
|
|
*/
|
|
|
PUSH_TIME_MUST_BEFORE_NOW: 144,
|
|
|
|
|
|
/**
|
|
|
* Error code file size error
|
|
|
* @constant
|
|
|
*/
|
|
|
FILE_SIZE_ERROR: 145,
|
|
|
|
|
|
/**
|
|
|
* Error code file name error
|
|
|
* @constant
|
|
|
*/
|
|
|
FILE_NAME_ERROR: 146,
|
|
|
FILE_NAME_ERROR: 147,
|
|
|
|
|
|
/**
|
|
|
* Error code file len error
|
|
|
* @constant
|
|
|
*/
|
|
|
FILE_LEN_ERROR: 148,
|
|
|
|
|
|
/**
|
|
|
* Error code file delete error
|
|
|
* @constant
|
|
|
*/
|
|
|
FILE_UPLOAD_ERROR: 150,
|
|
|
|
|
|
/**
|
|
|
* Error code indicating an unsaved file.
|
|
|
* @constant
|
|
|
*/
|
|
|
FILE_DELETE_ERROR: 151,
|
|
|
|
|
|
/**
|
|
|
* Error code image error
|
|
|
*/
|
|
|
IMAGE_ERROR: 160,
|
|
|
|
|
|
/**
|
|
|
* Error code image mode error
|
|
|
* @constant
|
|
|
*/
|
|
|
IMAGE_MODE_ERROR: 161,
|
|
|
|
|
|
/**
|
|
|
* Error code image width error
|
|
|
* @constant
|
|
|
*/
|
|
|
IMAGE_WIDTH_ERROR: 162,
|
|
|
|
|
|
/**
|
|
|
* Error code image height error
|
|
|
* @constant
|
|
|
*/
|
|
|
IMAGE_HEIGHT_ERROR: 163,
|
|
|
|
|
|
/**
|
|
|
* Error code image longEdge error
|
|
|
* @constant
|
|
|
*/
|
|
|
IMAGE_LONGEDGE_ERROR: 164,
|
|
|
|
|
|
/**
|
|
|
* Error code image shortgEdge error
|
|
|
* @constant
|
|
|
*/
|
|
|
IMAGE_SHORTEDGE_ERROR: 165,
|
|
|
|
|
|
/**
|
|
|
* Error code missing
|
|
|
* @constant
|
|
|
*/
|
|
|
USER_MISSING: 201,
|
|
|
|
|
|
/**
|
|
|
* Error code username '%s' already taken
|
|
|
* not be altered.
|
|
|
* @constant
|
|
|
*/
|
|
|
USER_NAME_TOKEN: 202,
|
|
|
|
|
|
/**
|
|
|
* Error code EMAIL already taken
|
|
|
* @constant
|
|
|
*/
|
|
|
EMAIL_EXIST: 203,
|
|
|
|
|
|
/**
|
|
|
* Error code you must provide an email
|
|
|
* @constant
|
|
|
*/
|
|
|
NO_EMAIL: 204,
|
|
|
|
|
|
/**
|
|
|
* Error code no user found with email
|
|
|
* @constant
|
|
|
*/
|
|
|
NOT_FOUND_EMAIL: 205,
|
|
|
|
|
|
/**
|
|
|
* Error code sessionToken Erro
|
|
|
* @constant
|
|
|
*/
|
|
|
SESSIONTOKEN_ERROR: 206,
|
|
|
|
|
|
/**
|
|
|
* Error code valid error
|
|
|
* @constant
|
|
|
*/
|
|
|
VALID_ERROR: 301
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
*
|
|
|
* <p>Bmob.Events 是 fork of Backbone's Events module</p>
|
|
|
*
|
|
|
* <p>A module that can be mixed in to any object in order to provide
|
|
|
* it with custom events. You may bind callback functions to an event
|
|
|
* with `on`, or remove these functions with `off`.
|
|
|
* Triggering an event fires all callbacks in the order that `on` was
|
|
|
* called.
|
|
|
*
|
|
|
* <pre>
|
|
|
* var object = {};
|
|
|
* _.extend(object, Bmob.Events);
|
|
|
* object.on('expand', function(){ alert('expanded'); });
|
|
|
* object.trigger('expand');</pre></p>
|
|
|
*
|
|
|
* <p>For more information, see the
|
|
|
* <a href="http://documentcloud.github.com/backbone/#Events">Backbone
|
|
|
* documentation</a>.</p>
|
|
|
*/
|
|
|
Bmob.Events = {
|
|
|
/**
|
|
|
* Bind one or more space separated events, `events`, to a `callback`
|
|
|
* function. Passing `"all"` will bind the callback to all events fired.
|
|
|
*/
|
|
|
on: function (events, callback, context) {
|
|
|
|
|
|
var calls, event, node, tail, list;
|
|
|
if (!callback) {
|
|
|
return this;
|
|
|
}
|
|
|
events = events.split(eventSplitter);
|
|
|
calls = this._callbacks || (this._callbacks = {});
|
|
|
|
|
|
// Create an immutable callback list, allowing traversal during
|
|
|
// modification. The tail is an empty object that will always be used
|
|
|
// as the next node.
|
|
|
event = events.shift();
|
|
|
while (event) {
|
|
|
list = calls[event];
|
|
|
node = list ? list.tail : {};
|
|
|
node.next = tail = {};
|
|
|
node.context = context;
|
|
|
node.callback = callback;
|
|
|
calls[event] = {
|
|
|
tail: tail,
|
|
|
next: list ? list.next : node
|
|
|
};
|
|
|
event = events.shift();
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Remove one or many callbacks. If `context` is null, removes all callbacks
|
|
|
* with that function. If `callback` is null, removes all callbacks for the
|
|
|
* event. If `events` is null, removes all bound callbacks for all events.
|
|
|
*/
|
|
|
off: function (events, callback, context) {
|
|
|
var event, calls, node, tail, cb, ctx;
|
|
|
|
|
|
// No events, or removing *all* events.
|
|
|
if (!(calls = this._callbacks)) {
|
|
|
return;
|
|
|
}
|
|
|
if (!(events || callback || context)) {
|
|
|
delete this._callbacks;
|
|
|
return this;
|
|
|
}
|
|
|
|
|
|
// Loop through the listed events and contexts, splicing them out of the
|
|
|
// linked list of callbacks if appropriate.
|
|
|
events = events ? events.split(eventSplitter) : _.keys(calls);
|
|
|
event = events.shift();
|
|
|
while (event) {
|
|
|
node = calls[event];
|
|
|
delete calls[event];
|
|
|
if (!node || !(callback || context)) {
|
|
|
continue;
|
|
|
}
|
|
|
// Create a new list, omitting the indicated callbacks.
|
|
|
tail = node.tail;
|
|
|
node = node.next;
|
|
|
while (node !== tail) {
|
|
|
cb = node.callback;
|
|
|
ctx = node.context;
|
|
|
if ((callback && cb !== callback) || (context && ctx !== context)) {
|
|
|
this.on(event, cb, ctx);
|
|
|
}
|
|
|
node = node.next;
|
|
|
}
|
|
|
event = events.shift();
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Trigger one or many events, firing all bound callbacks. Callbacks are
|
|
|
* passed the same arguments as `trigger` is, apart from the event name
|
|
|
* (unless you're listening on `"all"`, which will cause your callback to
|
|
|
* receive the true name of the event as the first argument).
|
|
|
*/
|
|
|
trigger: function (events) {
|
|
|
var event, node, calls, tail, args, all, rest;
|
|
|
if (!(calls = this._callbacks)) {
|
|
|
return this;
|
|
|
}
|
|
|
all = calls.all;
|
|
|
events = events.split(eventSplitter);
|
|
|
rest = slice.call(arguments, 1);
|
|
|
|
|
|
// For each event, walk through the linked list of callbacks twice,
|
|
|
// first to trigger the event, then to trigger any `"all"` callbacks.
|
|
|
event = events.shift();
|
|
|
while (event) {
|
|
|
node = calls[event];
|
|
|
if (node) {
|
|
|
tail = node.tail;
|
|
|
while ((node = node.next) !== tail) {
|
|
|
node.callback.apply(node.context || this, rest);
|
|
|
}
|
|
|
}
|
|
|
node = all;
|
|
|
if (node) {
|
|
|
tail = node.tail;
|
|
|
args = [event].concat(rest);
|
|
|
while ((node = node.next) !== tail) {
|
|
|
node.callback.apply(node.context || this, args);
|
|
|
}
|
|
|
}
|
|
|
event = events.shift();
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @function
|
|
|
*/
|
|
|
Bmob.Events.bind = Bmob.Events.on;
|
|
|
|
|
|
/**
|
|
|
* @function
|
|
|
*/
|
|
|
Bmob.Events.unbind = Bmob.Events.off;
|
|
|
|
|
|
/**
|
|
|
* 通过下面的任意一种形式可以创建GeoPoint<br>
|
|
|
* <pre>
|
|
|
* new GeoPoint(otherGeoPoint)
|
|
|
* new GeoPoint(30, 30)
|
|
|
* new GeoPoint([30, 30])
|
|
|
* new GeoPoint({latitude: 30, longitude: 30})
|
|
|
* new GeoPoint() // defaults to (0, 0)
|
|
|
* </pre>
|
|
|
* @class
|
|
|
*
|
|
|
* <p>在BmobObject中使用坐标点,或者在geo查询中使用</p>
|
|
|
* <p>在一个表中只有一个字段能使用GeoPoint.</p>
|
|
|
*
|
|
|
* <p>Example:<pre>
|
|
|
* var point = new Bmob.GeoPoint(30.0, -20.0);
|
|
|
* var object = new Bmob.Object("PlaceObject");
|
|
|
* object.set("location", point);
|
|
|
* object.save();</pre></p>
|
|
|
*/
|
|
|
Bmob.GeoPoint = function (arg1, arg2) {
|
|
|
if (_.isArray(arg1)) {
|
|
|
Bmob.GeoPoint._validate(arg1[0], arg1[1]);
|
|
|
this.latitude = arg1[0];
|
|
|
this.longitude = arg1[1];
|
|
|
} else if (_.isObject(arg1)) {
|
|
|
Bmob.GeoPoint._validate(arg1.latitude, arg1.longitude);
|
|
|
this.latitude = arg1.latitude;
|
|
|
this.longitude = arg1.longitude;
|
|
|
} else if (_.isNumber(arg1) && _.isNumber(arg2)) {
|
|
|
Bmob.GeoPoint._validate(arg1, arg2);
|
|
|
this.latitude = arg1;
|
|
|
this.longitude = arg2;
|
|
|
} else {
|
|
|
this.latitude = 0;
|
|
|
this.longitude = 0;
|
|
|
}
|
|
|
|
|
|
// Add properties so that anyone using Webkit or Mozilla will get an error
|
|
|
// if they try to set values that are out of bounds.
|
|
|
var self = this;
|
|
|
if (this.__defineGetter__ && this.__defineSetter__) {
|
|
|
// Use _latitude and _longitude to actually store the values, and add
|
|
|
// getters and setters for latitude and longitude.
|
|
|
this._latitude = this.latitude;
|
|
|
this._longitude = this.longitude;
|
|
|
this.__defineGetter__("latitude",
|
|
|
function () {
|
|
|
return self._latitude;
|
|
|
});
|
|
|
this.__defineGetter__("longitude",
|
|
|
function () {
|
|
|
return self._longitude;
|
|
|
});
|
|
|
this.__defineSetter__("latitude",
|
|
|
function (val) {
|
|
|
Bmob.GeoPoint._validate(val, self.longitude);
|
|
|
self._latitude = val;
|
|
|
});
|
|
|
this.__defineSetter__("longitude",
|
|
|
function (val) {
|
|
|
Bmob.GeoPoint._validate(self.latitude, val);
|
|
|
self._longitude = val;
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @lends Bmob.GeoPoint.prototype
|
|
|
* @property {float} latitude North-south portion of the coordinate, in range
|
|
|
* [-90, 90]. Throws an exception if set out of range in a modern browser.
|
|
|
* @property {float} longitude East-west portion of the coordinate, in range
|
|
|
* [-180, 180]. Throws if set out of range in a modern browser.
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* Throws an exception if the given lat-long is out of bounds.
|
|
|
*/
|
|
|
Bmob.GeoPoint._validate = function (latitude, longitude) {
|
|
|
if (latitude < -90.0) {
|
|
|
throw "Bmob.GeoPoint latitude " + latitude + " < -90.0.";
|
|
|
}
|
|
|
if (latitude > 90.0) {
|
|
|
throw "Bmob.GeoPoint latitude " + latitude + " > 90.0.";
|
|
|
}
|
|
|
if (longitude < -180.0) {
|
|
|
throw "Bmob.GeoPoint longitude " + longitude + " < -180.0.";
|
|
|
}
|
|
|
if (longitude > 180.0) {
|
|
|
throw "Bmob.GeoPoint longitude " + longitude + " > 180.0.";
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 使用用户当前的位置创建GeoPoint对象。
|
|
|
* 成功时调用options.success,或者options.error。
|
|
|
* @param {Object} options 调用成功或失败的回调
|
|
|
*/
|
|
|
Bmob.GeoPoint.current = function (options) {
|
|
|
var promise = new Bmob.Promise();
|
|
|
navigator.geolocation.getCurrentPosition(function (location) {
|
|
|
promise.resolve(new Bmob.GeoPoint({
|
|
|
latitude: location.coords.latitude,
|
|
|
longitude: location.coords.longitude
|
|
|
}));
|
|
|
|
|
|
},
|
|
|
function (error) {
|
|
|
promise.reject(error);
|
|
|
});
|
|
|
|
|
|
return promise._thenRunCallbacks(options);
|
|
|
};
|
|
|
|
|
|
Bmob.GeoPoint.prototype = {
|
|
|
/**
|
|
|
* 返回geopoint的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
Bmob.GeoPoint._validate(this.latitude, this.longitude);
|
|
|
return {
|
|
|
"__type": "GeoPoint",
|
|
|
latitude: this.latitude,
|
|
|
longitude: this.longitude
|
|
|
};
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回两个geopoint之间的弧度
|
|
|
* @param {Bmob.GeoPoint} point 另一个Bmob.GeoPoint.
|
|
|
* @return {Number}
|
|
|
*/
|
|
|
radiansTo: function (point) {
|
|
|
var d2r = Math.PI / 180.0;
|
|
|
var lat1rad = this.latitude * d2r;
|
|
|
var long1rad = this.longitude * d2r;
|
|
|
var lat2rad = point.latitude * d2r;
|
|
|
var long2rad = point.longitude * d2r;
|
|
|
var deltaLat = lat1rad - lat2rad;
|
|
|
var deltaLong = long1rad - long2rad;
|
|
|
var sinDeltaLatDiv2 = Math.sin(deltaLat / 2);
|
|
|
var sinDeltaLongDiv2 = Math.sin(deltaLong / 2);
|
|
|
// Square of half the straight line chord distance between both points.
|
|
|
var a = ((sinDeltaLatDiv2 * sinDeltaLatDiv2) + (Math.cos(lat1rad) * Math.cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2));
|
|
|
a = Math.min(1.0, a);
|
|
|
return 2 * Math.asin(Math.sqrt(a));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回两个geopoint之间的千米数
|
|
|
* @param {Bmob.GeoPoint} point 另一个Bmob.GeoPoint.
|
|
|
* @return {Number}
|
|
|
*/
|
|
|
kilometersTo: function (point) {
|
|
|
return this.radiansTo(point) * 6371.0;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回两个geopoint之间的米数
|
|
|
* @param {Bmob.GeoPoint} point 另一个Bmob.GeoPoint.
|
|
|
* @return {Number}
|
|
|
*/
|
|
|
milesTo: function (point) {
|
|
|
return this.radiansTo(point) * 3958.8;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var PUBLIC_KEY = "*";
|
|
|
|
|
|
/**
|
|
|
* 创建ACL
|
|
|
* 如果传任何参数,则任何人都没有权限
|
|
|
* 如果传入的参数是Bmob.User,那个usr会有读写权限。
|
|
|
* 如果传入的参数是json对象,则会有相应的acl权限。
|
|
|
*
|
|
|
* @see Bmob.Object#setACL
|
|
|
* @class
|
|
|
*
|
|
|
* <p>权限控制可以被添加到任何
|
|
|
* <code>Bmob.Object</code>,用来控制用户的访问权限
|
|
|
* </p>
|
|
|
*/
|
|
|
Bmob.ACL = function (arg1) {
|
|
|
var self = this;
|
|
|
self.permissionsById = {};
|
|
|
if (_.isObject(arg1)) {
|
|
|
if (arg1 instanceof Bmob.User) {
|
|
|
self.setReadAccess(arg1, true);
|
|
|
self.setWriteAccess(arg1, true);
|
|
|
} else {
|
|
|
if (_.isFunction(arg1)) {
|
|
|
throw "Bmob.ACL() called with a function. Did you forget ()?";
|
|
|
}
|
|
|
Bmob._objectEach(arg1,
|
|
|
function (accessList, userId) {
|
|
|
if (!_.isString(userId)) {
|
|
|
throw "Tried to create an ACL with an invalid userId.";
|
|
|
}
|
|
|
self.permissionsById[userId] = {};
|
|
|
Bmob._objectEach(accessList,
|
|
|
function (allowed, permission) {
|
|
|
if (permission !== "read" && permission !== "write") {
|
|
|
throw "Tried to create an ACL with an invalid permission type.";
|
|
|
}
|
|
|
if (!_.isBoolean(allowed)) {
|
|
|
throw "Tried to create an ACL with an invalid permission value.";
|
|
|
}
|
|
|
self.permissionsById[userId][permission] = allowed;
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 返回acl的json对象
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
Bmob.ACL.prototype.toJSON = function () {
|
|
|
return _.clone(this.permissionsById);
|
|
|
};
|
|
|
|
|
|
Bmob.ACL.prototype._setAccess = function (accessType, userId, allowed) {
|
|
|
if (userId instanceof Bmob.User) {
|
|
|
userId = userId.id;
|
|
|
} else if (userId instanceof Bmob.Role) {
|
|
|
userId = "role:" + userId.getName();
|
|
|
}
|
|
|
if (!_.isString(userId)) {
|
|
|
throw "userId must be a string.";
|
|
|
}
|
|
|
if (!_.isBoolean(allowed)) {
|
|
|
throw "allowed must be either true or false.";
|
|
|
}
|
|
|
var permissions = this.permissionsById[userId];
|
|
|
if (!permissions) {
|
|
|
if (!allowed) {
|
|
|
// The user already doesn't have this permission, so no action needed.
|
|
|
return;
|
|
|
} else {
|
|
|
permissions = {};
|
|
|
this.permissionsById[userId] = permissions;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (allowed) {
|
|
|
this.permissionsById[userId][accessType] = true;
|
|
|
} else {
|
|
|
delete permissions[accessType];
|
|
|
if (_.isEmpty(permissions)) {
|
|
|
delete permissions[userId];
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
Bmob.ACL.prototype._getAccess = function (accessType, userId) {
|
|
|
if (userId instanceof Bmob.User) {
|
|
|
userId = userId.id;
|
|
|
} else if (userId instanceof Bmob.Role) {
|
|
|
userId = "role:" + userId.getName();
|
|
|
}
|
|
|
var permissions = this.permissionsById[userId];
|
|
|
if (!permissions) {
|
|
|
return false;
|
|
|
}
|
|
|
return permissions[accessType] ? true : false;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 设置是否允许用户读取这个对象
|
|
|
* @param 用户id或对象id,或Bmob.Role
|
|
|
* @param {Boolean} 用户是否有读的权限
|
|
|
*/
|
|
|
Bmob.ACL.prototype.setReadAccess = function (userId, allowed) {
|
|
|
this._setAccess("read", userId, allowed);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 用户是否有读的权限。
|
|
|
* 就算是返回false,用户或许可以访问对象,如果getPublicReadAccess返回ture,或者用户的角色有写的权限。
|
|
|
* @param userId 户id或对象id, 或者Bmob.Role.
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
Bmob.ACL.prototype.getReadAccess = function (userId) {
|
|
|
return this._getAccess("read", userId);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 设置是否允许用户有写的权限
|
|
|
* @param userId 用户id或对象id,或Bmob.Role
|
|
|
* @param {Boolean} 用户是否有写的权限
|
|
|
*/
|
|
|
Bmob.ACL.prototype.setWriteAccess = function (userId, allowed) {
|
|
|
this._setAccess("write", userId, allowed);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 用户是否有写的权限。
|
|
|
* 就算是返回false,用户或许可以访问对象,如果getPublicReadAccess返回ture,或者用户的角色有写的权限。
|
|
|
* @param userId 用户id或对象id,或Bmob.Role
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
Bmob.ACL.prototype.getWriteAccess = function (userId) {
|
|
|
return this._getAccess("write", userId);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 设置所有用户有读的权限。
|
|
|
* @param {Boolean} allowed
|
|
|
*/
|
|
|
Bmob.ACL.prototype.setPublicReadAccess = function (allowed) {
|
|
|
this.setReadAccess(PUBLIC_KEY, allowed);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 是否所有用户有读的权限。
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
Bmob.ACL.prototype.getPublicReadAccess = function () {
|
|
|
return this.getReadAccess(PUBLIC_KEY);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 设置所有用户有写的权限。
|
|
|
* @param {Boolean} allowed
|
|
|
*/
|
|
|
Bmob.ACL.prototype.setPublicWriteAccess = function (allowed) {
|
|
|
this.setWriteAccess(PUBLIC_KEY, allowed);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 是否所有用户有写的权限。
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
Bmob.ACL.prototype.getPublicWriteAccess = function () {
|
|
|
return this.getWriteAccess(PUBLIC_KEY);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 用户所属的角色是否允许读这个对象。就算返回false,这个角色或许有读的权限,如果他的父角色有读的权限。
|
|
|
* @param role 角色名称,或者 Bmob.Role。
|
|
|
* @return {Boolean} 有读的权限返回true。
|
|
|
* @throws {String} role不是Bmob.Role或字符串。
|
|
|
*/
|
|
|
Bmob.ACL.prototype.getRoleReadAccess = function (role) {
|
|
|
if (role instanceof Bmob.Role) {
|
|
|
// Normalize to the String name
|
|
|
role = role.getName();
|
|
|
}
|
|
|
if (_.isString(role)) {
|
|
|
return this.getReadAccess("role:" + role);
|
|
|
}
|
|
|
throw "role must be a Bmob.Role or a String";
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 用户所属的角色是否允许写这个对象。就算返回false,这个角色或许有写的权限,如果他的父角色有写的权限。
|
|
|
* @param role 角色名称,或者 Bmob.Role。
|
|
|
* @return {Boolean} 有写的权限返回true。
|
|
|
* @throws {String} role不是Bmob.Role或字符串。
|
|
|
*/
|
|
|
Bmob.ACL.prototype.getRoleWriteAccess = function (role) {
|
|
|
if (role instanceof Bmob.Role) {
|
|
|
// Normalize to the String name
|
|
|
role = role.getName();
|
|
|
}
|
|
|
if (_.isString(role)) {
|
|
|
return this.getWriteAccess("role:" + role);
|
|
|
}
|
|
|
throw "role must be a Bmob.Role or a String";
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 设置用户所属的角色有读的权限
|
|
|
* @param role 角色名称,或者 Bmob.Role。
|
|
|
* @param {Boolean} 允许角色读这个对象
|
|
|
* @throws {String} role不是Bmob.Role或字符串。
|
|
|
*/
|
|
|
Bmob.ACL.prototype.setRoleReadAccess = function (role, allowed) {
|
|
|
if (role instanceof Bmob.Role) {
|
|
|
// Normalize to the String name
|
|
|
role = role.getName();
|
|
|
}
|
|
|
if (_.isString(role)) {
|
|
|
this.setReadAccess("role:" + role, allowed);
|
|
|
return;
|
|
|
}
|
|
|
throw "role must be a Bmob.Role or a String";
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 设置用户所属的角色有写的权限
|
|
|
* @param role 角色名称,或者 Bmob.Role。
|
|
|
* @param {Boolean} 允许角色写这个对象
|
|
|
* @throws {String} role不是Bmob.Role或字符串。
|
|
|
*/
|
|
|
Bmob.ACL.prototype.setRoleWriteAccess = function (role, allowed) {
|
|
|
if (role instanceof Bmob.Role) {
|
|
|
// Normalize to the String name
|
|
|
role = role.getName();
|
|
|
}
|
|
|
if (_.isString(role)) {
|
|
|
this.setWriteAccess("role:" + role, allowed);
|
|
|
return;
|
|
|
}
|
|
|
throw "role must be a Bmob.Role or a String";
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* A Bmob.Op is an atomic operation that can be applied to a field in a
|
|
|
* Bmob.Object. For example, calling <code>object.set("foo", "bar")</code>
|
|
|
* is an example of a Bmob.Op.Set. Calling <code>object.unset("foo")</code>
|
|
|
* is a Bmob.Op.Unset. These operations are stored in a Bmob.Object and
|
|
|
* sent to the server as part of <code>object.save()</code> operations.
|
|
|
* Instances of Bmob.Op should be immutable.
|
|
|
*
|
|
|
* You should not create subclasses of Bmob.Op or instantiate Bmob.Op
|
|
|
* directly.
|
|
|
*/
|
|
|
Bmob.Op = function () {
|
|
|
this._initialize.apply(this, arguments);
|
|
|
};
|
|
|
|
|
|
Bmob.Op.prototype = {
|
|
|
_initialize: function () { }
|
|
|
};
|
|
|
|
|
|
_.extend(Bmob.Op, {
|
|
|
/**
|
|
|
* To create a new Op, call Bmob.Op._extend();
|
|
|
*/
|
|
|
_extend: Bmob._extend,
|
|
|
|
|
|
// A map of __op string to decoder function.
|
|
|
_opDecoderMap: {},
|
|
|
|
|
|
/**
|
|
|
* Registers a function to convert a json object with an __op field into an
|
|
|
* instance of a subclass of Bmob.Op.
|
|
|
*/
|
|
|
_registerDecoder: function (opName, decoder) {
|
|
|
Bmob.Op._opDecoderMap[opName] = decoder;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Converts a json object into an instance of a subclass of Bmob.Op.
|
|
|
*/
|
|
|
_decode: function (json) {
|
|
|
var decoder = Bmob.Op._opDecoderMap[json.__op];
|
|
|
if (decoder) {
|
|
|
return decoder(json);
|
|
|
} else {
|
|
|
return undefined;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
/*
|
|
|
* Add a handler for Batch ops.
|
|
|
*/
|
|
|
Bmob.Op._registerDecoder("Batch",
|
|
|
function (json) {
|
|
|
var op = null;
|
|
|
Bmob._arrayEach(json.ops,
|
|
|
function (nextOp) {
|
|
|
nextOp = Bmob.Op._decode(nextOp);
|
|
|
op = nextOp._mergeWithPrevious(op);
|
|
|
});
|
|
|
return op;
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* set操作是表明字段的值会在Bmob.Object.set中改变,或者这确定要修改值。
|
|
|
*/
|
|
|
Bmob.Op.Set = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.Set.prototype */
|
|
|
{
|
|
|
_initialize: function (value) {
|
|
|
this._value = value;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回设置后的新值
|
|
|
*/
|
|
|
value: function () {
|
|
|
return this._value;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return Bmob._encode(this.value());
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue) {
|
|
|
return this.value();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* A sentinel value that is returned by Bmob.Op.Unset._estimate to
|
|
|
* indicate the field should be deleted. Basically, if you find _UNSET as a
|
|
|
* value in your object, you should remove that key.
|
|
|
*/
|
|
|
Bmob.Op._UNSET = {};
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* Unset 操作表明字段将会从对象中删除。
|
|
|
*/
|
|
|
Bmob.Op.Unset = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.Unset.prototype */
|
|
|
{
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return {
|
|
|
__op: "Delete"
|
|
|
};
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue) {
|
|
|
return Bmob.Op._UNSET;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Op._registerDecoder("Delete",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.Unset();
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* 将字段的值自增或自减
|
|
|
*/
|
|
|
Bmob.Op.Increment = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.Increment.prototype */
|
|
|
{
|
|
|
|
|
|
_initialize: function (amount) {
|
|
|
this._amount = amount;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回添加的数目。
|
|
|
* @return {Number} 增加或减少的数目。
|
|
|
*/
|
|
|
amount: function () {
|
|
|
return this._amount;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return {
|
|
|
__op: "Increment",
|
|
|
amount: this._amount
|
|
|
};
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
if (!previous) {
|
|
|
return this;
|
|
|
} else if (previous instanceof Bmob.Op.Unset) {
|
|
|
return new Bmob.Op.Set(this.amount());
|
|
|
} else if (previous instanceof Bmob.Op.Set) {
|
|
|
return new Bmob.Op.Set(previous.value() + this.amount());
|
|
|
} else if (previous instanceof Bmob.Op.Increment) {
|
|
|
return new Bmob.Op.Increment(this.amount() + previous.amount());
|
|
|
} else {
|
|
|
throw "Op is invalid after previous op.";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue) {
|
|
|
if (!oldValue) {
|
|
|
return this.amount();
|
|
|
}
|
|
|
return oldValue + this.amount();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Op._registerDecoder("Increment",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.Increment(json.amount);
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* 添加一个对象到数组中,不管元素是否存在。
|
|
|
*/
|
|
|
Bmob.Op.Add = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.Add.prototype */
|
|
|
{
|
|
|
_initialize: function (objects) {
|
|
|
this._objects = objects;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回添加到数组中的对象
|
|
|
* @return {Array} 添加到数组中的对象
|
|
|
*/
|
|
|
objects: function () {
|
|
|
return this._objects;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return {
|
|
|
__op: "Add",
|
|
|
objects: Bmob._encode(this.objects())
|
|
|
};
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
if (!previous) {
|
|
|
return this;
|
|
|
} else if (previous instanceof Bmob.Op.Unset) {
|
|
|
return new Bmob.Op.Set(this.objects());
|
|
|
} else if (previous instanceof Bmob.Op.Set) {
|
|
|
return new Bmob.Op.Set(this._estimate(previous.value()));
|
|
|
} else if (previous instanceof Bmob.Op.Add) {
|
|
|
return new Bmob.Op.Add(previous.objects().concat(this.objects()));
|
|
|
} else {
|
|
|
throw "Op is invalid after previous op.";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue) {
|
|
|
if (!oldValue) {
|
|
|
return _.clone(this.objects());
|
|
|
} else {
|
|
|
return oldValue.concat(this.objects());
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Op._registerDecoder("Add",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.Add(Bmob._decode(undefined, json.objects));
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* 添加一个元素到数组中,当元素已经存在,将不会重复添加。
|
|
|
*/
|
|
|
Bmob.Op.AddUnique = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.AddUnique.prototype */
|
|
|
{
|
|
|
|
|
|
_initialize: function (objects) {
|
|
|
this._objects = _.uniq(objects);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回添加到数组中的对象
|
|
|
* @return {Array} 添加到数组中的对象
|
|
|
*/
|
|
|
objects: function () {
|
|
|
return this._objects;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return {
|
|
|
__op: "AddUnique",
|
|
|
objects: Bmob._encode(this.objects())
|
|
|
};
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
if (!previous) {
|
|
|
return this;
|
|
|
} else if (previous instanceof Bmob.Op.Unset) {
|
|
|
return new Bmob.Op.Set(this.objects());
|
|
|
} else if (previous instanceof Bmob.Op.Set) {
|
|
|
return new Bmob.Op.Set(this._estimate(previous.value()));
|
|
|
} else if (previous instanceof Bmob.Op.AddUnique) {
|
|
|
return new Bmob.Op.AddUnique(this._estimate(previous.objects()));
|
|
|
} else {
|
|
|
throw "Op is invalid after previous op.";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue) {
|
|
|
if (!oldValue) {
|
|
|
return _.clone(this.objects());
|
|
|
} else {
|
|
|
// We can't just take the _.uniq(_.union(...)) of oldValue and
|
|
|
// this.objects, because the uniqueness may not apply to oldValue
|
|
|
// (especially if the oldValue was set via .set())
|
|
|
var newValue = _.clone(oldValue);
|
|
|
Bmob._arrayEach(this.objects(),
|
|
|
function (obj) {
|
|
|
if (obj instanceof Bmob.Object && obj.id) {
|
|
|
var matchingObj = _.find(newValue,
|
|
|
function (anObj) {
|
|
|
return (anObj instanceof Bmob.Object) && (anObj.id === obj.id);
|
|
|
});
|
|
|
if (!matchingObj) {
|
|
|
newValue.push(obj);
|
|
|
} else {
|
|
|
var index = _.indexOf(newValue, matchingObj);
|
|
|
newValue[index] = obj;
|
|
|
}
|
|
|
} else if (!_.contains(newValue, obj)) {
|
|
|
newValue.push(obj);
|
|
|
}
|
|
|
});
|
|
|
return newValue;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Op._registerDecoder("AddUnique",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.AddUnique(Bmob._decode(undefined, json.objects));
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* 从数组中移除一个元素。
|
|
|
*/
|
|
|
Bmob.Op.Remove = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.Remove.prototype */
|
|
|
{
|
|
|
_initialize: function (objects) {
|
|
|
this._objects = _.uniq(objects);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回移除出数组的对象
|
|
|
* @return {Array} 移除出数组的对象
|
|
|
*/
|
|
|
objects: function () {
|
|
|
return this._objects;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return {
|
|
|
__op: "Remove",
|
|
|
objects: Bmob._encode(this.objects())
|
|
|
};
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
if (!previous) {
|
|
|
return this;
|
|
|
} else if (previous instanceof Bmob.Op.Unset) {
|
|
|
return previous;
|
|
|
} else if (previous instanceof Bmob.Op.Set) {
|
|
|
return new Bmob.Op.Set(this._estimate(previous.value()));
|
|
|
} else if (previous instanceof Bmob.Op.Remove) {
|
|
|
return new Bmob.Op.Remove(_.union(previous.objects(), this.objects()));
|
|
|
} else {
|
|
|
throw "Op is invalid after previous op.";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue) {
|
|
|
if (!oldValue) {
|
|
|
return [];
|
|
|
} else {
|
|
|
var newValue = _.difference(oldValue, this.objects());
|
|
|
// If there are saved Bmob Objects being removed, also remove them.
|
|
|
Bmob._arrayEach(this.objects(),
|
|
|
function (obj) {
|
|
|
if (obj instanceof Bmob.Object && obj.id) {
|
|
|
newValue = _.reject(newValue,
|
|
|
function (other) {
|
|
|
return (other instanceof Bmob.Object) && (other.id === obj.id);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
return newValue;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Op._registerDecoder("Remove",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.Remove(Bmob._decode(undefined, json.objects));
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
* 关系操作标明这个字段是Bmob.Relation的实体,同时对象可以从关系中添加或移除
|
|
|
*/
|
|
|
Bmob.Op.Relation = Bmob.Op._extend(
|
|
|
/** @lends Bmob.Op.Relation.prototype */
|
|
|
{
|
|
|
|
|
|
_initialize: function (adds, removes) {
|
|
|
this._targetClassName = null;
|
|
|
|
|
|
var self = this;
|
|
|
|
|
|
var pointerToId = function (object) {
|
|
|
if (object instanceof Bmob.Object) {
|
|
|
if (!object.id) {
|
|
|
throw "You can't add an unsaved Bmob.Object to a relation.";
|
|
|
}
|
|
|
if (!self._targetClassName) {
|
|
|
self._targetClassName = object.className;
|
|
|
}
|
|
|
if (self._targetClassName !== object.className) {
|
|
|
throw "Tried to create a Bmob.Relation with 2 different types: " + self._targetClassName + " and " + object.className + ".";
|
|
|
}
|
|
|
return object.id;
|
|
|
}
|
|
|
return object;
|
|
|
};
|
|
|
|
|
|
this.relationsToAdd = _.uniq(_.map(adds, pointerToId));
|
|
|
this.relationsToRemove = _.uniq(_.map(removes, pointerToId));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回添加到关系中的Bmob.Object的数组对象
|
|
|
* @return {Array}
|
|
|
*/
|
|
|
added: function () {
|
|
|
var self = this;
|
|
|
return _.map(this.relationsToAdd,
|
|
|
function (objectId) {
|
|
|
var object = Bmob.Object._create(self._targetClassName);
|
|
|
object.id = objectId;
|
|
|
return object;
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回移除的Bmob.Object的数组对象
|
|
|
* @return {Array}
|
|
|
*/
|
|
|
removed: function () {
|
|
|
var self = this;
|
|
|
return _.map(this.relationsToRemove,
|
|
|
function (objectId) {
|
|
|
var object = Bmob.Object._create(self._targetClassName);
|
|
|
object.id = objectId;
|
|
|
return object;
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回发送到bmob的json
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
var adds = null;
|
|
|
var removes = null;
|
|
|
var self = this;
|
|
|
var idToPointer = function (id) {
|
|
|
return {
|
|
|
__type: 'Pointer',
|
|
|
className: self._targetClassName,
|
|
|
objectId: id
|
|
|
};
|
|
|
};
|
|
|
var pointers = null;
|
|
|
if (this.relationsToAdd.length > 0) {
|
|
|
pointers = _.map(this.relationsToAdd, idToPointer);
|
|
|
adds = {
|
|
|
"__op": "AddRelation",
|
|
|
"objects": pointers
|
|
|
};
|
|
|
}
|
|
|
|
|
|
if (this.relationsToRemove.length > 0) {
|
|
|
pointers = _.map(this.relationsToRemove, idToPointer);
|
|
|
removes = {
|
|
|
"__op": "RemoveRelation",
|
|
|
"objects": pointers
|
|
|
};
|
|
|
}
|
|
|
|
|
|
if (adds && removes) {
|
|
|
return {
|
|
|
"__op": "Batch",
|
|
|
"ops": [adds, removes]
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return adds || removes || {};
|
|
|
},
|
|
|
|
|
|
_mergeWithPrevious: function (previous) {
|
|
|
if (!previous) {
|
|
|
return this;
|
|
|
} else if (previous instanceof Bmob.Op.Unset) {
|
|
|
throw "You can't modify a relation after deleting it.";
|
|
|
} else if (previous instanceof Bmob.Op.Relation) {
|
|
|
if (previous._targetClassName && previous._targetClassName !== this._targetClassName) {
|
|
|
throw "Related object must be of class " + previous._targetClassName + ", but " + this._targetClassName + " was passed in.";
|
|
|
}
|
|
|
var newAdd = _.union(_.difference(previous.relationsToAdd, this.relationsToRemove), this.relationsToAdd);
|
|
|
var newRemove = _.union(_.difference(previous.relationsToRemove, this.relationsToAdd), this.relationsToRemove);
|
|
|
|
|
|
var newRelation = new Bmob.Op.Relation(newAdd, newRemove);
|
|
|
newRelation._targetClassName = this._targetClassName;
|
|
|
return newRelation;
|
|
|
} else {
|
|
|
throw "Op is invalid after previous op.";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_estimate: function (oldValue, object, key) {
|
|
|
if (!oldValue) {
|
|
|
var relation = new Bmob.Relation(object, key);
|
|
|
relation.targetClassName = this._targetClassName;
|
|
|
} else if (oldValue instanceof Bmob.Relation) {
|
|
|
if (this._targetClassName) {
|
|
|
if (oldValue.targetClassName) {
|
|
|
if (oldValue.targetClassName !== this._targetClassName) {
|
|
|
throw "Related object must be a " + oldValue.targetClassName + ", but a " + this._targetClassName + " was passed in.";
|
|
|
}
|
|
|
} else {
|
|
|
oldValue.targetClassName = this._targetClassName;
|
|
|
}
|
|
|
}
|
|
|
return oldValue;
|
|
|
} else {
|
|
|
throw "Op is invalid after previous op.";
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Op._registerDecoder("AddRelation",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.Relation(Bmob._decode(undefined, json.objects), []);
|
|
|
});
|
|
|
Bmob.Op._registerDecoder("RemoveRelation",
|
|
|
function (json) {
|
|
|
return new Bmob.Op.Relation([], Bmob._decode(undefined, json.objects));
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* Creates a new Relation for the given parent object and key. This
|
|
|
* constructor should rarely be used directly, but rather created by
|
|
|
* Bmob.Object.relation.
|
|
|
* @param {Bmob.Object} parent The parent of this relation.
|
|
|
* @param {String} key The key for this relation on the parent.
|
|
|
* @see Bmob.Object#relation
|
|
|
*
|
|
|
* <p>
|
|
|
* A class that is used to access all of the children of a many-to-many
|
|
|
* relationship. Each instance of Bmob.Relation is associated with a
|
|
|
* particular parent object and key.
|
|
|
* </p>
|
|
|
*/
|
|
|
Bmob.Relation = function (parent, key) {
|
|
|
this.parent = parent;
|
|
|
this.key = key;
|
|
|
this.targetClassName = null;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates a query that can be used to query the parent objects in this relation.
|
|
|
* @param {String} parentClass The parent class or name.
|
|
|
* @param {String} relationKey The relation field key in parent.
|
|
|
* @param {Bmob.Object} child The child object.
|
|
|
* @return {Bmob.Query}
|
|
|
*/
|
|
|
Bmob.Relation.reverseQuery = function (parentClass, relationKey, child) {
|
|
|
var query = new Bmob.Query(parentClass);
|
|
|
query.equalTo(relationKey, child._toPointer());
|
|
|
return query;
|
|
|
};
|
|
|
|
|
|
Bmob.Relation.prototype = {
|
|
|
/**
|
|
|
* Makes sure that this relation has the right parent and key.
|
|
|
*/
|
|
|
_ensureParentAndKey: function (parent, key) {
|
|
|
this.parent = this.parent || parent;
|
|
|
this.key = this.key || key;
|
|
|
if (this.parent !== parent) {
|
|
|
throw "Internal Error. Relation retrieved from two different Objects.";
|
|
|
}
|
|
|
if (this.key !== key) {
|
|
|
throw "Internal Error. Relation retrieved from two different keys.";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Adds a Bmob.Object or an array of Bmob.Objects to the relation.
|
|
|
* @param {} objects The item or items to add.
|
|
|
*/
|
|
|
add: function (objects) {
|
|
|
if (!_.isArray(objects)) {
|
|
|
objects = [objects];
|
|
|
}
|
|
|
|
|
|
var change = new Bmob.Op.Relation(objects, []);
|
|
|
this.parent.set(this.key, change);
|
|
|
this.targetClassName = change._targetClassName;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Removes a Bmob.Object or an array of Bmob.Objects from this relation.
|
|
|
* @param {} objects The item or items to remove.
|
|
|
*/
|
|
|
remove: function (objects) {
|
|
|
if (!_.isArray(objects)) {
|
|
|
objects = [objects];
|
|
|
}
|
|
|
|
|
|
var change = new Bmob.Op.Relation([], objects);
|
|
|
this.parent.set(this.key, change);
|
|
|
this.targetClassName = change._targetClassName;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a JSON version of the object suitable for saving to disk.
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return {
|
|
|
"__type": "Relation",
|
|
|
"className": this.targetClassName
|
|
|
};
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a Bmob.Query that is limited to objects in this
|
|
|
* relation.
|
|
|
* @return {Bmob.Query}
|
|
|
*/
|
|
|
query: function () {
|
|
|
var targetClass;
|
|
|
var query;
|
|
|
if (!this.targetClassName) {
|
|
|
targetClass = Bmob.Object._getSubclass(this.parent.className);
|
|
|
query = new Bmob.Query(targetClass);
|
|
|
query._extraOptions.redirectClassNameForKey = this.key;
|
|
|
} else {
|
|
|
targetClass = Bmob.Object._getSubclass(this.targetClassName);
|
|
|
query = new Bmob.Query(targetClass);
|
|
|
}
|
|
|
query._addCondition("$relatedTo", "object", this.parent._toPointer());
|
|
|
query._addCondition("$relatedTo", "key", this.key);
|
|
|
|
|
|
return query;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* A Promise is returned by async methods as a hook to provide callbacks to be
|
|
|
* called when the async task is fulfilled.
|
|
|
*
|
|
|
* <p>Typical usage would be like:<pre>
|
|
|
* query.findAsync().then(function(results) {
|
|
|
* results[0].set("foo", "bar");
|
|
|
* return results[0].saveAsync();
|
|
|
* }).then(function(result) {
|
|
|
* console.log("Updated " + result.id);
|
|
|
* });
|
|
|
* </pre></p>
|
|
|
*
|
|
|
* @see Bmob.Promise.prototype.next
|
|
|
*/
|
|
|
Bmob.Promise = function () {
|
|
|
this._resolved = false;
|
|
|
this._rejected = false;
|
|
|
this._resolvedCallbacks = [];
|
|
|
this._rejectedCallbacks = [];
|
|
|
};
|
|
|
|
|
|
_.extend(Bmob.Promise,
|
|
|
/** @lends Bmob.Promise */
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* Returns true iff the given object fulfils the Promise interface.
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
is: function (promise) {
|
|
|
return promise && promise.then && _.isFunction(promise.then);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a new promise that is resolved with a given value.
|
|
|
* @return {Bmob.Promise} the new promise.
|
|
|
*/
|
|
|
as: function () {
|
|
|
var promise = new Bmob.Promise();
|
|
|
promise.resolve.apply(promise, arguments);
|
|
|
return promise;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a new promise that is rejected with a given error.
|
|
|
* @return {Bmob.Promise} the new promise.
|
|
|
*/
|
|
|
error: function () {
|
|
|
var promise = new Bmob.Promise();
|
|
|
promise.reject.apply(promise, arguments);
|
|
|
return promise;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a new promise that is fulfilled when all of the input promises
|
|
|
* are resolved. If any promise in the list fails, then the returned promise
|
|
|
* will fail with the last error. If they all succeed, then the returned
|
|
|
* promise will succeed, with the result being an array with the results of
|
|
|
* all the input promises.
|
|
|
* @param {Array} promises a list of promises to wait for.
|
|
|
* @return {Bmob.Promise} the new promise.
|
|
|
*/
|
|
|
when: function (promises) {
|
|
|
// Allow passing in Promises as separate arguments instead of an Array.
|
|
|
var objects;
|
|
|
if (promises && Bmob._isNullOrUndefined(promises.length)) {
|
|
|
objects = arguments;
|
|
|
} else {
|
|
|
objects = promises;
|
|
|
}
|
|
|
|
|
|
var total = objects.length;
|
|
|
var hadError = false;
|
|
|
var results = [];
|
|
|
var errors = [];
|
|
|
results.length = objects.length;
|
|
|
errors.length = objects.length;
|
|
|
|
|
|
if (total === 0) {
|
|
|
return Bmob.Promise.as.apply(this, results);
|
|
|
}
|
|
|
|
|
|
var promise = new Bmob.Promise();
|
|
|
|
|
|
var resolveOne = function () {
|
|
|
total = total - 1;
|
|
|
if (total === 0) {
|
|
|
if (hadError) {
|
|
|
promise.reject(errors);
|
|
|
} else {
|
|
|
promise.resolve.apply(promise, results);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
Bmob._arrayEach(objects,
|
|
|
function (object, i) {
|
|
|
if (Bmob.Promise.is(object)) {
|
|
|
object.then(function (result) {
|
|
|
results[i] = result;
|
|
|
resolveOne();
|
|
|
},
|
|
|
function (error) {
|
|
|
errors[i] = error;
|
|
|
hadError = true;
|
|
|
resolveOne();
|
|
|
});
|
|
|
} else {
|
|
|
results[i] = object;
|
|
|
resolveOne();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return promise;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Runs the given asyncFunction repeatedly, as long as the predicate
|
|
|
* function returns a truthy value. Stops repeating if asyncFunction returns
|
|
|
* a rejected promise.
|
|
|
* @param {Function} predicate should return false when ready to stop.
|
|
|
* @param {Function} asyncFunction should return a Promise.
|
|
|
*/
|
|
|
_continueWhile: function (predicate, asyncFunction) {
|
|
|
if (predicate()) {
|
|
|
return asyncFunction().then(function () {
|
|
|
return Bmob.Promise._continueWhile(predicate, asyncFunction);
|
|
|
});
|
|
|
}
|
|
|
return Bmob.Promise.as();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
_.extend(Bmob.Promise.prototype,
|
|
|
/** @lends Bmob.Promise.prototype */
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* Marks this promise as fulfilled, firing any callbacks waiting on it.
|
|
|
* @param {Object} result the result to pass to the callbacks.
|
|
|
*/
|
|
|
resolve: function (result) {
|
|
|
if (this._resolved || this._rejected) {
|
|
|
throw "A promise was resolved even though it had already been " + (this._resolved ? "resolved" : "rejected") + ".";
|
|
|
}
|
|
|
this._resolved = true;
|
|
|
this._result = arguments;
|
|
|
var results = arguments;
|
|
|
Bmob._arrayEach(this._resolvedCallbacks,
|
|
|
function (resolvedCallback) {
|
|
|
resolvedCallback.apply(this, results);
|
|
|
});
|
|
|
this._resolvedCallbacks = [];
|
|
|
this._rejectedCallbacks = [];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Marks this promise as fulfilled, firing any callbacks waiting on it.
|
|
|
* @param {Object} error the error to pass to the callbacks.
|
|
|
*/
|
|
|
reject: function (error) {
|
|
|
if (this._resolved || this._rejected) {
|
|
|
throw "A promise was rejected even though it had already been " + (this._resolved ? "resolved" : "rejected") + ".";
|
|
|
}
|
|
|
this._rejected = true;
|
|
|
this._error = error;
|
|
|
Bmob._arrayEach(this._rejectedCallbacks,
|
|
|
function (rejectedCallback) {
|
|
|
rejectedCallback(error);
|
|
|
});
|
|
|
this._resolvedCallbacks = [];
|
|
|
this._rejectedCallbacks = [];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Adds callbacks to be called when this promise is fulfilled. Returns a new
|
|
|
* Promise that will be fulfilled when the callback is complete. It allows
|
|
|
* chaining. If the callback itself returns a Promise, then the one returned
|
|
|
* by "then" will not be fulfilled until that one returned by the callback
|
|
|
* is fulfilled.
|
|
|
* @param {Function} resolvedCallback Function that is called when this
|
|
|
* Promise is resolved. Once the callback is complete, then the Promise
|
|
|
* returned by "then" will also be fulfilled.
|
|
|
* @param {Function} rejectedCallback Function that is called when this
|
|
|
* Promise is rejected with an error. Once the callback is complete, then
|
|
|
* the promise returned by "then" with be resolved successfully. If
|
|
|
* rejectedCallback is null, or it returns a rejected Promise, then the
|
|
|
* Promise returned by "then" will be rejected with that error.
|
|
|
* @return {Bmob.Promise} A new Promise that will be fulfilled after this
|
|
|
* Promise is fulfilled and either callback has completed. If the callback
|
|
|
* returned a Promise, then this Promise will not be fulfilled until that
|
|
|
* one is.
|
|
|
*/
|
|
|
then: function (resolvedCallback, rejectedCallback) {
|
|
|
var promise = new Bmob.Promise();
|
|
|
|
|
|
var wrappedResolvedCallback = function () {
|
|
|
var result = arguments;
|
|
|
if (resolvedCallback) {
|
|
|
result = [resolvedCallback.apply(this, result)];
|
|
|
}
|
|
|
if (result.length === 1 && Bmob.Promise.is(result[0])) {
|
|
|
result[0].then(function () {
|
|
|
promise.resolve.apply(promise, arguments);
|
|
|
},
|
|
|
function (error) {
|
|
|
promise.reject(error);
|
|
|
});
|
|
|
} else {
|
|
|
promise.resolve.apply(promise, result);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var wrappedRejectedCallback = function (error) {
|
|
|
var result = [];
|
|
|
if (rejectedCallback) {
|
|
|
result = [rejectedCallback(error)];
|
|
|
if (result.length === 1 && Bmob.Promise.is(result[0])) {
|
|
|
result[0].then(function () {
|
|
|
promise.resolve.apply(promise, arguments);
|
|
|
},
|
|
|
function (error) {
|
|
|
promise.reject(error);
|
|
|
});
|
|
|
} else {
|
|
|
// A Promises/A+ compliant implementation would call:
|
|
|
// promise.resolve.apply(promise, result);
|
|
|
promise.reject(result[0]);
|
|
|
}
|
|
|
} else {
|
|
|
promise.reject(error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
if (this._resolved) {
|
|
|
wrappedResolvedCallback.apply(this, this._result);
|
|
|
} else if (this._rejected) {
|
|
|
wrappedRejectedCallback(this._error);
|
|
|
} else {
|
|
|
this._resolvedCallbacks.push(wrappedResolvedCallback);
|
|
|
this._rejectedCallbacks.push(wrappedRejectedCallback);
|
|
|
}
|
|
|
|
|
|
return promise;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Run the given callbacks after this promise is fulfilled.
|
|
|
* @param optionsOrCallback {} A Backbone-style options callback, or a
|
|
|
* callback function. If this is an options object and contains a "model"
|
|
|
* attributes, that will be passed to error callbacks as the first argument.
|
|
|
* @param model {} If truthy, this will be passed as the first result of
|
|
|
* error callbacks. This is for Backbone-compatability.
|
|
|
* @return {Bmob.Promise} A promise that will be resolved after the
|
|
|
* callbacks are run, with the same result as this.
|
|
|
*/
|
|
|
_thenRunCallbacks: function (optionsOrCallback, model) {
|
|
|
var options;
|
|
|
if (_.isFunction(optionsOrCallback)) {
|
|
|
var callback = optionsOrCallback;
|
|
|
options = {
|
|
|
success: function (result) {
|
|
|
callback(result, null);
|
|
|
},
|
|
|
error: function (error) {
|
|
|
callback(null, error);
|
|
|
}
|
|
|
};
|
|
|
} else {
|
|
|
options = _.clone(optionsOrCallback);
|
|
|
}
|
|
|
options = options || {};
|
|
|
|
|
|
return this.then(function (result) {
|
|
|
if (options.success) {
|
|
|
options.success.apply(this, arguments);
|
|
|
} else if (model) {
|
|
|
// When there's no callback, a sync event should be triggered.
|
|
|
model.trigger('sync', model, result, options);
|
|
|
}
|
|
|
return Bmob.Promise.as.apply(Bmob.Promise, arguments);
|
|
|
},
|
|
|
function (error) {
|
|
|
if (options.error) {
|
|
|
if (!_.isUndefined(model)) {
|
|
|
options.error(model, error);
|
|
|
} else {
|
|
|
options.error(error);
|
|
|
}
|
|
|
} else if (model) {
|
|
|
// When there's no error callback, an error event should be triggered.
|
|
|
model.trigger('error', model, error, options);
|
|
|
}
|
|
|
// By explicitly returning a rejected Promise, this will work with
|
|
|
// either jQuery or Promises/A semantics.
|
|
|
return Bmob.Promise.error(error);
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Adds a callback function that should be called regardless of whether
|
|
|
* this promise failed or succeeded. The callback will be given either the
|
|
|
* array of results for its first argument, or the error as its second,
|
|
|
* depending on whether this Promise was rejected or resolved. Returns a
|
|
|
* new Promise, like "then" would.
|
|
|
* @param {Function} continuation the callback.
|
|
|
*/
|
|
|
_continueWith: function (continuation) {
|
|
|
return this.then(function () {
|
|
|
return continuation(arguments, null);
|
|
|
},
|
|
|
function (error) {
|
|
|
return continuation(null, error);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
var b64Digit = function (number) {
|
|
|
if (number < 26) {
|
|
|
return String.fromCharCode(65 + number);
|
|
|
}
|
|
|
if (number < 52) {
|
|
|
return String.fromCharCode(97 + (number - 26));
|
|
|
}
|
|
|
if (number < 62) {
|
|
|
return String.fromCharCode(48 + (number - 52));
|
|
|
}
|
|
|
if (number === 62) {
|
|
|
return "+";
|
|
|
}
|
|
|
if (number === 63) {
|
|
|
return "/";
|
|
|
}
|
|
|
throw "Tried to encode large digit " + number + " in base64.";
|
|
|
};
|
|
|
|
|
|
var encodeBase64 = function (str) {
|
|
|
var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
|
|
var out, i, len;
|
|
|
var c1, c2, c3;
|
|
|
|
|
|
len = str.length;
|
|
|
i = 0;
|
|
|
out = "";
|
|
|
while (i < len) {
|
|
|
c1 = str.charCodeAt(i++) & 0xff;
|
|
|
if (i == len) {
|
|
|
out += base64EncodeChars.charAt(c1 >> 2);
|
|
|
out += base64EncodeChars.charAt((c1 & 0x3) << 4);
|
|
|
out += "==";
|
|
|
break;
|
|
|
}
|
|
|
c2 = str.charCodeAt(i++);
|
|
|
if (i == len) {
|
|
|
out += base64EncodeChars.charAt(c1 >> 2);
|
|
|
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
|
|
|
out += base64EncodeChars.charAt((c2 & 0xF) << 2);
|
|
|
out += "=";
|
|
|
break;
|
|
|
}
|
|
|
c3 = str.charCodeAt(i++);
|
|
|
out += base64EncodeChars.charAt(c1 >> 2);
|
|
|
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
|
|
|
out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
|
|
|
out += base64EncodeChars.charAt(c3 & 0x3F);
|
|
|
}
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
var utf16to8 = function (str) {
|
|
|
var out, i, len, c;
|
|
|
|
|
|
out = "";
|
|
|
len = str.length;
|
|
|
for (i = 0; i < len; i++) {
|
|
|
c = str.charCodeAt(i);
|
|
|
if ((c >= 0x0001) && (c <= 0x007F)) {
|
|
|
out += str.charAt(i);
|
|
|
} else if (c > 0x07FF) {
|
|
|
out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
|
|
|
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
|
|
|
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
|
|
|
} else {
|
|
|
out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
|
|
|
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
|
|
|
}
|
|
|
}
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
// A list of file extensions to mime types as found here:
|
|
|
// http://stackoverflow.com/questions/58510/using-net-how-can-you-find-the-
|
|
|
// mime-type-of-a-file-based-on-the-file-signature
|
|
|
var mimeTypes = {
|
|
|
ai: "application/postscript",
|
|
|
aif: "audio/x-aiff",
|
|
|
aifc: "audio/x-aiff",
|
|
|
aiff: "audio/x-aiff",
|
|
|
asc: "text/plain",
|
|
|
atom: "application/atom+xml",
|
|
|
au: "audio/basic",
|
|
|
avi: "video/x-msvideo",
|
|
|
bcpio: "application/x-bcpio",
|
|
|
bin: "application/octet-stream",
|
|
|
bmp: "image/bmp",
|
|
|
cdf: "application/x-netcdf",
|
|
|
cgm: "image/cgm",
|
|
|
"class": "application/octet-stream",
|
|
|
cpio: "application/x-cpio",
|
|
|
cpt: "application/mac-compactpro",
|
|
|
csh: "application/x-csh",
|
|
|
css: "text/css",
|
|
|
dcr: "application/x-director",
|
|
|
dif: "video/x-dv",
|
|
|
dir: "application/x-director",
|
|
|
djv: "image/vnd.djvu",
|
|
|
djvu: "image/vnd.djvu",
|
|
|
dll: "application/octet-stream",
|
|
|
dmg: "application/octet-stream",
|
|
|
dms: "application/octet-stream",
|
|
|
doc: "application/msword",
|
|
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml." + "document",
|
|
|
dotx: "application/vnd.openxmlformats-officedocument.wordprocessingml." + "template",
|
|
|
docm: "application/vnd.ms-word.document.macroEnabled.12",
|
|
|
dotm: "application/vnd.ms-word.template.macroEnabled.12",
|
|
|
dtd: "application/xml-dtd",
|
|
|
dv: "video/x-dv",
|
|
|
dvi: "application/x-dvi",
|
|
|
dxr: "application/x-director",
|
|
|
eps: "application/postscript",
|
|
|
etx: "text/x-setext",
|
|
|
exe: "application/octet-stream",
|
|
|
ez: "application/andrew-inset",
|
|
|
gif: "image/gif",
|
|
|
gram: "application/srgs",
|
|
|
grxml: "application/srgs+xml",
|
|
|
gtar: "application/x-gtar",
|
|
|
hdf: "application/x-hdf",
|
|
|
hqx: "application/mac-binhex40",
|
|
|
htm: "text/html",
|
|
|
html: "text/html",
|
|
|
ice: "x-conference/x-cooltalk",
|
|
|
ico: "image/x-icon",
|
|
|
ics: "text/calendar",
|
|
|
ief: "image/ief",
|
|
|
ifb: "text/calendar",
|
|
|
iges: "model/iges",
|
|
|
igs: "model/iges",
|
|
|
jnlp: "application/x-java-jnlp-file",
|
|
|
jp2: "image/jp2",
|
|
|
jpe: "image/jpeg",
|
|
|
jpeg: "image/jpeg",
|
|
|
jpg: "image/jpeg",
|
|
|
js: "application/x-javascript",
|
|
|
kar: "audio/midi",
|
|
|
latex: "application/x-latex",
|
|
|
lha: "application/octet-stream",
|
|
|
lzh: "application/octet-stream",
|
|
|
m3u: "audio/x-mpegurl",
|
|
|
m4a: "audio/mp4a-latm",
|
|
|
m4b: "audio/mp4a-latm",
|
|
|
m4p: "audio/mp4a-latm",
|
|
|
m4u: "video/vnd.mpegurl",
|
|
|
m4v: "video/x-m4v",
|
|
|
mac: "image/x-macpaint",
|
|
|
man: "application/x-troff-man",
|
|
|
mathml: "application/mathml+xml",
|
|
|
me: "application/x-troff-me",
|
|
|
mesh: "model/mesh",
|
|
|
mid: "audio/midi",
|
|
|
midi: "audio/midi",
|
|
|
mif: "application/vnd.mif",
|
|
|
mov: "video/quicktime",
|
|
|
movie: "video/x-sgi-movie",
|
|
|
mp2: "audio/mpeg",
|
|
|
mp3: "audio/mpeg",
|
|
|
mp4: "video/mp4",
|
|
|
mpe: "video/mpeg",
|
|
|
mpeg: "video/mpeg",
|
|
|
mpg: "video/mpeg",
|
|
|
mpga: "audio/mpeg",
|
|
|
ms: "application/x-troff-ms",
|
|
|
msh: "model/mesh",
|
|
|
mxu: "video/vnd.mpegurl",
|
|
|
nc: "application/x-netcdf",
|
|
|
oda: "application/oda",
|
|
|
ogg: "application/ogg",
|
|
|
pbm: "image/x-portable-bitmap",
|
|
|
pct: "image/pict",
|
|
|
pdb: "chemical/x-pdb",
|
|
|
pdf: "application/pdf",
|
|
|
pgm: "image/x-portable-graymap",
|
|
|
pgn: "application/x-chess-pgn",
|
|
|
pic: "image/pict",
|
|
|
pict: "image/pict",
|
|
|
png: "image/png",
|
|
|
pnm: "image/x-portable-anymap",
|
|
|
pnt: "image/x-macpaint",
|
|
|
pntg: "image/x-macpaint",
|
|
|
ppm: "image/x-portable-pixmap",
|
|
|
ppt: "application/vnd.ms-powerpoint",
|
|
|
pptx: "application/vnd.openxmlformats-officedocument.presentationml." + "presentation",
|
|
|
potx: "application/vnd.openxmlformats-officedocument.presentationml." + "template",
|
|
|
ppsx: "application/vnd.openxmlformats-officedocument.presentationml." + "slideshow",
|
|
|
ppam: "application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
|
|
pptm: "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
|
potm: "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
|
|
ppsm: "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
|
ps: "application/postscript",
|
|
|
qt: "video/quicktime",
|
|
|
qti: "image/x-quicktime",
|
|
|
qtif: "image/x-quicktime",
|
|
|
ra: "audio/x-pn-realaudio",
|
|
|
ram: "audio/x-pn-realaudio",
|
|
|
ras: "image/x-cmu-raster",
|
|
|
rdf: "application/rdf+xml",
|
|
|
rgb: "image/x-rgb",
|
|
|
rm: "application/vnd.rn-realmedia",
|
|
|
roff: "application/x-troff",
|
|
|
rtf: "text/rtf",
|
|
|
rtx: "text/richtext",
|
|
|
sgm: "text/sgml",
|
|
|
sgml: "text/sgml",
|
|
|
sh: "application/x-sh",
|
|
|
shar: "application/x-shar",
|
|
|
silo: "model/mesh",
|
|
|
sit: "application/x-stuffit",
|
|
|
skd: "application/x-koan",
|
|
|
skm: "application/x-koan",
|
|
|
skp: "application/x-koan",
|
|
|
skt: "application/x-koan",
|
|
|
smi: "application/smil",
|
|
|
smil: "application/smil",
|
|
|
snd: "audio/basic",
|
|
|
so: "application/octet-stream",
|
|
|
spl: "application/x-futuresplash",
|
|
|
src: "application/x-wais-source",
|
|
|
sv4cpio: "application/x-sv4cpio",
|
|
|
sv4crc: "application/x-sv4crc",
|
|
|
svg: "image/svg+xml",
|
|
|
swf: "application/x-shockwave-flash",
|
|
|
t: "application/x-troff",
|
|
|
tar: "application/x-tar",
|
|
|
tcl: "application/x-tcl",
|
|
|
tex: "application/x-tex",
|
|
|
texi: "application/x-texinfo",
|
|
|
texinfo: "application/x-texinfo",
|
|
|
tif: "image/tiff",
|
|
|
tiff: "image/tiff",
|
|
|
tr: "application/x-troff",
|
|
|
tsv: "text/tab-separated-values",
|
|
|
txt: "text/plain",
|
|
|
ustar: "application/x-ustar",
|
|
|
vcd: "application/x-cdlink",
|
|
|
vrml: "model/vrml",
|
|
|
vxml: "application/voicexml+xml",
|
|
|
wav: "audio/x-wav",
|
|
|
wbmp: "image/vnd.wap.wbmp",
|
|
|
wbmxl: "application/vnd.wap.wbxml",
|
|
|
wml: "text/vnd.wap.wml",
|
|
|
wmlc: "application/vnd.wap.wmlc",
|
|
|
wmls: "text/vnd.wap.wmlscript",
|
|
|
wmlsc: "application/vnd.wap.wmlscriptc",
|
|
|
wrl: "model/vrml",
|
|
|
xbm: "image/x-xbitmap",
|
|
|
xht: "application/xhtml+xml",
|
|
|
xhtml: "application/xhtml+xml",
|
|
|
xls: "application/vnd.ms-excel",
|
|
|
xml: "application/xml",
|
|
|
xpm: "image/x-xpixmap",
|
|
|
xsl: "application/xml",
|
|
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
xltx: "application/vnd.openxmlformats-officedocument.spreadsheetml." + "template",
|
|
|
xlsm: "application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
|
xltm: "application/vnd.ms-excel.template.macroEnabled.12",
|
|
|
xlam: "application/vnd.ms-excel.addin.macroEnabled.12",
|
|
|
xlsb: "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
|
xslt: "application/xslt+xml",
|
|
|
xul: "application/vnd.mozilla.xul+xml",
|
|
|
xwd: "image/x-xwindowdump",
|
|
|
xyz: "chemical/x-xyz",
|
|
|
zip: "application/zip"
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Reads a File using a FileReader.
|
|
|
* @param file {File} the File to read.
|
|
|
* @param type {String} (optional) the mimetype to override with.
|
|
|
* @return {Bmob.Promise} A Promise that will be fulfilled with a
|
|
|
* base64-encoded string of the data and its mime type.
|
|
|
*/
|
|
|
var readAsync = function (file, type) {
|
|
|
var promise = new Bmob.Promise();
|
|
|
|
|
|
if (typeof (FileReader) === "undefined") {
|
|
|
return Bmob.Promise.error(new Bmob.Error(- 1, "Attempted to use a FileReader on an unsupported browser."));
|
|
|
}
|
|
|
|
|
|
var reader = new FileReader();
|
|
|
reader.onloadend = function () {
|
|
|
|
|
|
promise.resolve(reader.result);
|
|
|
};
|
|
|
reader.readAsBinaryString(file);
|
|
|
return promise;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Bmob.File 保存文件到bmob
|
|
|
* cloud.
|
|
|
* @param name {String} 文件名。在服务器中,这会改为唯一的文件名
|
|
|
* @param data {file} 文件的数据
|
|
|
*
|
|
|
* 文件对象是在" file upload control"中被选中,只能在下面的浏览器使用
|
|
|
* in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+.
|
|
|
* 例如:<pre>
|
|
|
*
|
|
|
* var fileUploadControl = $("#profilePhotoFileUpload")[0];
|
|
|
* if (fileUploadControl.files.length > 0) {
|
|
|
* var file = fileUploadControl.files[0];
|
|
|
* var name = "photo.jpg";
|
|
|
* var bmobFile = new Bmob.File(name, file);
|
|
|
* bmobFile.save().then(function() {
|
|
|
* // The file has been saved to Bmob.
|
|
|
* }, function(error) {
|
|
|
* // The file either could not be read, or could not be saved to Bmob.
|
|
|
* });
|
|
|
* }</pre>
|
|
|
* @param type {String} 文件的类型.
|
|
|
*/
|
|
|
Bmob.File = function (name, data, type) {
|
|
|
data = data[0];
|
|
|
this._name = name;
|
|
|
// this._name = encodeBase64(utf16to8(name));
|
|
|
var currentUser = Bmob.User.current();
|
|
|
this._metaData = {
|
|
|
owner: (currentUser != null ? currentUser.id : 'unknown')
|
|
|
};
|
|
|
|
|
|
// Guess the content type from the extension if we need to.
|
|
|
var extension = /\.([^.]*)$/.exec(name);
|
|
|
if (extension) {
|
|
|
extension = extension[1].toLowerCase();
|
|
|
}
|
|
|
var guessedType = type || mimeTypes[extension] || "text/plain";
|
|
|
this._guessedType = guessedType;
|
|
|
|
|
|
if (typeof (File) !== "undefined" && data instanceof File) {
|
|
|
this._source = readAsync(data, type);
|
|
|
} else {
|
|
|
// throw "Creating a Bmob.File from a String is not yet supported.";
|
|
|
this._source = Bmob.Promise.as(data, guessedType);
|
|
|
this._metaData.size = data.length;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Bmob.File.prototype = {
|
|
|
|
|
|
/**
|
|
|
* Gets the name of the file. Before save is called, this is the filename
|
|
|
* given by the user. After save is called, that name gets prefixed with a
|
|
|
* unique identifier.
|
|
|
*/
|
|
|
name: function () {
|
|
|
return this._name;
|
|
|
},
|
|
|
|
|
|
setName: function (name) {
|
|
|
this._name = name;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets the url of the file. It is only available after you save the file or
|
|
|
* after you get the file from a Bmob.Object.
|
|
|
* @return {String}
|
|
|
*/
|
|
|
url: function () {
|
|
|
return this._url;
|
|
|
},
|
|
|
|
|
|
setUrl: function (url) {
|
|
|
this._url = url;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets the group of the file. It is only available after you save the file or
|
|
|
* after you get the file from a Bmob.Object.
|
|
|
* @return {String}
|
|
|
*/
|
|
|
cdn: function () {
|
|
|
return this._cdn;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* <p>Returns the file's metadata JSON object if no arguments is given.Returns the
|
|
|
* metadata value if a key is given.Set metadata value if key and value are both given.</p>
|
|
|
* <p><pre>
|
|
|
* var metadata = file.metaData(); //Get metadata JSON object.
|
|
|
* var size = file.metaData('size'); // Get the size metadata value.
|
|
|
* file.metaData('format', 'jpeg'); //set metadata attribute and value.
|
|
|
*</pre></p>
|
|
|
* @return {Object} The file's metadata JSON object.
|
|
|
* @param {String} attr an optional metadata key.
|
|
|
* @param {Object} value an optional metadata value.
|
|
|
**/
|
|
|
metaData: function (attr, value) {
|
|
|
if (attr != null && value != null) {
|
|
|
this._metaData[attr] = value;
|
|
|
return this;
|
|
|
} else if (attr != null) {
|
|
|
return this._metaData[attr];
|
|
|
} else {
|
|
|
return this._metaData;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Destroy the file.
|
|
|
* @return {Bmob.Promise} A promise that is fulfilled when the destroy
|
|
|
* completes.
|
|
|
*/
|
|
|
destroy: function (options) {
|
|
|
if (!this._url && !this._cdn) return Bmob.Promise.error('The file url and cdn is not eixsts.')._thenRunCallbacks(options);
|
|
|
|
|
|
var data = {
|
|
|
cdn: this._cdn,
|
|
|
_ContentType: "application/json",
|
|
|
url: this._url,
|
|
|
metaData: self._metaData,
|
|
|
};
|
|
|
var request = Bmob._request("2/files", null, null, 'DELETE', data);
|
|
|
return request._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Saves the file to the Bmob cloud.
|
|
|
* @param {Object} options A Backbone-style options object.
|
|
|
* @return {Bmob.Promise} Promise that is resolved when the save finishes.
|
|
|
*/
|
|
|
save: function (options) {
|
|
|
var self = this;
|
|
|
if (!self._previousSave) {
|
|
|
if (self._source) {
|
|
|
self._previousSave = self._source.then(function (base64, type) {
|
|
|
var data = {
|
|
|
base64: base64,
|
|
|
// base64: encodeBase64(base64),
|
|
|
_ContentType: "text/plain",
|
|
|
mime_type: "text/plain",
|
|
|
metaData: self._metaData,
|
|
|
category: "wechatApp",
|
|
|
};
|
|
|
if (!self._metaData.size) {
|
|
|
self._metaData.size = base64.length;
|
|
|
}
|
|
|
return Bmob._request("2/files", self._name, null, 'POST', data);
|
|
|
}).then(function (response) {
|
|
|
|
|
|
self._name = response.filename;
|
|
|
self._url = response.url;
|
|
|
self._cdn = response.cdn;
|
|
|
|
|
|
return self;
|
|
|
});
|
|
|
} else {
|
|
|
throw "not source file"
|
|
|
}
|
|
|
}
|
|
|
return self._previousSave._thenRunCallbacks(options);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
/**
|
|
|
* 包含push的函数
|
|
|
* @name Bmob.Push
|
|
|
* @namespace 推送消息
|
|
|
*/
|
|
|
Bmob.Files = Bmob.Files || {};
|
|
|
|
|
|
|
|
|
Bmob.Files.del = function (urls, options) {
|
|
|
var _url = urls.split(".com");
|
|
|
if (!_url) {
|
|
|
return Bmob.Promise.error('The file url and cdn is not eixsts.')._thenRunCallbacks(options);
|
|
|
}
|
|
|
|
|
|
var data = {
|
|
|
_ContentType: "application/json",
|
|
|
// url:_url,
|
|
|
};
|
|
|
var request = Bmob._request("2/files/upyun", _url[1], null, 'DELETE', data);
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates a new model with defined attributes. A client id (cid) is
|
|
|
* automatically generated and assigned for you.
|
|
|
*
|
|
|
* <p>You won't normally call this method directly. It is recommended that
|
|
|
* you use a subclass of <code>Bmob.Object</code> instead, created by calling
|
|
|
* <code>extend</code>.</p>
|
|
|
*
|
|
|
* <p>However, if you don't want to use a subclass, or aren't sure which
|
|
|
* subclass is appropriate, you can use this form:<pre>
|
|
|
* var object = new Bmob.Object("ClassName");
|
|
|
* </pre>
|
|
|
* That is basically equivalent to:<pre>
|
|
|
* var MyClass = Bmob.Object.extend("ClassName");
|
|
|
* var object = new MyClass();
|
|
|
* </pre></p>
|
|
|
*
|
|
|
* @param {Object} attributes The initial set of data to store in the object.
|
|
|
* @param {Object} options A set of Backbone-like options for creating the
|
|
|
* object. The only option currently supported is "collection".
|
|
|
* @see Bmob.Object.extend
|
|
|
*
|
|
|
*
|
|
|
* <p>The fundamental unit of Bmob data, which implements the Backbone Model
|
|
|
* interface.</p>
|
|
|
*/
|
|
|
Bmob.Object = function (attributes, options) {
|
|
|
// Allow new Bmob.Object("ClassName") as a shortcut to _create.
|
|
|
if (_.isString(attributes)) {
|
|
|
return Bmob.Object._create.apply(this, arguments);
|
|
|
}
|
|
|
|
|
|
attributes = attributes || {};
|
|
|
if (options && options.parse) {
|
|
|
attributes = this.parse(attributes);
|
|
|
}
|
|
|
var defaults = Bmob._getValue(this, 'defaults');
|
|
|
if (defaults) {
|
|
|
attributes = _.extend({},
|
|
|
defaults, attributes);
|
|
|
}
|
|
|
if (options && options.collection) {
|
|
|
this.collection = options.collection;
|
|
|
}
|
|
|
|
|
|
this._serverData = {}; // The last known data for this object from cloud.
|
|
|
this._opSetQueue = [{}]; // List of sets of changes to the data.
|
|
|
this.attributes = {}; // The best estimate of this's current data.
|
|
|
this._hashedJSON = {}; // Hash of values of containers at last save.
|
|
|
this._escapedAttributes = {};
|
|
|
this.cid = _.uniqueId('c');
|
|
|
this.changed = {};
|
|
|
this._silent = {};
|
|
|
this._pending = {};
|
|
|
if (!this.set(attributes, {
|
|
|
silent: true
|
|
|
})) {
|
|
|
throw new Error("Can't create an invalid Bmob.Object");
|
|
|
}
|
|
|
this.changed = {};
|
|
|
this._silent = {};
|
|
|
this._pending = {};
|
|
|
this._hasData = true;
|
|
|
this._previousAttributes = _.clone(this.attributes);
|
|
|
this.initialize.apply(this, arguments);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* @lends Bmob.Object.prototype
|
|
|
* @property {String} id The objectId of the Bmob Object.
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
* Saves the given list of Bmob.Object.
|
|
|
* If any error is encountered, stops and calls the error handler.
|
|
|
* There are two ways you can call this function.
|
|
|
*
|
|
|
* The Backbone way:<pre>
|
|
|
* Bmob.Object.saveAll([object1, object2, ...], {
|
|
|
* success: function(list) {
|
|
|
* // All the objects were saved.
|
|
|
* },
|
|
|
* error: function(error) {
|
|
|
* // An error occurred while saving one of the objects.
|
|
|
* },
|
|
|
* });
|
|
|
* </pre>
|
|
|
* A simplified syntax:<pre>
|
|
|
* Bmob.Object.saveAll([object1, object2, ...], function(list, error) {
|
|
|
* if (list) {
|
|
|
* // All the objects were saved.
|
|
|
* } else {
|
|
|
* // An error occurred.
|
|
|
* }
|
|
|
* });
|
|
|
* </pre>
|
|
|
*
|
|
|
* @param {Array} list A list of <code>Bmob.Object</code>.
|
|
|
* @param {Object} options A Backbone-style callback object.
|
|
|
*/
|
|
|
Bmob.Object.saveAll = function (list, options) {
|
|
|
return Bmob.Object._deepSaveAsync(list)._thenRunCallbacks(options);
|
|
|
};
|
|
|
|
|
|
// Attach all inheritable methods to the Bmob.Object prototype.
|
|
|
_.extend(Bmob.Object.prototype, Bmob.Events,
|
|
|
/** @lends Bmob.Object.prototype */
|
|
|
{
|
|
|
_existed: false,
|
|
|
_fetchWhenSave: false,
|
|
|
|
|
|
/**
|
|
|
* Initialize is an empty function by default. Override it with your own
|
|
|
* initialization logic.
|
|
|
*/
|
|
|
initialize: function () { },
|
|
|
|
|
|
/**
|
|
|
* Set whether to enable fetchWhenSave option when updating object.
|
|
|
* When set true, SDK would fetch the latest object after saving.
|
|
|
* Default is false.
|
|
|
* @param {boolean} enable true to enable fetchWhenSave option.
|
|
|
*/
|
|
|
fetchWhenSave: function (enable) {
|
|
|
if (typeof enable !== 'boolean') {
|
|
|
throw "Expect boolean value for fetchWhenSave";
|
|
|
}
|
|
|
this._fetchWhenSave = enable;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a JSON version of the object suitable for saving to Bmob.
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
var json = this._toFullJSON();
|
|
|
Bmob._arrayEach(["__type", "className"],
|
|
|
function (key) {
|
|
|
delete json[key];
|
|
|
});
|
|
|
return json;
|
|
|
},
|
|
|
|
|
|
_toFullJSON: function (seenObjects) {
|
|
|
var json = _.clone(this.attributes);
|
|
|
Bmob._objectEach(json,
|
|
|
function (val, key) {
|
|
|
json[key] = Bmob._encode(val, seenObjects);
|
|
|
});
|
|
|
Bmob._objectEach(this._operations,
|
|
|
function (val, key) {
|
|
|
json[key] = val;
|
|
|
});
|
|
|
|
|
|
if (_.has(this, "id")) {
|
|
|
json.objectId = this.id;
|
|
|
}
|
|
|
if (_.has(this, "createdAt")) {
|
|
|
if (_.isDate(this.createdAt)) {
|
|
|
json.createdAt = this.createdAt.toJSON();
|
|
|
} else {
|
|
|
json.createdAt = this.createdAt;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (_.has(this, "updatedAt")) {
|
|
|
if (_.isDate(this.updatedAt)) {
|
|
|
json.updatedAt = this.updatedAt.toJSON();
|
|
|
} else {
|
|
|
json.updatedAt = this.updatedAt;
|
|
|
}
|
|
|
}
|
|
|
json.__type = "Object";
|
|
|
json.className = this.className;
|
|
|
return json;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Updates _hashedJSON to reflect the current state of this object.
|
|
|
* Adds any changed hash values to the set of pending changes.
|
|
|
*/
|
|
|
_refreshCache: function () {
|
|
|
var self = this;
|
|
|
if (self._refreshingCache) {
|
|
|
return;
|
|
|
}
|
|
|
self._refreshingCache = true;
|
|
|
Bmob._objectEach(this.attributes,
|
|
|
function (value, key) {
|
|
|
if (value instanceof Bmob.Object) {
|
|
|
value._refreshCache();
|
|
|
} else if (_.isObject(value)) {
|
|
|
if (self._resetCacheForKey(key)) {
|
|
|
self.set(key, new Bmob.Op.Set(value), {
|
|
|
silent: true
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
delete self._refreshingCache;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns true if this object has been modified since its last
|
|
|
* save/refresh. If an attribute is specified, it returns true only if that
|
|
|
* particular attribute has been modified since the last save/refresh.
|
|
|
* @param {String} attr An attribute name (optional).
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
dirty: function (attr) {
|
|
|
this._refreshCache();
|
|
|
|
|
|
var currentChanges = _.last(this._opSetQueue);
|
|
|
|
|
|
if (attr) {
|
|
|
return (currentChanges[attr] ? true : false);
|
|
|
}
|
|
|
if (!this.id) {
|
|
|
return true;
|
|
|
}
|
|
|
if (_.keys(currentChanges).length > 0) {
|
|
|
return true;
|
|
|
}
|
|
|
return false;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets a Pointer referencing this Object.
|
|
|
*/
|
|
|
_toPointer: function () {
|
|
|
// if (!this.id) {
|
|
|
// throw new Error("Can't serialize an unsaved Bmob.Object");
|
|
|
// }
|
|
|
return {
|
|
|
__type: "Pointer",
|
|
|
className: this.className,
|
|
|
objectId: this.id
|
|
|
};
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets the value of an attribute.
|
|
|
* @param {String} attr The string name of an attribute.
|
|
|
*/
|
|
|
get: function (attr) {
|
|
|
return this.attributes[attr];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets a relation on the given class for the attribute.
|
|
|
* @param String attr The attribute to get the relation for.
|
|
|
*/
|
|
|
relation: function (attr) {
|
|
|
var value = this.get(attr);
|
|
|
if (value) {
|
|
|
if (!(value instanceof Bmob.Relation)) {
|
|
|
throw "Called relation() on non-relation field " + attr;
|
|
|
}
|
|
|
value._ensureParentAndKey(this, attr);
|
|
|
return value;
|
|
|
} else {
|
|
|
return new Bmob.Relation(this, attr);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets the HTML-escaped value of an attribute.
|
|
|
*/
|
|
|
escape: function (attr) {
|
|
|
var html = this._escapedAttributes[attr];
|
|
|
if (html) {
|
|
|
return html;
|
|
|
}
|
|
|
var val = this.attributes[attr];
|
|
|
var escaped;
|
|
|
if (Bmob._isNullOrUndefined(val)) {
|
|
|
escaped = '';
|
|
|
} else {
|
|
|
escaped = _.escape(val.toString());
|
|
|
}
|
|
|
this._escapedAttributes[attr] = escaped;
|
|
|
return escaped;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns <code>true</code> if the attribute contains a value that is not
|
|
|
* null or undefined.
|
|
|
* @param {String} attr The string name of the attribute.
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
has: function (attr) {
|
|
|
return !Bmob._isNullOrUndefined(this.attributes[attr]);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Pulls "special" fields like objectId, createdAt, etc. out of attrs
|
|
|
* and puts them on "this" directly. Removes them from attrs.
|
|
|
* @param attrs - A dictionary with the data for this Bmob.Object.
|
|
|
*/
|
|
|
_mergeMagicFields: function (attrs) {
|
|
|
// Check for changes of magic fields.
|
|
|
var model = this;
|
|
|
var specialFields = ["id", "objectId", "createdAt", "updatedAt"];
|
|
|
Bmob._arrayEach(specialFields,
|
|
|
function (attr) {
|
|
|
if (attrs[attr]) {
|
|
|
if (attr === "objectId") {
|
|
|
model.id = attrs[attr];
|
|
|
} else {
|
|
|
model[attr] = attrs[attr];
|
|
|
}
|
|
|
delete attrs[attr];
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns the json to be sent to the server.
|
|
|
*/
|
|
|
_startSave: function () {
|
|
|
this._opSetQueue.push({});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Called when a save fails because of an error. Any changes that were part
|
|
|
* of the save need to be merged with changes made after the save. This
|
|
|
* might throw an exception is you do conflicting operations. For example,
|
|
|
* if you do:
|
|
|
* object.set("foo", "bar");
|
|
|
* object.set("invalid field name", "baz");
|
|
|
* object.save();
|
|
|
* object.increment("foo");
|
|
|
* then this will throw when the save fails and the client tries to merge
|
|
|
* "bar" with the +1.
|
|
|
*/
|
|
|
_cancelSave: function () {
|
|
|
var self = this;
|
|
|
var failedChanges = _.first(this._opSetQueue);
|
|
|
this._opSetQueue = _.rest(this._opSetQueue);
|
|
|
var nextChanges = _.first(this._opSetQueue);
|
|
|
Bmob._objectEach(failedChanges,
|
|
|
function (op, key) {
|
|
|
var op1 = failedChanges[key];
|
|
|
var op2 = nextChanges[key];
|
|
|
if (op1 && op2) {
|
|
|
nextChanges[key] = op2._mergeWithPrevious(op1);
|
|
|
} else if (op1) {
|
|
|
nextChanges[key] = op1;
|
|
|
}
|
|
|
});
|
|
|
this._saving = this._saving - 1;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Called when a save completes successfully. This merges the changes that
|
|
|
* were saved into the known server data, and overrides it with any data
|
|
|
* sent directly from the server.
|
|
|
*/
|
|
|
_finishSave: function (serverData) {
|
|
|
// Grab a copy of any object referenced by this object. These instances
|
|
|
// may have already been fetched, and we don't want to lose their data.
|
|
|
// Note that doing it like this means we will unify separate copies of the
|
|
|
// same object, but that's a risk we have to take.
|
|
|
var fetchedObjects = {};
|
|
|
Bmob._traverse(this.attributes,
|
|
|
function (object) {
|
|
|
if (object instanceof Bmob.Object && object.id && object._hasData) {
|
|
|
fetchedObjects[object.id] = object;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
var savedChanges = _.first(this._opSetQueue);
|
|
|
this._opSetQueue = _.rest(this._opSetQueue);
|
|
|
this._applyOpSet(savedChanges, this._serverData);
|
|
|
this._mergeMagicFields(serverData);
|
|
|
var self = this;
|
|
|
Bmob._objectEach(serverData,
|
|
|
function (value, key) {
|
|
|
self._serverData[key] = Bmob._decode(key, value);
|
|
|
|
|
|
// Look for any objects that might have become unfetched and fix them
|
|
|
// by replacing their values with the previously observed values.
|
|
|
var fetched = Bmob._traverse(self._serverData[key],
|
|
|
function (object) {
|
|
|
if (object instanceof Bmob.Object && fetchedObjects[object.id]) {
|
|
|
return fetchedObjects[object.id];
|
|
|
}
|
|
|
});
|
|
|
if (fetched) {
|
|
|
self._serverData[key] = fetched;
|
|
|
}
|
|
|
});
|
|
|
this._rebuildAllEstimatedData();
|
|
|
this._saving = this._saving - 1;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Called when a fetch or login is complete to set the known server data to
|
|
|
* the given object.
|
|
|
*/
|
|
|
_finishFetch: function (serverData, hasData) {
|
|
|
// Clear out any changes the user might have made previously.
|
|
|
this._opSetQueue = [{}];
|
|
|
|
|
|
// Bring in all the new server data.
|
|
|
this._mergeMagicFields(serverData);
|
|
|
var self = this;
|
|
|
Bmob._objectEach(serverData,
|
|
|
function (value, key) {
|
|
|
self._serverData[key] = Bmob._decode(key, value);
|
|
|
});
|
|
|
|
|
|
// Refresh the attributes.
|
|
|
this._rebuildAllEstimatedData();
|
|
|
|
|
|
// Clear out the cache of mutable containers.
|
|
|
this._refreshCache();
|
|
|
this._opSetQueue = [{}];
|
|
|
|
|
|
this._hasData = hasData;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Applies the set of Bmob.Op in opSet to the object target.
|
|
|
*/
|
|
|
_applyOpSet: function (opSet, target) {
|
|
|
var self = this;
|
|
|
Bmob._objectEach(opSet,
|
|
|
function (change, key) {
|
|
|
target[key] = change._estimate(target[key], self, key);
|
|
|
if (target[key] === Bmob.Op._UNSET) {
|
|
|
delete target[key];
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Replaces the cached value for key with the current value.
|
|
|
* Returns true if the new value is different than the old value.
|
|
|
*/
|
|
|
_resetCacheForKey: function (key) {
|
|
|
var value = this.attributes[key];
|
|
|
if (_.isObject(value) && !(value instanceof Bmob.Object) && !(value instanceof Bmob.File)) {
|
|
|
value = value.toJSON ? value.toJSON() : value;
|
|
|
var json = JSON.stringify(value);
|
|
|
if (this._hashedJSON[key] !== json) {
|
|
|
this._hashedJSON[key] = json;
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Populates attributes[key] by starting with the last known data from the
|
|
|
* server, and applying all of the local changes that have been made to that
|
|
|
* key since then.
|
|
|
*/
|
|
|
_rebuildEstimatedDataForKey: function (key) {
|
|
|
var self = this;
|
|
|
delete this.attributes[key];
|
|
|
if (this._serverData[key]) {
|
|
|
this.attributes[key] = this._serverData[key];
|
|
|
}
|
|
|
Bmob._arrayEach(this._opSetQueue,
|
|
|
function (opSet) {
|
|
|
var op = opSet[key];
|
|
|
if (op) {
|
|
|
self.attributes[key] = op._estimate(self.attributes[key], self, key);
|
|
|
if (self.attributes[key] === Bmob.Op._UNSET) {
|
|
|
delete self.attributes[key];
|
|
|
} else {
|
|
|
self._resetCacheForKey(key);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Populates attributes by starting with the last known data from the
|
|
|
* server, and applying all of the local changes that have been made since
|
|
|
* then.
|
|
|
*/
|
|
|
_rebuildAllEstimatedData: function () {
|
|
|
var self = this;
|
|
|
|
|
|
var previousAttributes = _.clone(this.attributes);
|
|
|
|
|
|
this.attributes = _.clone(this._serverData);
|
|
|
Bmob._arrayEach(this._opSetQueue,
|
|
|
function (opSet) {
|
|
|
self._applyOpSet(opSet, self.attributes);
|
|
|
Bmob._objectEach(opSet,
|
|
|
function (op, key) {
|
|
|
self._resetCacheForKey(key);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// Trigger change events for anything that changed because of the fetch.
|
|
|
Bmob._objectEach(previousAttributes,
|
|
|
function (oldValue, key) {
|
|
|
if (self.attributes[key] !== oldValue) {
|
|
|
self.trigger('change:' + key, self, self.attributes[key], {});
|
|
|
}
|
|
|
});
|
|
|
Bmob._objectEach(this.attributes,
|
|
|
function (newValue, key) {
|
|
|
if (!_.has(previousAttributes, key)) {
|
|
|
self.trigger('change:' + key, self, newValue, {});
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Sets a hash of model attributes on the object, firing
|
|
|
* <code>"change"</code> unless you choose to silence it.
|
|
|
*
|
|
|
* <p>You can call it with an object containing keys and values, or with one
|
|
|
* key and value. For example:<pre>
|
|
|
* gameTurn.set({
|
|
|
* player: player1,
|
|
|
* diceRoll: 2
|
|
|
* }, {
|
|
|
* error: function(gameTurnAgain, error) {
|
|
|
* // The set failed validation.
|
|
|
* }
|
|
|
* });
|
|
|
*
|
|
|
* game.set("currentPlayer", player2, {
|
|
|
* error: function(gameTurnAgain, error) {
|
|
|
* // The set failed validation.
|
|
|
* }
|
|
|
* });
|
|
|
*
|
|
|
* game.set("finished", true);</pre></p>
|
|
|
*
|
|
|
* @param {String} key The key to set.
|
|
|
* @param {} value The value to give it.
|
|
|
* @param {Object} options A set of Backbone-like options for the set.
|
|
|
* The only supported options are <code>silent</code>,
|
|
|
* <code>error</code>, and <code>promise</code>.
|
|
|
* @return {Boolean} true if the set succeeded.
|
|
|
* @see Bmob.Object#validate
|
|
|
* @see Bmob.Error
|
|
|
*/
|
|
|
set: function (key, value, options) {
|
|
|
var attrs, attr;
|
|
|
if (_.isObject(key) || Bmob._isNullOrUndefined(key)) {
|
|
|
attrs = key;
|
|
|
Bmob._objectEach(attrs,
|
|
|
function (v, k) {
|
|
|
attrs[k] = Bmob._decode(k, v);
|
|
|
});
|
|
|
options = value;
|
|
|
} else {
|
|
|
attrs = {};
|
|
|
attrs[key] = Bmob._decode(key, value);
|
|
|
}
|
|
|
|
|
|
// Extract attributes and options.
|
|
|
options = options || {};
|
|
|
if (!attrs) {
|
|
|
return this;
|
|
|
}
|
|
|
if (attrs instanceof Bmob.Object) {
|
|
|
attrs = attrs.attributes;
|
|
|
}
|
|
|
|
|
|
// If the unset option is used, every attribute should be a Unset.
|
|
|
if (options.unset) {
|
|
|
Bmob._objectEach(attrs,
|
|
|
function (unused_value, key) {
|
|
|
attrs[key] = new Bmob.Op.Unset();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Apply all the attributes to get the estimated values.
|
|
|
var dataToValidate = _.clone(attrs);
|
|
|
var self = this;
|
|
|
Bmob._objectEach(dataToValidate,
|
|
|
function (value, key) {
|
|
|
if (value instanceof Bmob.Op) {
|
|
|
dataToValidate[key] = value._estimate(self.attributes[key], self, key);
|
|
|
if (dataToValidate[key] === Bmob.Op._UNSET) {
|
|
|
delete dataToValidate[key];
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Run validation.
|
|
|
if (!this._validate(attrs, options)) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
this._mergeMagicFields(attrs);
|
|
|
|
|
|
options.changes = {};
|
|
|
var escaped = this._escapedAttributes;
|
|
|
var prev = this._previousAttributes || {};
|
|
|
|
|
|
// Update attributes.
|
|
|
Bmob._arrayEach(_.keys(attrs),
|
|
|
function (attr) {
|
|
|
var val = attrs[attr];
|
|
|
|
|
|
// If this is a relation object we need to set the parent correctly,
|
|
|
// since the location where it was parsed does not have access to
|
|
|
// this object.
|
|
|
if (val instanceof Bmob.Relation) {
|
|
|
val.parent = self;
|
|
|
}
|
|
|
|
|
|
if (!(val instanceof Bmob.Op)) {
|
|
|
val = new Bmob.Op.Set(val);
|
|
|
}
|
|
|
|
|
|
// See if this change will actually have any effect.
|
|
|
var isRealChange = true;
|
|
|
if (val instanceof Bmob.Op.Set && _.isEqual(self.attributes[attr], val.value)) {
|
|
|
isRealChange = false;
|
|
|
}
|
|
|
|
|
|
if (isRealChange) {
|
|
|
delete escaped[attr];
|
|
|
if (options.silent) {
|
|
|
self._silent[attr] = true;
|
|
|
} else {
|
|
|
options.changes[attr] = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var currentChanges = _.last(self._opSetQueue);
|
|
|
currentChanges[attr] = val._mergeWithPrevious(currentChanges[attr]);
|
|
|
self._rebuildEstimatedDataForKey(attr);
|
|
|
|
|
|
if (isRealChange) {
|
|
|
self.changed[attr] = self.attributes[attr];
|
|
|
if (!options.silent) {
|
|
|
self._pending[attr] = true;
|
|
|
}
|
|
|
} else {
|
|
|
delete self.changed[attr];
|
|
|
delete self._pending[attr];
|
|
|
}
|
|
|
});
|
|
|
|
|
|
if (!options.silent) {
|
|
|
this.change(options);
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Remove an attribute from the model, firing <code>"change"</code> unless
|
|
|
* you choose to silence it. This is a noop if the attribute doesn't
|
|
|
* exist.
|
|
|
*/
|
|
|
unset: function (attr, options) {
|
|
|
options = options || {};
|
|
|
options.unset = true;
|
|
|
return this.set(attr, null, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Atomically increments the value of the given attribute the next time the
|
|
|
* object is saved. If no amount is specified, 1 is used by default.
|
|
|
*
|
|
|
* @param attr {String} The key.
|
|
|
* @param amount {Number} The amount to increment by.
|
|
|
*/
|
|
|
increment: function (attr, amount) {
|
|
|
if (_.isUndefined(amount) || _.isNull(amount)) {
|
|
|
amount = 1;
|
|
|
}
|
|
|
return this.set(attr, new Bmob.Op.Increment(amount));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Atomically add an object to the end of the array associated with a given
|
|
|
* key.
|
|
|
* @param attr {String} The key.
|
|
|
* @param item {} The item to add.
|
|
|
*/
|
|
|
add: function (attr, item) {
|
|
|
return this.set(attr, new Bmob.Op.Add([item]));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Atomically add an object to the array associated with a given key, only
|
|
|
* if it is not already present in the array. The position of the insert is
|
|
|
* not guaranteed.
|
|
|
*
|
|
|
* @param attr {String} The key.
|
|
|
* @param item {} The object to add.
|
|
|
*/
|
|
|
addUnique: function (attr, item) {
|
|
|
return this.set(attr, new Bmob.Op.AddUnique([item]));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Atomically remove all instances of an object from the array associated
|
|
|
* with a given key.
|
|
|
*
|
|
|
* @param attr {String} The key.
|
|
|
* @param item {} The object to remove.
|
|
|
*/
|
|
|
remove: function (attr, item) {
|
|
|
return this.set(attr, new Bmob.Op.Remove([item]));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns an instance of a subclass of Bmob.Op describing what kind of
|
|
|
* modification has been performed on this field since the last time it was
|
|
|
* saved. For example, after calling object.increment("x"), calling
|
|
|
* object.op("x") would return an instance of Bmob.Op.Increment.
|
|
|
*
|
|
|
* @param attr {String} The key.
|
|
|
* @returns {Bmob.Op} The operation, or undefined if none.
|
|
|
*/
|
|
|
op: function (attr) {
|
|
|
return _.last(this._opSetQueue)[attr];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Clear all attributes on the model, firing <code>"change"</code> unless
|
|
|
* you choose to silence it.
|
|
|
*/
|
|
|
clear: function (options) {
|
|
|
options = options || {};
|
|
|
options.unset = true;
|
|
|
var keysToClear = _.extend(this.attributes, this._operations);
|
|
|
return this.set(keysToClear, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns a JSON-encoded set of operations to be sent with the next save
|
|
|
* request.
|
|
|
*/
|
|
|
_getSaveJSON: function () {
|
|
|
var json = _.clone(_.first(this._opSetQueue));
|
|
|
Bmob._objectEach(json,
|
|
|
function (op, key) {
|
|
|
json[key] = op.toJSON();
|
|
|
});
|
|
|
return json;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns true if this object can be serialized for saving.
|
|
|
*/
|
|
|
_canBeSerialized: function () {
|
|
|
return Bmob.Object._canBeSerializedAsValue(this.attributes);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Fetch the model from the server. If the server's representation of the
|
|
|
* model differs from its current attributes, they will be overriden,
|
|
|
* triggering a <code>"change"</code> event.
|
|
|
* @return {Bmob.Promise} A promise that is fulfilled when the fetch
|
|
|
* completes.
|
|
|
*/
|
|
|
fetch: function (options) {
|
|
|
var self = this;
|
|
|
var request = Bmob._request("classes", this.className, this.id, 'GET');
|
|
|
return request.then(function (response, status, xhr) {
|
|
|
self._finishFetch(self.parse(response, status, xhr), true);
|
|
|
return self;
|
|
|
})._thenRunCallbacks(options, this);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Set a hash of model attributes, and save the model to the server.
|
|
|
* updatedAt will be updated when the request returns.
|
|
|
* You can either call it as:<pre>
|
|
|
* object.save();</pre>
|
|
|
* or<pre>
|
|
|
* object.save(null, options);</pre>
|
|
|
* or<pre>
|
|
|
* object.save(attrs, options);</pre>
|
|
|
* or<pre>
|
|
|
* object.save(key, value, options);</pre>
|
|
|
*
|
|
|
* For example, <pre>
|
|
|
* gameTurn.save({
|
|
|
* player: "Jake Cutter",
|
|
|
* diceRoll: 2
|
|
|
* }, {
|
|
|
* success: function(gameTurnAgain) {
|
|
|
* // The save was successful.
|
|
|
* },
|
|
|
* error: function(gameTurnAgain, error) {
|
|
|
* // The save failed. Error is an instance of Bmob.Error.
|
|
|
* }
|
|
|
* });</pre>
|
|
|
* or with promises:<pre>
|
|
|
* gameTurn.save({
|
|
|
* player: "Jake Cutter",
|
|
|
* diceRoll: 2
|
|
|
* }).then(function(gameTurnAgain) {
|
|
|
* // The save was successful.
|
|
|
* }, function(error) {
|
|
|
* // The save failed. Error is an instance of Bmob.Error.
|
|
|
* });</pre>
|
|
|
*
|
|
|
* @return {Bmob.Promise} A promise that is fulfilled when the save
|
|
|
* completes.
|
|
|
* @see Bmob.Error
|
|
|
*/
|
|
|
save: function (arg1, arg2, arg3) {
|
|
|
var i, attrs, current, options, saved;
|
|
|
if (_.isObject(arg1) || Bmob._isNullOrUndefined(arg1)) {
|
|
|
attrs = arg1;
|
|
|
options = arg2;
|
|
|
} else {
|
|
|
attrs = {};
|
|
|
attrs[arg1] = arg2;
|
|
|
options = arg3;
|
|
|
}
|
|
|
|
|
|
// Make save({ success: function() {} }) work.
|
|
|
if (!options && attrs) {
|
|
|
var extra_keys = _.reject(attrs,
|
|
|
function (value, key) {
|
|
|
return _.include(["success", "error", "wait"], key);
|
|
|
});
|
|
|
if (extra_keys.length === 0) {
|
|
|
var all_functions = true;
|
|
|
if (_.has(attrs, "success") && !_.isFunction(attrs.success)) {
|
|
|
all_functions = false;
|
|
|
}
|
|
|
if (_.has(attrs, "error") && !_.isFunction(attrs.error)) {
|
|
|
all_functions = false;
|
|
|
}
|
|
|
if (all_functions) {
|
|
|
// This attrs object looks like it's really an options object,
|
|
|
// and there's no other options object, so let's just use it.
|
|
|
return this.save(null, attrs);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
options = _.clone(options) || {};
|
|
|
if (options.wait) {
|
|
|
current = _.clone(this.attributes);
|
|
|
}
|
|
|
|
|
|
var setOptions = _.clone(options) || {};
|
|
|
if (setOptions.wait) {
|
|
|
setOptions.silent = true;
|
|
|
}
|
|
|
var setError;
|
|
|
setOptions.error = function (model, error) {
|
|
|
setError = error;
|
|
|
};
|
|
|
if (attrs && !this.set(attrs, setOptions)) {
|
|
|
return Bmob.Promise.error(setError)._thenRunCallbacks(options, this);
|
|
|
}
|
|
|
|
|
|
var model = this;
|
|
|
|
|
|
// If there is any unsaved child, save it first.
|
|
|
model._refreshCache();
|
|
|
|
|
|
var unsavedChildren = [];
|
|
|
var unsavedFiles = [];
|
|
|
Bmob.Object._findUnsavedChildren(model.attributes, unsavedChildren, unsavedFiles);
|
|
|
if (unsavedChildren.length + unsavedFiles.length > 0) {
|
|
|
return Bmob.Object._deepSaveAsync(this.attributes).then(function () {
|
|
|
return model.save(null, options);
|
|
|
},
|
|
|
function (error) {
|
|
|
return Bmob.Promise.error(error)._thenRunCallbacks(options, model);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
this._startSave();
|
|
|
this._saving = (this._saving || 0) + 1;
|
|
|
|
|
|
this._allPreviousSaves = this._allPreviousSaves || Bmob.Promise.as();
|
|
|
this._allPreviousSaves = this._allPreviousSaves._continueWith(function () {
|
|
|
var method = model.id ? 'PUT' : 'POST';
|
|
|
|
|
|
var json = model._getSaveJSON();
|
|
|
|
|
|
if (method === 'PUT' && model._fetchWhenSave) {
|
|
|
//Sepcial-case fetchWhenSave when updating object.
|
|
|
json._fetchWhenSave = true;
|
|
|
}
|
|
|
|
|
|
var route = "classes";
|
|
|
var className = model.className;
|
|
|
if (model.className === "_User" && !model.id) {
|
|
|
// Special-case user sign-up.
|
|
|
route = "users";
|
|
|
className = null;
|
|
|
}
|
|
|
var request = Bmob._request(route, className, model.id, method, json);
|
|
|
|
|
|
request = request.then(function (resp, status, xhr) {
|
|
|
var serverAttrs = model.parse(resp, status, xhr);
|
|
|
if (options.wait) {
|
|
|
serverAttrs = _.extend(attrs || {},
|
|
|
serverAttrs);
|
|
|
}
|
|
|
model._finishSave(serverAttrs);
|
|
|
if (options.wait) {
|
|
|
model.set(current, setOptions);
|
|
|
}
|
|
|
return model;
|
|
|
|
|
|
},
|
|
|
function (error) {
|
|
|
model._cancelSave();
|
|
|
return Bmob.Promise.error(error);
|
|
|
|
|
|
})._thenRunCallbacks(options, model);
|
|
|
|
|
|
return request;
|
|
|
});
|
|
|
return this._allPreviousSaves;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Destroy this model on the server if it was already persisted.
|
|
|
* Optimistically removes the model from its collection, if it has one.
|
|
|
* If `wait: true` is passed, waits for the server to respond
|
|
|
* before removal.
|
|
|
*
|
|
|
* @return {Bmob.Promise} A promise that is fulfilled when the destroy
|
|
|
* completes.
|
|
|
*/
|
|
|
destroy: function (options) {
|
|
|
options = options || {};
|
|
|
var model = this;
|
|
|
|
|
|
var triggerDestroy = function () {
|
|
|
model.trigger('destroy', model, model.collection, options);
|
|
|
};
|
|
|
|
|
|
if (!this.id) {
|
|
|
return triggerDestroy();
|
|
|
}
|
|
|
|
|
|
if (!options.wait) {
|
|
|
triggerDestroy();
|
|
|
}
|
|
|
|
|
|
var request = Bmob._request("classes", this.className, this.id, 'DELETE');
|
|
|
return request.then(function () {
|
|
|
if (options.wait) {
|
|
|
triggerDestroy();
|
|
|
}
|
|
|
return model;
|
|
|
})._thenRunCallbacks(options, this);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Converts a response into the hash of attributes to be set on the model.
|
|
|
* @ignore
|
|
|
*/
|
|
|
parse: function (resp, status, xhr) {
|
|
|
var output = _.clone(resp);
|
|
|
_(["createdAt", "updatedAt"]).each(function (key) {
|
|
|
if (output[key]) {
|
|
|
output[key] = output[key];
|
|
|
}
|
|
|
});
|
|
|
if (!output.updatedAt) {
|
|
|
output.updatedAt = output.createdAt;
|
|
|
}
|
|
|
if (status) {
|
|
|
this._existed = (status !== 201);
|
|
|
}
|
|
|
return output;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Creates a new model with identical attributes to this one.
|
|
|
* @return {Bmob.Object}
|
|
|
*/
|
|
|
clone: function () {
|
|
|
return new this.constructor(this.attributes);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns true if this object has never been saved to Bmob.
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
isNew: function () {
|
|
|
return !this.id;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Call this method to manually fire a `"change"` event for this model and
|
|
|
* a `"change:attribute"` event for each changed attribute.
|
|
|
* Calling this will cause all objects observing the model to update.
|
|
|
*/
|
|
|
change: function (options) {
|
|
|
options = options || {};
|
|
|
var changing = this._changing;
|
|
|
this._changing = true;
|
|
|
|
|
|
// Silent changes become pending changes.
|
|
|
var self = this;
|
|
|
Bmob._objectEach(this._silent,
|
|
|
function (attr) {
|
|
|
self._pending[attr] = true;
|
|
|
});
|
|
|
|
|
|
// Silent changes are triggered.
|
|
|
var changes = _.extend({},
|
|
|
options.changes, this._silent);
|
|
|
this._silent = {};
|
|
|
Bmob._objectEach(changes,
|
|
|
function (unused_value, attr) {
|
|
|
self.trigger('change:' + attr, self, self.get(attr), options);
|
|
|
});
|
|
|
if (changing) {
|
|
|
return this;
|
|
|
}
|
|
|
|
|
|
// This is to get around lint not letting us make a function in a loop.
|
|
|
var deleteChanged = function (value, attr) {
|
|
|
if (!self._pending[attr] && !self._silent[attr]) {
|
|
|
delete self.changed[attr];
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// Continue firing `"change"` events while there are pending changes.
|
|
|
while (!_.isEmpty(this._pending)) {
|
|
|
this._pending = {};
|
|
|
this.trigger('change', this, options);
|
|
|
// Pending and silent changes still remain.
|
|
|
Bmob._objectEach(this.changed, deleteChanged);
|
|
|
self._previousAttributes = _.clone(this.attributes);
|
|
|
}
|
|
|
|
|
|
this._changing = false;
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns true if this object was created by the Bmob server when the
|
|
|
* object might have already been there (e.g. in the case of a Facebook
|
|
|
* login)
|
|
|
*/
|
|
|
existed: function () {
|
|
|
return this._existed;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Determine if the model has changed since the last <code>"change"</code>
|
|
|
* event. If you specify an attribute name, determine if that attribute
|
|
|
* has changed.
|
|
|
* @param {String} attr Optional attribute name
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
hasChanged: function (attr) {
|
|
|
if (!arguments.length) {
|
|
|
return !_.isEmpty(this.changed);
|
|
|
}
|
|
|
return this.changed && _.has(this.changed, attr);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns an object containing all the attributes that have changed, or
|
|
|
* false if there are no changed attributes. Useful for determining what
|
|
|
* parts of a view need to be updated and/or what attributes need to be
|
|
|
* persisted to the server. Unset attributes will be set to undefined.
|
|
|
* You can also pass an attributes object to diff against the model,
|
|
|
* determining if there *would be* a change.
|
|
|
*/
|
|
|
changedAttributes: function (diff) {
|
|
|
if (!diff) {
|
|
|
return this.hasChanged() ? _.clone(this.changed) : false;
|
|
|
}
|
|
|
var changed = {};
|
|
|
var old = this._previousAttributes;
|
|
|
Bmob._objectEach(diff,
|
|
|
function (diffVal, attr) {
|
|
|
if (!_.isEqual(old[attr], diffVal)) {
|
|
|
changed[attr] = diffVal;
|
|
|
}
|
|
|
});
|
|
|
return changed;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets the previous value of an attribute, recorded at the time the last
|
|
|
* <code>"change"</code> event was fired.
|
|
|
* @param {String} attr Name of the attribute to get.
|
|
|
*/
|
|
|
previous: function (attr) {
|
|
|
if (!arguments.length || !this._previousAttributes) {
|
|
|
return null;
|
|
|
}
|
|
|
return this._previousAttributes[attr];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Gets all of the attributes of the model at the time of the previous
|
|
|
* <code>"change"</code> event.
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
previousAttributes: function () {
|
|
|
return _.clone(this._previousAttributes);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Checks if the model is currently in a valid state. It's only possible to
|
|
|
* get into an *invalid* state if you're using silent changes.
|
|
|
* @return {Boolean}
|
|
|
*/
|
|
|
isValid: function () {
|
|
|
return !this.validate(this.attributes);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* You should not call this function directly unless you subclass
|
|
|
* <code>Bmob.Object</code>, in which case you can override this method
|
|
|
* to provide additional validation on <code>set</code> and
|
|
|
* <code>save</code>. Your implementation should return
|
|
|
*
|
|
|
* @param {Object} attrs The current data to validate.
|
|
|
* @param {Object} options A Backbone-like options object.
|
|
|
* @return {} False if the data is valid. An error object otherwise.
|
|
|
* @see Bmob.Object#set
|
|
|
*/
|
|
|
validate: function (attrs, options) {
|
|
|
if (_.has(attrs, "ACL") && !(attrs.ACL instanceof Bmob.ACL)) {
|
|
|
return new Bmob.Error(Bmob.Error.OTHER_CAUSE, "ACL must be a Bmob.ACL.");
|
|
|
}
|
|
|
return false;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Run validation against a set of incoming attributes, returning `true`
|
|
|
* if all is well. If a specific `error` callback has been passed,
|
|
|
* call that instead of firing the general `"error"` event.
|
|
|
*/
|
|
|
_validate: function (attrs, options) {
|
|
|
if (options.silent || !this.validate) {
|
|
|
return true;
|
|
|
}
|
|
|
attrs = _.extend({},
|
|
|
this.attributes, attrs);
|
|
|
var error = this.validate(attrs, options);
|
|
|
if (!error) {
|
|
|
return true;
|
|
|
}
|
|
|
if (options && options.error) {
|
|
|
options.error(this, error, options);
|
|
|
} else {
|
|
|
this.trigger('error', this, error, options);
|
|
|
}
|
|
|
return false;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Returns the ACL for this object.
|
|
|
* @returns {Bmob.ACL} An instance of Bmob.ACL.
|
|
|
* @see Bmob.Object#get
|
|
|
*/
|
|
|
getACL: function () {
|
|
|
return this.get("ACL");
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Sets the ACL to be used for this object.
|
|
|
* @param {Bmob.ACL} acl An instance of Bmob.ACL.
|
|
|
* @param {Object} options Optional Backbone-like options object to be
|
|
|
* passed in to set.
|
|
|
* @return {Boolean} Whether the set passed validation.
|
|
|
* @see Bmob.Object#set
|
|
|
*/
|
|
|
setACL: function (acl, options) {
|
|
|
return this.set("ACL", acl, options);
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* Creates an instance of a subclass of Bmob.Object for the give classname
|
|
|
* and id.
|
|
|
* @param {String} className The name of the Bmob class backing this model.
|
|
|
* @param {String} id The object id of this model.
|
|
|
* @return {Bmob.Object} A new subclass instance of Bmob.Object.
|
|
|
*/
|
|
|
Bmob.Object.createWithoutData = function (className, id, hasData) {
|
|
|
var result = new Bmob.Object(className);
|
|
|
result.id = id;
|
|
|
result._hasData = hasData;
|
|
|
return result;
|
|
|
};
|
|
|
/**
|
|
|
* Delete objects in batch.The objects className must be the same.
|
|
|
* @param {Array} The ParseObject array to be deleted.
|
|
|
* @param {Object} options Standard options object with success and error
|
|
|
* callbacks.
|
|
|
* @return {Bmob.Promise} A promise that is fulfilled when the save
|
|
|
* completes.
|
|
|
*/
|
|
|
Bmob.Object.destroyAll = function (objects, options) {
|
|
|
if (objects == null || objects.length == 0) {
|
|
|
return Bmob.Promise.as()._thenRunCallbacks(options);
|
|
|
}
|
|
|
var className = objects[0].className;
|
|
|
var id = "";
|
|
|
var wasFirst = true;
|
|
|
objects.forEach(function (obj) {
|
|
|
if (obj.className != className) throw "Bmob.Object.destroyAll requires the argument object array's classNames must be the same";
|
|
|
if (!obj.id) throw "Could not delete unsaved object";
|
|
|
if (wasFirst) {
|
|
|
id = obj.id;
|
|
|
wasFirst = false;
|
|
|
} else {
|
|
|
id = id + ',' + obj.id;
|
|
|
}
|
|
|
});
|
|
|
var request = Bmob._request("classes", className, id, 'DELETE');
|
|
|
return request._thenRunCallbacks(options);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Returns the appropriate subclass for making new instances of the given
|
|
|
* className string.
|
|
|
*/
|
|
|
Bmob.Object._getSubclass = function (className) {
|
|
|
if (!_.isString(className)) {
|
|
|
throw "Bmob.Object._getSubclass requires a string argument.";
|
|
|
}
|
|
|
var ObjectClass = Bmob.Object._classMap[className];
|
|
|
if (!ObjectClass) {
|
|
|
ObjectClass = Bmob.Object.extend(className);
|
|
|
Bmob.Object._classMap[className] = ObjectClass;
|
|
|
}
|
|
|
return ObjectClass;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates an instance of a subclass of Bmob.Object for the given classname.
|
|
|
*/
|
|
|
Bmob.Object._create = function (className, attributes, options) {
|
|
|
var ObjectClass = Bmob.Object._getSubclass(className);
|
|
|
return new ObjectClass(attributes, options);
|
|
|
};
|
|
|
|
|
|
// Set up a map of className to class so that we can create new instances of
|
|
|
// Bmob Objects from JSON automatically.
|
|
|
Bmob.Object._classMap = {};
|
|
|
|
|
|
Bmob.Object._extend = Bmob._extend;
|
|
|
|
|
|
/**
|
|
|
* Creates a new subclass of Bmob.Object for the given Bmob class name.
|
|
|
*
|
|
|
* <p>Every extension of a Bmob class will inherit from the most recent
|
|
|
* previous extension of that class. When a Bmob.Object is automatically
|
|
|
* created by parsing JSON, it will use the most recent extension of that
|
|
|
* class.</p>
|
|
|
*
|
|
|
* <p>You should call either:<pre>
|
|
|
* var MyClass = Bmob.Object.extend("MyClass", {
|
|
|
* <i>Instance properties</i>
|
|
|
* }, {
|
|
|
* <i>Class properties</i>
|
|
|
* });</pre>
|
|
|
* or, for Backbone compatibility:<pre>
|
|
|
* var MyClass = Bmob.Object.extend({
|
|
|
* className: "MyClass",
|
|
|
* <i>Other instance properties</i>
|
|
|
* }, {
|
|
|
* <i>Class properties</i>
|
|
|
* });</pre></p>
|
|
|
*
|
|
|
* @param {String} className The name of the Bmob class backing this model.
|
|
|
* @param {Object} protoProps Instance properties to add to instances of the
|
|
|
* class returned from this method.
|
|
|
* @param {Object} classProps Class properties to add the class returned from
|
|
|
* this method.
|
|
|
* @return {Class} A new subclass of Bmob.Object.
|
|
|
*/
|
|
|
Bmob.Object.extend = function (className, protoProps, classProps) {
|
|
|
// Handle the case with only two args.
|
|
|
if (!_.isString(className)) {
|
|
|
if (className && _.has(className, "className")) {
|
|
|
return Bmob.Object.extend(className.className, className, protoProps);
|
|
|
} else {
|
|
|
throw new Error("Bmob.Object.extend's first argument should be the className.");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// If someone tries to subclass "User", coerce it to the right type.
|
|
|
if (className === "User") {
|
|
|
className = "_User";
|
|
|
}
|
|
|
|
|
|
var NewClassObject = null;
|
|
|
if (_.has(Bmob.Object._classMap, className)) {
|
|
|
var OldClassObject = Bmob.Object._classMap[className];
|
|
|
// This new subclass has been told to extend both from "this" and from
|
|
|
// OldClassObject. This is multiple inheritance, which isn't supported.
|
|
|
// For now, let's just pick one.
|
|
|
NewClassObject = OldClassObject._extend(protoProps, classProps);
|
|
|
} else {
|
|
|
protoProps = protoProps || {};
|
|
|
protoProps.className = className;
|
|
|
NewClassObject = this._extend(protoProps, classProps);
|
|
|
}
|
|
|
// Extending a subclass should reuse the classname automatically.
|
|
|
NewClassObject.extend = function (arg0) {
|
|
|
if (_.isString(arg0) || (arg0 && _.has(arg0, "className"))) {
|
|
|
return Bmob.Object.extend.apply(NewClassObject, arguments);
|
|
|
}
|
|
|
var newArguments = [className].concat(Bmob._.toArray(arguments));
|
|
|
return Bmob.Object.extend.apply(NewClassObject, newArguments);
|
|
|
};
|
|
|
Bmob.Object._classMap[className] = NewClassObject;
|
|
|
return NewClassObject;
|
|
|
};
|
|
|
|
|
|
Bmob.Object._findUnsavedChildren = function (object, children, files) {
|
|
|
Bmob._traverse(object,
|
|
|
function (object) {
|
|
|
if (object instanceof Bmob.Object) {
|
|
|
object._refreshCache();
|
|
|
if (object.dirty()) {
|
|
|
children.push(object);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (object instanceof Bmob.File) {
|
|
|
if (!object.url()) {
|
|
|
files.push(object);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
});
|
|
|
};
|
|
|
|
|
|
Bmob.Object._canBeSerializedAsValue = function (object) {
|
|
|
var canBeSerializedAsValue = true;
|
|
|
|
|
|
if (object instanceof Bmob.Object) {
|
|
|
canBeSerializedAsValue = !!object.id;
|
|
|
|
|
|
} else if (_.isArray(object)) {
|
|
|
Bmob._arrayEach(object,
|
|
|
function (child) {
|
|
|
if (!Bmob.Object._canBeSerializedAsValue(child)) {
|
|
|
canBeSerializedAsValue = false;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
} else if (_.isObject(object)) {
|
|
|
Bmob._objectEach(object,
|
|
|
function (child) {
|
|
|
if (!Bmob.Object._canBeSerializedAsValue(child)) {
|
|
|
canBeSerializedAsValue = false;
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
return canBeSerializedAsValue;
|
|
|
};
|
|
|
|
|
|
Bmob.Object._deepSaveAsync = function (object) {
|
|
|
var unsavedChildren = [];
|
|
|
var unsavedFiles = [];
|
|
|
Bmob.Object._findUnsavedChildren(object, unsavedChildren, unsavedFiles);
|
|
|
|
|
|
var promise = Bmob.Promise.as();
|
|
|
_.each(unsavedFiles,
|
|
|
function (file) {
|
|
|
promise = promise.then(function () {
|
|
|
return file.save();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
var objects = _.uniq(unsavedChildren);
|
|
|
var remaining = _.uniq(objects);
|
|
|
|
|
|
return promise.then(function () {
|
|
|
return Bmob.Promise._continueWhile(function () {
|
|
|
return remaining.length > 0;
|
|
|
},
|
|
|
function () {
|
|
|
|
|
|
// Gather up all the objects that can be saved in this batch.
|
|
|
var batch = [];
|
|
|
var newRemaining = [];
|
|
|
Bmob._arrayEach(remaining,
|
|
|
function (object) {
|
|
|
// Limit batches to 20 objects.
|
|
|
if (batch.length > 20) {
|
|
|
newRemaining.push(object);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (object._canBeSerialized()) {
|
|
|
batch.push(object);
|
|
|
} else {
|
|
|
newRemaining.push(object);
|
|
|
}
|
|
|
});
|
|
|
remaining = newRemaining;
|
|
|
|
|
|
// If we can't save any objects, there must be a circular reference.
|
|
|
if (batch.length === 0) {
|
|
|
return Bmob.Promise.error(new Bmob.Error(Bmob.Error.OTHER_CAUSE, "Tried to save a batch with a cycle."));
|
|
|
}
|
|
|
|
|
|
// Reserve a spot in every object's save queue.
|
|
|
var readyToStart = Bmob.Promise.when(_.map(batch,
|
|
|
function (object) {
|
|
|
return object._allPreviousSaves || Bmob.Promise.as();
|
|
|
}));
|
|
|
var batchFinished = new Bmob.Promise();
|
|
|
Bmob._arrayEach(batch,
|
|
|
function (object) {
|
|
|
object._allPreviousSaves = batchFinished;
|
|
|
});
|
|
|
|
|
|
// Save a single batch, whether previous saves succeeded or failed.
|
|
|
return readyToStart._continueWith(function () {
|
|
|
return Bmob._request("batch", null, null, "POST", {
|
|
|
requests: _.map(batch,
|
|
|
function (object) {
|
|
|
var json = object._getSaveJSON();
|
|
|
var method = "POST";
|
|
|
|
|
|
var path = "/1/classes/" + object.className;
|
|
|
if (object.id) {
|
|
|
path = path + "/" + object.id;
|
|
|
method = "PUT";
|
|
|
}
|
|
|
|
|
|
object._startSave();
|
|
|
|
|
|
return {
|
|
|
method: method,
|
|
|
path: path,
|
|
|
body: json
|
|
|
};
|
|
|
})
|
|
|
|
|
|
}).then(function (response, status, xhr) {
|
|
|
var error;
|
|
|
Bmob._arrayEach(batch,
|
|
|
function (object, i) {
|
|
|
if (response[i].success) {
|
|
|
object._finishSave(object.parse(response[i].success, status, xhr));
|
|
|
} else {
|
|
|
error = error || response[i].error;
|
|
|
object._cancelSave();
|
|
|
}
|
|
|
});
|
|
|
if (error) {
|
|
|
return Bmob.Promise.error(new Bmob.Error(error.code, error.error));
|
|
|
}
|
|
|
|
|
|
}).then(function (results) {
|
|
|
batchFinished.resolve(results);
|
|
|
return results;
|
|
|
},
|
|
|
function (error) {
|
|
|
batchFinished.reject(error);
|
|
|
return Bmob.Promise.error(error);
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
}).then(function () {
|
|
|
return object;
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Bmob.Role acl权限控制中的用户角色类
|
|
|
*
|
|
|
* <p>角色必须要有名称(名称创建后不能修改), 同时必须指定ACL</p>
|
|
|
* @class
|
|
|
* @namespace acl权限控制中的用户角色类
|
|
|
*/
|
|
|
Bmob.Role = Bmob.Object.extend("_Role",
|
|
|
/** @lends Bmob.Role.prototype */
|
|
|
{
|
|
|
// Instance Methods
|
|
|
/**
|
|
|
* 通过名称和ACL构造一个BmobRole
|
|
|
* @param {String} name 创建role的名称
|
|
|
* @param {Bmob.ACL} acl 这个角色的acl,角色必须要有一个ACL。
|
|
|
*/
|
|
|
constructor: function (name, acl) {
|
|
|
if (_.isString(name) && (acl instanceof Bmob.ACL)) {
|
|
|
Bmob.Object.prototype.constructor.call(this, null, null);
|
|
|
this.setName(name);
|
|
|
this.setACL(acl);
|
|
|
} else {
|
|
|
Bmob.Object.prototype.constructor.call(this, name, acl);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 获取角色的name。同时可以使用role.get("name")
|
|
|
* @return {String} 角色的名称
|
|
|
*/
|
|
|
getName: function () {
|
|
|
return this.get("name");
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 设置角色的名称。这个值必须要在保存前设置,而且只能设置一次
|
|
|
* <p>
|
|
|
* 角色的名称只能包含数字,字母, _, -。
|
|
|
* </p>
|
|
|
*
|
|
|
* <p>等同于使用 role.set("name", name)</p>
|
|
|
* @param {String} name 角色的名称
|
|
|
* @param {Object} options 标准options对象
|
|
|
*/
|
|
|
setName: function (name, options) {
|
|
|
return this.set("name", name, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 获取这个角色对应的用户Bmob.Users。这些用户已经被分配了权限(例如读写的权限)。
|
|
|
* 你能通过relation添加和移除这些用户
|
|
|
* <p>这等同于使用 role.relation("users")</p>
|
|
|
*
|
|
|
* @return {Bmob.Relation} the relation for the users belonging to this
|
|
|
* role.
|
|
|
*/
|
|
|
getUsers: function () {
|
|
|
return this.relation("users");
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 获取这个角色对应的角色Bmob.Roles。这些用户已经被分配了权限(例如读写的权限)。
|
|
|
* 你能通过relation添加和移除这些用户
|
|
|
* <p>这等同于使用 role.relation("roles")</p>
|
|
|
*
|
|
|
* @return {Bmob.Relation} the relation for the roles belonging to this
|
|
|
* role.
|
|
|
*/
|
|
|
getRoles: function () {
|
|
|
return this.relation("roles");
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* @ignore
|
|
|
*/
|
|
|
validate: function (attrs, options) {
|
|
|
if ("name" in attrs && attrs.name !== this.getName()) {
|
|
|
var newName = attrs.name;
|
|
|
if (this.id && this.id !== attrs.objectId) {
|
|
|
// Check to see if the objectId being set matches this.id.
|
|
|
// This happens during a fetch -- the id is set before calling fetch.
|
|
|
// Let the name be set in this case.
|
|
|
return new Bmob.Error(Bmob.Error.OTHER_CAUSE, "A role's name can only be set before it has been saved.");
|
|
|
}
|
|
|
if (!_.isString(newName)) {
|
|
|
return new Bmob.Error(Bmob.Error.OTHER_CAUSE, "A role's name must be a String.");
|
|
|
}
|
|
|
if (!(/^[0-9a-zA-Z\-_ ]+$/).test(newName)) {
|
|
|
return new Bmob.Error(Bmob.Error.OTHER_CAUSE, "A role's name can only contain alphanumeric characters, _," + " -, and spaces.");
|
|
|
}
|
|
|
}
|
|
|
if (Bmob.Object.prototype.validate) {
|
|
|
return Bmob.Object.prototype.validate.call(this, attrs, options);
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
*创建model和options的实体。特别地,你不会直接调用这个方法,你会<code>Bmob.Collection.extend</code>通过创建一*个子类。
|
|
|
* @param {Array} <code>Bmob.Object</code>数组.
|
|
|
*
|
|
|
* @param {Object} options Backbone-style options 的可选options object.
|
|
|
* 有效的 options<ul>
|
|
|
* <li>model: Bmob.Object
|
|
|
* <li>query: Bmob.Query
|
|
|
* <li>comparator: 属性名称或排序函数
|
|
|
* </ul>
|
|
|
*
|
|
|
* @see Bmob.Collection.extend
|
|
|
*
|
|
|
*
|
|
|
* <p>提供标准的 collection class。 更详细的信息请看
|
|
|
* <a href="http://documentcloud.github.com/backbone/#Collection">Backbone
|
|
|
* documentation</a>.</p>
|
|
|
*/
|
|
|
Bmob.Collection = function (models, options) {
|
|
|
options = options || {};
|
|
|
if (options.comparator) {
|
|
|
this.comparator = options.comparator;
|
|
|
}
|
|
|
if (options.model) {
|
|
|
this.model = options.model;
|
|
|
}
|
|
|
if (options.query) {
|
|
|
this.query = options.query;
|
|
|
}
|
|
|
this._reset();
|
|
|
this.initialize.apply(this, arguments);
|
|
|
if (models) {
|
|
|
this.reset(models, {
|
|
|
silent: true,
|
|
|
parse: options.parse
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// Define the Collection's inheritable methods.
|
|
|
_.extend(Bmob.Collection.prototype, Bmob.Events,
|
|
|
/** @lends Bmob.Collection.prototype */
|
|
|
{
|
|
|
|
|
|
// The default model for a collection is just a Bmob.Object.
|
|
|
// This should be overridden in most cases.
|
|
|
model: Bmob.Object,
|
|
|
|
|
|
/**
|
|
|
* Initialize 默认是空函数. 请根据自身的逻辑重写这个方法
|
|
|
*/
|
|
|
initialize: function () { },
|
|
|
|
|
|
/**
|
|
|
*
|
|
|
* json 格式的models'属性数组
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
return this.map(function (model) {
|
|
|
return model.toJSON();
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加model,或者一系列的对象集合。传入**silent**避免触发`add`事件。
|
|
|
*/
|
|
|
add: function (models, options) {
|
|
|
var i, index, length, model, cid, id, cids = {},
|
|
|
ids = {};
|
|
|
options = options || {};
|
|
|
models = _.isArray(models) ? models.slice() : [models];
|
|
|
|
|
|
// Begin by turning bare objects into model references, and preventing
|
|
|
// invalid models or duplicate models from being added.
|
|
|
for (i = 0, length = models.length; i < length; i++) {
|
|
|
models[i] = this._prepareModel(models[i], options);
|
|
|
model = models[i];
|
|
|
if (!model) {
|
|
|
throw new Error("Can't add an invalid model to a collection");
|
|
|
}
|
|
|
cid = model.cid;
|
|
|
if (cids[cid] || this._byCid[cid]) {
|
|
|
throw new Error("Duplicate cid: can't add the same model " + "to a collection twice");
|
|
|
}
|
|
|
id = model.id;
|
|
|
if (!Bmob._isNullOrUndefined(id) && (ids[id] || this._byId[id])) {
|
|
|
throw new Error("Duplicate id: can't add the same model " + "to a collection twice");
|
|
|
}
|
|
|
ids[id] = model;
|
|
|
cids[cid] = model;
|
|
|
}
|
|
|
|
|
|
// Listen to added models' events, and index models for lookup by
|
|
|
// `id` and by `cid`.
|
|
|
for (i = 0; i < length; i++) {
|
|
|
(model = models[i]).on('all', this._onModelEvent, this);
|
|
|
this._byCid[model.cid] = model;
|
|
|
if (model.id) {
|
|
|
this._byId[model.id] = model;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Insert models into the collection, re-sorting if needed, and triggering
|
|
|
// `add` events unless silenced.
|
|
|
this.length += length;
|
|
|
index = Bmob._isNullOrUndefined(options.at) ? this.models.length : options.at;
|
|
|
this.models.splice.apply(this.models, [index, 0].concat(models));
|
|
|
if (this.comparator) {
|
|
|
this.sort({
|
|
|
silent: true
|
|
|
});
|
|
|
}
|
|
|
if (options.silent) {
|
|
|
return this;
|
|
|
}
|
|
|
for (i = 0, length = this.models.length; i < length; i++) {
|
|
|
model = this.models[i];
|
|
|
if (cids[model.cid]) {
|
|
|
options.index = i;
|
|
|
model.trigger('add', model, this, options);
|
|
|
}
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 移除一个model,或者从集合中移除一系列models。当移除对象时,传入silent避免触发<code>remove</code>事件。
|
|
|
*/
|
|
|
remove: function (models, options) {
|
|
|
var i, l, index, model;
|
|
|
options = options || {};
|
|
|
models = _.isArray(models) ? models.slice() : [models];
|
|
|
for (i = 0, l = models.length; i < l; i++) {
|
|
|
model = this.getByCid(models[i]) || this.get(models[i]);
|
|
|
if (!model) {
|
|
|
continue;
|
|
|
}
|
|
|
delete this._byId[model.id];
|
|
|
delete this._byCid[model.cid];
|
|
|
index = this.indexOf(model);
|
|
|
this.models.splice(index, 1);
|
|
|
this.length--;
|
|
|
if (!options.silent) {
|
|
|
options.index = index;
|
|
|
model.trigger('remove', model, this, options);
|
|
|
}
|
|
|
this._removeReference(model);
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 通过id获取一个model
|
|
|
*/
|
|
|
get: function (id) {
|
|
|
return id && this._byId[id.id || id];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 通过client id获取一个model
|
|
|
*/
|
|
|
getByCid: function (cid) {
|
|
|
return cid && this._byCid[cid.cid || cid];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 通过下标获取一个model
|
|
|
*/
|
|
|
at: function (index) {
|
|
|
return this.models[index];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 强制collection对自身的元素进行重新排序。一般情况下你不需要调用这个函数,因为当添加对象时这个函数会自动调用
|
|
|
*/
|
|
|
sort: function (options) {
|
|
|
options = options || {};
|
|
|
if (!this.comparator) {
|
|
|
throw new Error('Cannot sort a set without a comparator');
|
|
|
}
|
|
|
var boundComparator = _.bind(this.comparator, this);
|
|
|
if (this.comparator.length === 1) {
|
|
|
this.models = this.sortBy(boundComparator);
|
|
|
} else {
|
|
|
this.models.sort(boundComparator);
|
|
|
}
|
|
|
if (!options.silent) {
|
|
|
this.trigger('reset', this, options);
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 采集集合中每个对象的属性
|
|
|
*/
|
|
|
pluck: function (attr) {
|
|
|
return _.map(this.models,
|
|
|
function (model) {
|
|
|
return model.get(attr);
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* When you have more items than you want to add or remove individually,
|
|
|
* you can reset the entire set with a new list of models, without firing
|
|
|
* any `add` or `remove` events. Fires `reset` when finished.
|
|
|
*/
|
|
|
reset: function (models, options) {
|
|
|
var self = this;
|
|
|
models = models || [];
|
|
|
options = options || {};
|
|
|
Bmob._arrayEach(this.models,
|
|
|
function (model) {
|
|
|
self._removeReference(model);
|
|
|
});
|
|
|
this._reset();
|
|
|
this.add(models, {
|
|
|
silent: true,
|
|
|
parse: options.parse
|
|
|
});
|
|
|
if (!options.silent) {
|
|
|
this.trigger('reset', this, options);
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Fetches the default set of models for this collection, resetting the
|
|
|
* collection when they arrive. If `add: true` is passed, appends the
|
|
|
* models to the collection instead of resetting.
|
|
|
*/
|
|
|
fetch: function (options) {
|
|
|
options = _.clone(options) || {};
|
|
|
if (options.parse === undefined) {
|
|
|
options.parse = true;
|
|
|
}
|
|
|
var collection = this;
|
|
|
var query = this.query || new Bmob.Query(this.model);
|
|
|
return query.find().then(function (results) {
|
|
|
if (options.add) {
|
|
|
collection.add(results, options);
|
|
|
} else {
|
|
|
collection.reset(results, options);
|
|
|
}
|
|
|
return collection;
|
|
|
})._thenRunCallbacks(options, this);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Creates a new instance of a model in this collection. Add the model to
|
|
|
* the collection immediately, unless `wait: true` is passed, in which case
|
|
|
* we wait for the server to agree.
|
|
|
*/
|
|
|
create: function (model, options) {
|
|
|
var coll = this;
|
|
|
options = options ? _.clone(options) : {};
|
|
|
model = this._prepareModel(model, options);
|
|
|
if (!model) {
|
|
|
return false;
|
|
|
}
|
|
|
if (!options.wait) {
|
|
|
coll.add(model, options);
|
|
|
}
|
|
|
var success = options.success;
|
|
|
options.success = function (nextModel, resp, xhr) {
|
|
|
if (options.wait) {
|
|
|
coll.add(nextModel, options);
|
|
|
}
|
|
|
if (success) {
|
|
|
success(nextModel, resp);
|
|
|
} else {
|
|
|
nextModel.trigger('sync', model, resp, options);
|
|
|
}
|
|
|
};
|
|
|
model.save(null, options);
|
|
|
return model;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Converts a response into a list of models to be added to the collection.
|
|
|
* The default implementation is just to pass it through.
|
|
|
* @ignore
|
|
|
*/
|
|
|
parse: function (resp, xhr) {
|
|
|
return resp;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Proxy to _'s chain. Can't be proxied the same way the rest of the
|
|
|
* underscore methods are proxied because it relies on the underscore
|
|
|
* constructor.
|
|
|
*/
|
|
|
chain: function () {
|
|
|
return _(this.models).chain();
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Reset all internal state. Called when the collection is reset.
|
|
|
*/
|
|
|
_reset: function (options) {
|
|
|
this.length = 0;
|
|
|
this.models = [];
|
|
|
this._byId = {};
|
|
|
this._byCid = {};
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Prepare a model or hash of attributes to be added to this collection.
|
|
|
*/
|
|
|
_prepareModel: function (model, options) {
|
|
|
if (!(model instanceof Bmob.Object)) {
|
|
|
var attrs = model;
|
|
|
options.collection = this;
|
|
|
model = new this.model(attrs, options);
|
|
|
if (!model._validate(model.attributes, options)) {
|
|
|
model = false;
|
|
|
}
|
|
|
} else if (!model.collection) {
|
|
|
model.collection = this;
|
|
|
}
|
|
|
return model;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Internal method to remove a model's ties to a collection.
|
|
|
*/
|
|
|
_removeReference: function (model) {
|
|
|
if (this === model.collection) {
|
|
|
delete model.collection;
|
|
|
}
|
|
|
model.off('all', this._onModelEvent, this);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Internal method called every time a model in the set fires an event.
|
|
|
* Sets need to update their indexes when models change ids. All other
|
|
|
* events simply proxy through. "add" and "remove" events that originate
|
|
|
* in other collections are ignored.
|
|
|
*/
|
|
|
_onModelEvent: function (ev, model, collection, options) {
|
|
|
if ((ev === 'add' || ev === 'remove') && collection !== this) {
|
|
|
return;
|
|
|
}
|
|
|
if (ev === 'destroy') {
|
|
|
this.remove(model, options);
|
|
|
}
|
|
|
if (model && ev === 'change:objectId') {
|
|
|
delete this._byId[model.previous("objectId")];
|
|
|
this._byId[model.id] = model;
|
|
|
}
|
|
|
this.trigger.apply(this, arguments);
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// Underscore methods that we want to implement on the Collection.
|
|
|
var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
|
|
|
|
|
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
|
|
Bmob._arrayEach(methods,
|
|
|
function (method) {
|
|
|
Bmob.Collection.prototype[method] = function () {
|
|
|
return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
|
|
|
};
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* Creates a new subclass of <code>Bmob.Collection</code>. For example,<pre>
|
|
|
* var MyCollection = Bmob.Collection.extend({
|
|
|
* // Instance properties
|
|
|
*
|
|
|
* model: MyClass,
|
|
|
* query: MyQuery,
|
|
|
*
|
|
|
* getFirst: function() {
|
|
|
* return this.at(0);
|
|
|
* }
|
|
|
* }, {
|
|
|
* // Class properties
|
|
|
*
|
|
|
* makeOne: function() {
|
|
|
* return new MyCollection();
|
|
|
* }
|
|
|
* });
|
|
|
*
|
|
|
* var collection = new MyCollection();
|
|
|
* </pre>
|
|
|
*
|
|
|
* @function
|
|
|
* @param {Object} instanceProps Instance properties for the collection.
|
|
|
* @param {Object} classProps Class properies for the collection.
|
|
|
* @return {Class} A new subclass of <code>Bmob.Collection</code>.
|
|
|
*/
|
|
|
Bmob.Collection.extend = Bmob._extend;
|
|
|
|
|
|
/**
|
|
|
* Creating a Bmob.View creates its initial element outside of the DOM,
|
|
|
* if an existing element is not provided...
|
|
|
*
|
|
|
* <p>A fork of Backbone.View, provided for your convenience. If you use this
|
|
|
* class, you must also include jQuery, or another library that provides a
|
|
|
* jQuery-compatible $ function. For more information, see the
|
|
|
* <a href="http://documentcloud.github.com/backbone/#View">Backbone
|
|
|
* documentation</a>.</p>
|
|
|
* <p><strong><em>Available in the client SDK only.</em></strong></p>
|
|
|
*/
|
|
|
Bmob.View = function (options) {
|
|
|
this.cid = _.uniqueId('view');
|
|
|
this._configure(options || {});
|
|
|
this._ensureElement();
|
|
|
this.initialize.apply(this, arguments);
|
|
|
this.delegateEvents();
|
|
|
};
|
|
|
|
|
|
// Cached regex to split keys for `delegate`.
|
|
|
var eventSplitter = /^(\S+)\s*(.*)$/;
|
|
|
|
|
|
// List of view options to be merged as properties.
|
|
|
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
|
|
|
|
|
// Set up all inheritable **Bmob.View** properties and methods.
|
|
|
_.extend(Bmob.View.prototype, Bmob.Events,
|
|
|
/** @lends Bmob.View.prototype */
|
|
|
{
|
|
|
|
|
|
// The default `tagName` of a View's element is `"div"`.
|
|
|
tagName: 'div',
|
|
|
|
|
|
/**
|
|
|
* jQuery delegate for element lookup, scoped to DOM elements within the
|
|
|
* current view. This should be prefered to global lookups where possible.
|
|
|
*/
|
|
|
$: function (selector) {
|
|
|
return this.$el.find(selector);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Initialize is an empty function by default. Override it with your own
|
|
|
* initialization logic.
|
|
|
*/
|
|
|
initialize: function () { },
|
|
|
|
|
|
/**
|
|
|
* The core function that your view should override, in order
|
|
|
* to populate its element (`this.el`), with the appropriate HTML. The
|
|
|
* convention is for **render** to always return `this`.
|
|
|
*/
|
|
|
render: function () {
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Remove this view from the DOM. Note that the view isn't present in the
|
|
|
* DOM by default, so calling this method may be a no-op.
|
|
|
*/
|
|
|
remove: function () {
|
|
|
this.$el.remove();
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* For small amounts of DOM Elements, where a full-blown template isn't
|
|
|
* needed, use **make** to manufacture elements, one at a time.
|
|
|
* <pre>
|
|
|
* var el = this.make('li', {'class': 'row'},
|
|
|
* this.model.escape('title'));</pre>
|
|
|
*/
|
|
|
make: function (tagName, attributes, content) {
|
|
|
var el = document.createElement(tagName);
|
|
|
if (attributes) {
|
|
|
Bmob.$(el).attr(attributes);
|
|
|
}
|
|
|
if (content) {
|
|
|
Bmob.$(el).html(content);
|
|
|
}
|
|
|
return el;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Changes the view's element (`this.el` property), including event
|
|
|
* re-delegation.
|
|
|
*/
|
|
|
setElement: function (element, delegate) {
|
|
|
this.$el = Bmob.$(element);
|
|
|
this.el = this.$el[0];
|
|
|
if (delegate !== false) {
|
|
|
this.delegateEvents();
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Set callbacks. <code>this.events</code> is a hash of
|
|
|
* <pre>
|
|
|
* *{"event selector": "callback"}*
|
|
|
*
|
|
|
* {
|
|
|
* 'mousedown .title': 'edit',
|
|
|
* 'click .button': 'save'
|
|
|
* 'click .open': function(e) { ... }
|
|
|
* }
|
|
|
* </pre>
|
|
|
* pairs. Callbacks will be bound to the view, with `this` set properly.
|
|
|
* Uses event delegation for efficiency.
|
|
|
* Omitting the selector binds the event to `this.el`.
|
|
|
* This only works for delegate-able events: not `focus`, `blur`, and
|
|
|
* not `change`, `submit`, and `reset` in Internet Explorer.
|
|
|
*/
|
|
|
delegateEvents: function (events) {
|
|
|
events = events || Bmob._getValue(this, 'events');
|
|
|
if (!events) {
|
|
|
return;
|
|
|
}
|
|
|
this.undelegateEvents();
|
|
|
var self = this;
|
|
|
Bmob._objectEach(events,
|
|
|
function (method, key) {
|
|
|
if (!_.isFunction(method)) {
|
|
|
method = self[events[key]];
|
|
|
}
|
|
|
if (!method) {
|
|
|
throw new Error('Event "' + events[key] + '" does not exist');
|
|
|
}
|
|
|
var match = key.match(eventSplitter);
|
|
|
var eventName = match[1],
|
|
|
selector = match[2];
|
|
|
method = _.bind(method, self);
|
|
|
eventName += '.delegateEvents' + self.cid;
|
|
|
if (selector === '') {
|
|
|
self.$el.bind(eventName, method);
|
|
|
} else {
|
|
|
self.$el.delegate(selector, eventName, method);
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Clears all callbacks previously bound to the view with `delegateEvents`.
|
|
|
* You usually don't need to use this, but may wish to if you have multiple
|
|
|
* Backbone views attached to the same DOM element.
|
|
|
*/
|
|
|
undelegateEvents: function () {
|
|
|
this.$el.unbind('.delegateEvents' + this.cid);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Performs the initial configuration of a View with a set of options.
|
|
|
* Keys with special meaning *(model, collection, id, className)*, are
|
|
|
* attached directly to the view.
|
|
|
*/
|
|
|
_configure: function (options) {
|
|
|
if (this.options) {
|
|
|
options = _.extend({},
|
|
|
this.options, options);
|
|
|
}
|
|
|
var self = this;
|
|
|
_.each(viewOptions,
|
|
|
function (attr) {
|
|
|
if (options[attr]) {
|
|
|
self[attr] = options[attr];
|
|
|
}
|
|
|
});
|
|
|
this.options = options;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Ensure that the View has a DOM element to render into.
|
|
|
* If `this.el` is a string, pass it through `$()`, take the first
|
|
|
* matching element, and re-assign it to `el`. Otherwise, create
|
|
|
* an element from the `id`, `className` and `tagName` properties.
|
|
|
*/
|
|
|
_ensureElement: function () {
|
|
|
if (!this.el) {
|
|
|
var attrs = Bmob._getValue(this, 'attributes') || {};
|
|
|
if (this.id) {
|
|
|
attrs.id = this.id;
|
|
|
}
|
|
|
if (this.className) {
|
|
|
attrs['class'] = this.className;
|
|
|
}
|
|
|
this.setElement(this.make(this.tagName, attrs), false);
|
|
|
} else {
|
|
|
this.setElement(this.el, false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @function
|
|
|
* @param {Object} instanceProps Instance properties for the view.
|
|
|
* @param {Object} classProps Class properies for the view.
|
|
|
* @return {Class} A new subclass of <code>Bmob.View</code>.
|
|
|
*/
|
|
|
Bmob.View.extend = Bmob._extend;
|
|
|
|
|
|
/**
|
|
|
* @class
|
|
|
*
|
|
|
* <p>这个类是Bmob.Object的子类,同时拥有Bmob.Object的所有函数,但是扩展了用户的特殊函数,例如验证,登录等</p>
|
|
|
*/
|
|
|
Bmob.User = Bmob.Object.extend("_User",
|
|
|
/** @lends Bmob.User.prototype */
|
|
|
{
|
|
|
// Instance Variables
|
|
|
_isCurrentUser: false,
|
|
|
|
|
|
// Instance Methods
|
|
|
/**
|
|
|
* Internal method to handle special fields in a _User response.
|
|
|
*/
|
|
|
_mergeMagicFields: function (attrs) {
|
|
|
if (attrs.sessionToken) {
|
|
|
this._sessionToken = attrs.sessionToken;
|
|
|
delete attrs.sessionToken;
|
|
|
}
|
|
|
Bmob.User.__super__._mergeMagicFields.call(this, attrs);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Removes null values from authData (which exist temporarily for
|
|
|
* unlinking)
|
|
|
*/
|
|
|
_cleanupAuthData: function () {
|
|
|
if (!this.isCurrent()) {
|
|
|
return;
|
|
|
}
|
|
|
var authData = this.get('authData');
|
|
|
if (!authData) {
|
|
|
return;
|
|
|
}
|
|
|
Bmob._objectEach(this.get('authData'),
|
|
|
function (value, key) {
|
|
|
if (!authData[key]) {
|
|
|
delete authData[key];
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Synchronizes authData for all providers.
|
|
|
*/
|
|
|
_synchronizeAllAuthData: function () {
|
|
|
var authData = this.get('authData');
|
|
|
if (!authData) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
var self = this;
|
|
|
Bmob._objectEach(this.get('authData'),
|
|
|
function (value, key) {
|
|
|
self._synchronizeAuthData(key);
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Synchronizes auth data for a provider (e.g. puts the access token in the
|
|
|
* right place to be used by the Facebook SDK).
|
|
|
*/
|
|
|
_synchronizeAuthData: function (provider) {
|
|
|
if (!this.isCurrent()) {
|
|
|
return;
|
|
|
}
|
|
|
var authType;
|
|
|
if (_.isString(provider)) {
|
|
|
authType = provider;
|
|
|
provider = Bmob.User._authProviders[authType];
|
|
|
} else {
|
|
|
authType = provider.getAuthType();
|
|
|
}
|
|
|
var authData = this.get('authData');
|
|
|
if (!authData || !provider) {
|
|
|
return;
|
|
|
}
|
|
|
var success = provider.restoreAuthentication(authData[authType]);
|
|
|
if (!success) {
|
|
|
this._unlinkFrom(provider);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_handleSaveResult: function (makeCurrent) {
|
|
|
// Clean up and synchronize the authData object, removing any unset values
|
|
|
if (makeCurrent) {
|
|
|
this._isCurrentUser = true;
|
|
|
}
|
|
|
this._cleanupAuthData();
|
|
|
this._synchronizeAllAuthData();
|
|
|
// Don't keep the password around.
|
|
|
delete this._serverData.password;
|
|
|
this._rebuildEstimatedDataForKey("password");
|
|
|
this._refreshCache();
|
|
|
if (makeCurrent || this.isCurrent()) {
|
|
|
Bmob.User._saveCurrentUser(this);
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
/**
|
|
|
* 使用第三方登录,登录或注册
|
|
|
* @Magic 2.0.0
|
|
|
* @return {Bmob.User}
|
|
|
*/
|
|
|
_linkWith: function _linkWith(provider, data) {
|
|
|
var _this = this;
|
|
|
|
|
|
var authType;
|
|
|
if (_.isString(provider)) {
|
|
|
authType = provider;
|
|
|
provider = Bmob.User._authProviders[provider];
|
|
|
} else {
|
|
|
authType = provider.getAuthType();
|
|
|
}
|
|
|
if (data) {
|
|
|
var authData = this.get('authData') || {};
|
|
|
authData[authType] = data;
|
|
|
this.set('authData', authData);
|
|
|
|
|
|
var promise = new Bmob.Promise();
|
|
|
this.save({
|
|
|
'authData': authData
|
|
|
}, newOptions).then(
|
|
|
function (model) {
|
|
|
model._handleSaveResult(true);
|
|
|
promise.resolve(model);
|
|
|
}
|
|
|
);
|
|
|
|
|
|
|
|
|
return promise._thenRunCallbacks({});
|
|
|
|
|
|
// Overridden so that the user can be made the current user.
|
|
|
var newOptions = _.clone(data) || {};
|
|
|
newOptions.success = function (model) {
|
|
|
model._handleSaveResult(true);
|
|
|
if (data.success) {
|
|
|
data.success.apply(this, arguments);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
return this.save({
|
|
|
'authData': authData
|
|
|
},
|
|
|
newOptions);
|
|
|
} else {
|
|
|
return provider.authenticate().then(function (result) {
|
|
|
return _this._linkWith(provider, result);
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 使用当前使用小程序的微信用户身份注册或登录,成功后用户的 session 会在设备上持久化保存,之后可以使用 Bmob.User.current() 获取当前登录用户。
|
|
|
* @Magic 2.0.0
|
|
|
* @return {Bmob.User}
|
|
|
*/
|
|
|
loginWithWeapp: function (code) {
|
|
|
var that = this;
|
|
|
var promise = new Bmob.Promise();
|
|
|
Bmob.User.requestOpenId(code, {
|
|
|
success: function (authData) {//获取授权成功
|
|
|
var platform = "weapp";
|
|
|
var user = Bmob.Object._create("_User");
|
|
|
user._linkWith(platform, authData).then(function (resp) {
|
|
|
promise.resolve(resp);
|
|
|
}, function (error) {
|
|
|
promise.reject(error);
|
|
|
});
|
|
|
|
|
|
},
|
|
|
error: function (error) {
|
|
|
promise.reject(error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
);
|
|
|
return promise._thenRunCallbacks({});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
/**
|
|
|
* Unlinks a user from a service.
|
|
|
*/
|
|
|
_unlinkFrom: function (provider, options) {
|
|
|
var authType;
|
|
|
if (_.isString(provider)) {
|
|
|
authType = provider;
|
|
|
provider = Bmob.User._authProviders[provider];
|
|
|
} else {
|
|
|
authType = provider.getAuthType();
|
|
|
}
|
|
|
var newOptions = _.clone(options);
|
|
|
var self = this;
|
|
|
newOptions.authData = null;
|
|
|
newOptions.success = function (model) {
|
|
|
self._synchronizeAuthData(provider);
|
|
|
if (options.success) {
|
|
|
options.success.apply(this, arguments);
|
|
|
}
|
|
|
};
|
|
|
return this._linkWith(provider, newOptions);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Checks whether a user is linked to a service.
|
|
|
*/
|
|
|
_isLinked: function (provider) {
|
|
|
var authType;
|
|
|
if (_.isString(provider)) {
|
|
|
authType = provider;
|
|
|
} else {
|
|
|
authType = provider.getAuthType();
|
|
|
}
|
|
|
var authData = this.get('authData') || {};
|
|
|
return !!authData[authType];
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Deauthenticates all providers.
|
|
|
*/
|
|
|
_logOutWithAll: function () {
|
|
|
var authData = this.get('authData');
|
|
|
if (!authData) {
|
|
|
return;
|
|
|
}
|
|
|
var self = this;
|
|
|
Bmob._objectEach(this.get('authData'),
|
|
|
function (value, key) {
|
|
|
self._logOutWith(key);
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Deauthenticates a single provider (e.g. removing access tokens from the
|
|
|
* Facebook SDK).
|
|
|
*/
|
|
|
_logOutWith: function (provider) {
|
|
|
if (!this.isCurrent()) {
|
|
|
return;
|
|
|
}
|
|
|
if (_.isString(provider)) {
|
|
|
provider = Bmob.User._authProviders[provider];
|
|
|
}
|
|
|
if (provider && provider.deauthenticate) {
|
|
|
provider.deauthenticate();
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 注册一个新用户。当创建一个新用户时,应该调用这个方法而不是save方法。这个方法会创建
|
|
|
* 一个新的Bmob.User在服务器上,同时保存session在本地磁盘因此你可以通过<code>current</code>访问user
|
|
|
* <p>在注册前必须设置username和password</p>
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {Object} attrs 用户的额外的属性,或者null
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* @return {Bmob.Promise} 当调用结束将会返回promise。
|
|
|
* @see Bmob.User.signUp
|
|
|
*/
|
|
|
signUp: function (attrs, options) {
|
|
|
var error;
|
|
|
options = options || {};
|
|
|
|
|
|
var username = (attrs && attrs.username) || this.get("username");
|
|
|
if (!username || (username === "")) {
|
|
|
error = new Bmob.Error(Bmob.Error.OTHER_CAUSE, "Cannot sign up user with an empty name.");
|
|
|
if (options && options.error) {
|
|
|
options.error(this, error);
|
|
|
}
|
|
|
return Bmob.Promise.error(error);
|
|
|
}
|
|
|
|
|
|
var password = (attrs && attrs.password) || this.get("password");
|
|
|
if (!password || (password === "")) {
|
|
|
error = new Bmob.Error(Bmob.Error.OTHER_CAUSE, "Cannot sign up user with an empty password.");
|
|
|
if (options && options.error) {
|
|
|
options.error(this, error);
|
|
|
}
|
|
|
return Bmob.Promise.error(error);
|
|
|
}
|
|
|
|
|
|
// Overridden so that the user can be made the current user.
|
|
|
var newOptions = _.clone(options);
|
|
|
newOptions.success = function (model) {
|
|
|
model._handleSaveResult(true);
|
|
|
if (options.success) {
|
|
|
options.success.apply(this, arguments);
|
|
|
}
|
|
|
};
|
|
|
return this.save(attrs, newOptions);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 用户登录。当登录成功,将会保存session在本地,可以通过<code>current</code>获取用户对象。
|
|
|
* <p>在注册前必须设置username和password</p>
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* @see Bmob.User.logIn
|
|
|
* @return {Bmob.Promise} 当调用结束将会返回promise。
|
|
|
*/
|
|
|
logIn: function (options) {
|
|
|
var model = this;
|
|
|
var request = Bmob._request("login", null, null, "GET", this.toJSON());
|
|
|
return request.then(function (resp, status, xhr) {
|
|
|
var serverAttrs = model.parse(resp, status, xhr);
|
|
|
model._finishFetch(serverAttrs);
|
|
|
model._handleSaveResult(true);
|
|
|
return model;
|
|
|
})._thenRunCallbacks(options, this);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 保存对象
|
|
|
* @see Bmob.Object#save
|
|
|
*/
|
|
|
save: function (arg1, arg2, arg3) {
|
|
|
var i, attrs, current, options, saved;
|
|
|
if (_.isObject(arg1) || _.isNull(arg1) || _.isUndefined(arg1)) {
|
|
|
attrs = arg1;
|
|
|
options = arg2;
|
|
|
} else {
|
|
|
attrs = {};
|
|
|
attrs[arg1] = arg2;
|
|
|
options = arg3;
|
|
|
}
|
|
|
options = options || {};
|
|
|
|
|
|
var newOptions = _.clone(options);
|
|
|
newOptions.success = function (model) {
|
|
|
model._handleSaveResult(false);
|
|
|
if (options.success) {
|
|
|
options.success.apply(this, arguments);
|
|
|
}
|
|
|
};
|
|
|
return Bmob.Object.prototype.save.call(this, attrs, newOptions);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 获取一个对象
|
|
|
* @see Bmob.Object#fetch
|
|
|
*/
|
|
|
fetch: function (options) {
|
|
|
var newOptions = options ? _.clone(options) : {};
|
|
|
newOptions.success = function (model) {
|
|
|
model._handleSaveResult(false);
|
|
|
if (options && options.success) {
|
|
|
options.success.apply(this, arguments);
|
|
|
}
|
|
|
};
|
|
|
return Bmob.Object.prototype.fetch.call(this, newOptions);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回true 如果<code>current</code>可以返回这个user。
|
|
|
* @see Bmob.User#cu 你不能添加一个没保存的Bmob.Object到关系中
|
|
|
*/
|
|
|
isCurrent: function () {
|
|
|
return this._isCurrentUser;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回 get("username").
|
|
|
* @return {String}
|
|
|
* @see Bmob.Object#get
|
|
|
*/
|
|
|
getUsername: function () {
|
|
|
return this.get("username");
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 调用 set("username", username, options) 同时返回结果
|
|
|
* @param {String} username
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* @return {Boolean}
|
|
|
* @see Bmob.Object.set
|
|
|
*/
|
|
|
setUsername: function (username, options) {
|
|
|
return this.set("username", username, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 调用 set("password", password, options) 同时返回结果
|
|
|
* @param {String} password
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* @return {Boolean}
|
|
|
* @see Bmob.Object.set
|
|
|
*/
|
|
|
setPassword: function (password, options) {
|
|
|
return this.set("password", password, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回 get("email").
|
|
|
* @return {String}
|
|
|
* @see Bmob.Object#get
|
|
|
*/
|
|
|
getEmail: function () {
|
|
|
return this.get("email");
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 调用 set("email", email, options) 同时返回结果
|
|
|
* @param {String} email
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* @return {Boolean}
|
|
|
* @see Bmob.Object.set
|
|
|
*/
|
|
|
setEmail: function (email, options) {
|
|
|
return this.set("email", email, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 检查这个用户是否当前用户并且已经登录。
|
|
|
* @return (Boolean) 这个用户是否当前用户并且已经登录。
|
|
|
*/
|
|
|
authenticated: function () {
|
|
|
return !!this._sessionToken && (Bmob.User.current() && Bmob.User.current().id === this.id);
|
|
|
}
|
|
|
|
|
|
},
|
|
|
/** @lends Bmob.User */
|
|
|
{
|
|
|
// Class Variables
|
|
|
// The currently logged-in user.
|
|
|
_currentUser: null,
|
|
|
|
|
|
// Whether currentUser is known to match the serialized version on disk.
|
|
|
// This is useful for saving a localstorage check if you try to load
|
|
|
// _currentUser frequently while there is none stored.
|
|
|
_currentUserMatchesDisk: false,
|
|
|
|
|
|
// The localStorage key suffix that the current user is stored under.
|
|
|
_CURRENT_USER_KEY: "currentUser",
|
|
|
|
|
|
// The mapping of auth provider names to actual providers
|
|
|
_authProviders: {},
|
|
|
|
|
|
// Class Methods
|
|
|
/**
|
|
|
* 注册一个新用户。当创建一个新用户时,应该调用这个方法而不是save方法。这个方法会创建
|
|
|
* 一个新的Bmob.User在服务器上,同时保存session在本地磁盘因此你可以通过<code>current</code>访问user
|
|
|
*
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {String} username 注册的用户名或email
|
|
|
* @param {String} password 注册的密码
|
|
|
* @param {Object} attrs 新用户所需要的额外数据
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* @return {Bmob.Promise} 当调用结束将会返回promise。
|
|
|
* @see Bmob.User#signUp
|
|
|
*/
|
|
|
signUp: function (username, password, attrs, options) {
|
|
|
attrs = attrs || {};
|
|
|
attrs.username = username;
|
|
|
attrs.password = password;
|
|
|
var user = Bmob.Object._create("_User");
|
|
|
return user.signUp(attrs, options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 用户登录。当登录成功,将会保存session在本地,可以通过<code>current</code>获取用户对象。
|
|
|
*
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {String} username 注册的用户名或email
|
|
|
* @param {String} password 注册的密码
|
|
|
* @param {Object} options 新用户所需要的额外数据
|
|
|
* @return {Bmob.Promise} Backbone-style options 对象。
|
|
|
* @see Bmob.User#logIn
|
|
|
*/
|
|
|
logIn: function (username, password, options) {
|
|
|
var user = Bmob.Object._create("_User");
|
|
|
user._finishFetch({
|
|
|
username: username,
|
|
|
password: password
|
|
|
});
|
|
|
return user.logIn(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 退出当前登录的用户。磁盘中的session将会被移除,调用<code>current</code>将会
|
|
|
* 返回<code>null</code>。
|
|
|
*/
|
|
|
logOut: function () {
|
|
|
if (Bmob.User._currentUser !== null) {
|
|
|
Bmob.User._currentUser._logOutWithAll();
|
|
|
Bmob.User._currentUser._isCurrentUser = false;
|
|
|
}
|
|
|
Bmob.User._currentUserMatchesDisk = true;
|
|
|
Bmob.User._currentUser = null;
|
|
|
|
|
|
wx.removeStorage({
|
|
|
key: Bmob._getBmobPath(Bmob.User._CURRENT_USER_KEY),
|
|
|
success: function (res) {
|
|
|
console.log(res.data)
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把重设密码的邮件发送到用户的注册邮箱。邮件允许用户在bmob网站上重设密码。
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {String} email 用户注册的邮箱
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
*/
|
|
|
requestPasswordReset: function (email, options) {
|
|
|
var json = {
|
|
|
email: email
|
|
|
};
|
|
|
var request = Bmob._request("requestPasswordReset", null, null, "POST", json);
|
|
|
return request._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 请求验证email
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {String} email 需要验证email的email的地址
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
*/
|
|
|
requestEmailVerify: function (email, options) {
|
|
|
var json = {
|
|
|
email: email
|
|
|
};
|
|
|
var request = Bmob._request("requestEmailVerify", null, null, "POST", json);
|
|
|
return request._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 请求openid值
|
|
|
* <p>完成后调用options.success 或者 options.error</p>
|
|
|
*
|
|
|
* @param {String} code 微信的code
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
*/
|
|
|
requestOpenId: function (code, options) {
|
|
|
var json = {
|
|
|
code: code
|
|
|
};
|
|
|
var request = Bmob._request("wechatApp", code, null, "POST", json);
|
|
|
return request._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回当前已经登陆的用户。
|
|
|
* @return {Bmob.Object} 已经登录的Bmob.User.
|
|
|
*/
|
|
|
current: function () {
|
|
|
if (Bmob.User._currentUser) {
|
|
|
return Bmob.User._currentUser;
|
|
|
}
|
|
|
|
|
|
if (Bmob.User._currentUserMatchesDisk) {
|
|
|
|
|
|
return Bmob.User._currentUser;
|
|
|
}
|
|
|
|
|
|
// Load the user from local storage.
|
|
|
Bmob.User._currentUserMatchesDisk = true;
|
|
|
|
|
|
// var userData = Bmob.localStorage.getItem(Bmob._getBmobPath(
|
|
|
// Bmob.User._CURRENT_USER_KEY));
|
|
|
var userData = false;
|
|
|
try {
|
|
|
var userData = wx.getStorageSync(Bmob._getBmobPath(Bmob.User._CURRENT_USER_KEY))
|
|
|
if (userData) {
|
|
|
// Do something with return value
|
|
|
Bmob.User._currentUser = Bmob.Object._create("_User");
|
|
|
Bmob.User._currentUser._isCurrentUser = true;
|
|
|
|
|
|
var json = JSON.parse(userData);
|
|
|
Bmob.User._currentUser.id = json._id;
|
|
|
delete json._id;
|
|
|
Bmob.User._currentUser._sessionToken = json._sessionToken;
|
|
|
delete json._sessionToken;
|
|
|
Bmob.User._currentUser.set(json);
|
|
|
|
|
|
Bmob.User._currentUser._synchronizeAllAuthData();
|
|
|
Bmob.User._currentUser._refreshCache();
|
|
|
Bmob.User._currentUser._opSetQueue = [{}];
|
|
|
return Bmob.User._currentUser;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// Do something when catch error
|
|
|
return null;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Persists a user as currentUser to localStorage, and into the singleton.
|
|
|
*/
|
|
|
_saveCurrentUser: function (user) {
|
|
|
if (Bmob.User._currentUser !== user) {
|
|
|
Bmob.User.logOut();
|
|
|
}
|
|
|
user._isCurrentUser = true;
|
|
|
Bmob.User._currentUser = user;
|
|
|
Bmob.User._currentUserMatchesDisk = true;
|
|
|
|
|
|
var json = user.toJSON();
|
|
|
json._id = user.id;
|
|
|
json._sessionToken = user._sessionToken;
|
|
|
wx.setStorage({
|
|
|
key: Bmob._getBmobPath(Bmob.User._CURRENT_USER_KEY),
|
|
|
data: JSON.stringify(json)
|
|
|
})
|
|
|
},
|
|
|
|
|
|
_registerAuthenticationProvider: function (provider) {
|
|
|
Bmob.User._authProviders[provider.getAuthType()] = provider;
|
|
|
// Synchronize the current user with the auth provider.
|
|
|
if (Bmob.User.current()) {
|
|
|
Bmob.User.current()._synchronizeAuthData(provider.getAuthType());
|
|
|
}
|
|
|
},
|
|
|
|
|
|
_logInWith: function (provider, options) {
|
|
|
var user = Bmob.Object._create("_User");
|
|
|
return user._linkWith(provider, options);
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* 为Bmob.Object类创建一个新的bmob Bmob.Query 。
|
|
|
* @param objectClass -
|
|
|
* Bmob.Object的实例,或者Bmob类名
|
|
|
*
|
|
|
*
|
|
|
* <p>Bmob.Query 为Bmob.Objects定义了query操作。最常用的操作就是用query<code>find</code>
|
|
|
* 操作去获取所有的对象。例如,下面简单的操作是获取所有的<code>MyClass</code>。根据操作的成功或失败,
|
|
|
* 会回调不同的函数。
|
|
|
* <pre>
|
|
|
* var query = new Bmob.Query(MyClass);
|
|
|
* query.find({
|
|
|
* success: function(results) {
|
|
|
* // results is an array of Bmob.Object.
|
|
|
* },
|
|
|
*
|
|
|
* error: function(error) {
|
|
|
* // error is an instance of Bmob.Error.
|
|
|
* }
|
|
|
* });</pre></p>
|
|
|
*
|
|
|
* <p>Bmob.Query也可以用来获取一个id已知的对象。例如,下面的例子获取了<code>MyClass</code> 和 id <code>myId</code>
|
|
|
* 根据操作的成功或失败,会回调不同的函数。
|
|
|
* <pre>
|
|
|
* var query = new Bmob.Query(MyClass);
|
|
|
* query.get(myId, {
|
|
|
* success: function(object) {
|
|
|
* // object is an instance of Bmob.Object.
|
|
|
* },
|
|
|
*
|
|
|
* error: function(object, error) {
|
|
|
* // error is an instance of Bmob.Error.
|
|
|
* }
|
|
|
* });</pre></p>
|
|
|
*
|
|
|
* <p>Bmob.Query 同时也能获取查询结果的数目。例如,下面的例子获取了<code>MyClass</code>的数目<pre>
|
|
|
* var query = new Bmob.Query(MyClass);
|
|
|
* query.count({
|
|
|
* success: function(number) {
|
|
|
* // There are number instances of MyClass.
|
|
|
* },
|
|
|
*
|
|
|
* error: function(error) {
|
|
|
* // error is an instance of Bmob.Error.
|
|
|
* }
|
|
|
* });</pre></p>
|
|
|
|
|
|
* @class Bmob.Query 为Bmob.Objects定义了query操作
|
|
|
*/
|
|
|
Bmob.Query = function (objectClass) {
|
|
|
if (_.isString(objectClass)) {
|
|
|
objectClass = Bmob.Object._getSubclass(objectClass);
|
|
|
}
|
|
|
|
|
|
this.objectClass = objectClass;
|
|
|
|
|
|
this.className = objectClass.prototype.className;
|
|
|
|
|
|
this._where = {};
|
|
|
this._include = [];
|
|
|
this._limit = -1; // negative limit means, do not send a limit
|
|
|
this._skip = 0;
|
|
|
this._extraOptions = {};
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 通过传递query构造or的Bmob.Query对象。 For
|
|
|
* example:
|
|
|
* <pre>var compoundQuery = Bmob.Query.or(query1, query2, query3);</pre>
|
|
|
* 通过query1, query2, 和 query3创建一个or查询
|
|
|
* @param {...Bmob.Query} var_args or的query查询.
|
|
|
* @return {Bmob.Query} 查询结果.
|
|
|
*/
|
|
|
Bmob.Query.or = function () {
|
|
|
var queries = _.toArray(arguments);
|
|
|
var className = null;
|
|
|
Bmob._arrayEach(queries,
|
|
|
function (q) {
|
|
|
if (_.isNull(className)) {
|
|
|
className = q.className;
|
|
|
}
|
|
|
|
|
|
if (className !== q.className) {
|
|
|
throw "All queries must be for the same class";
|
|
|
}
|
|
|
});
|
|
|
var query = new Bmob.Query(className);
|
|
|
query._orQuery(queries);
|
|
|
return query;
|
|
|
};
|
|
|
|
|
|
Bmob.Query._extend = Bmob._extend;
|
|
|
|
|
|
Bmob.Query.prototype = {
|
|
|
//hook to iterate result. Added by dennis<xzhuang@bmob.cn>.
|
|
|
_processResult: function (obj) {
|
|
|
return obj;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 获取Bmob.Object,适用于id已经知道的情况。当查询完成会调用options.success 或 options.error。
|
|
|
* @param {} objectId 要获取的对象id
|
|
|
* @param {Object} options Backbone-style options 对象.
|
|
|
*/
|
|
|
get: function (objectId, options) {
|
|
|
var self = this;
|
|
|
self.equalTo('objectId', objectId);
|
|
|
|
|
|
return self.first().then(function (response) {
|
|
|
if (response) {
|
|
|
return response;
|
|
|
}
|
|
|
|
|
|
var errorObject = new Bmob.Error(Bmob.Error.OBJECT_NOT_FOUND, "Object not found.");
|
|
|
return Bmob.Promise.error(errorObject);
|
|
|
|
|
|
})._thenRunCallbacks(options, null);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 返回json的结局
|
|
|
* @return {Object}
|
|
|
*/
|
|
|
toJSON: function () {
|
|
|
var params = {
|
|
|
where: this._where
|
|
|
};
|
|
|
|
|
|
if (this._include.length > 0) {
|
|
|
params.include = this._include.join(",");
|
|
|
}
|
|
|
if (this._select) {
|
|
|
params.keys = this._select.join(",");
|
|
|
}
|
|
|
if (this._limit >= 0) {
|
|
|
params.limit = this._limit;
|
|
|
}
|
|
|
if (this._skip > 0) {
|
|
|
params.skip = this._skip;
|
|
|
}
|
|
|
if (this._order !== undefined) {
|
|
|
params.order = this._order;
|
|
|
}
|
|
|
|
|
|
Bmob._objectEach(this._extraOptions,
|
|
|
function (v, k) {
|
|
|
params[k] = v;
|
|
|
});
|
|
|
|
|
|
return params;
|
|
|
},
|
|
|
|
|
|
_newObject: function (response) {
|
|
|
if (typeof (obj) === "undefined") {
|
|
|
var obj;
|
|
|
}
|
|
|
if (response && response.className) {
|
|
|
obj = new Bmob.Object(response.className);
|
|
|
} else {
|
|
|
obj = new this.objectClass();
|
|
|
}
|
|
|
return obj;
|
|
|
},
|
|
|
_createRequest: function (params) {
|
|
|
return Bmob._request("classes", this.className, null, "GET", params || this.toJSON());
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查找满足查询条件的对象。完成后,options.success 或 options.error 会被调用。
|
|
|
* @param {Object} options A Backbone-style options 对象.
|
|
|
* @return {Bmob.Promise} 当查询完成后,结果的 promise 会被调用。
|
|
|
*/
|
|
|
find: function (options) {
|
|
|
var self = this;
|
|
|
|
|
|
var request = this._createRequest();
|
|
|
|
|
|
return request.then(function (response) {
|
|
|
return _.map(response.results,
|
|
|
function (json) {
|
|
|
var obj = self._newObject(response);
|
|
|
obj._finishFetch(self._processResult(json), true);
|
|
|
return obj;
|
|
|
});
|
|
|
})._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 把查询到的所有对象删除。
|
|
|
* @param {Object} options 标准的带 success and error回调的options对象。
|
|
|
* @return {Bmob.Promise} 当完成后,结果的 promise 会被调用。
|
|
|
*/
|
|
|
destroyAll: function (options) {
|
|
|
var self = this;
|
|
|
return self.find().then(function (objects) {
|
|
|
return Bmob.Object.destroyAll(objects);
|
|
|
})._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查询结果的数目。
|
|
|
* 完成后,options.success 或 options.error 会被调用。
|
|
|
*
|
|
|
* @param {Object} options A Backbone-style options 对象.
|
|
|
* @return {Bmob.Promise} 完成后,结果的 promise 会被调用。
|
|
|
*/
|
|
|
count: function (options) {
|
|
|
var params = this.toJSON();
|
|
|
params.limit = 0;
|
|
|
params.count = 1;
|
|
|
var request = this._createRequest(params);
|
|
|
|
|
|
return request.then(function (response) {
|
|
|
return response.count;
|
|
|
})._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 在返回的结果中,返回第一个对象
|
|
|
* 完成后,options.success 或 options.error 会被调用。
|
|
|
*
|
|
|
* @param {Object} options A Backbone-style options 对象.
|
|
|
* @return {Bmob.Promise} 完成后,结果的 promise 会被调用。
|
|
|
*/
|
|
|
first: function (options) {
|
|
|
var self = this;
|
|
|
|
|
|
var params = this.toJSON();
|
|
|
params.limit = 1;
|
|
|
var request = this._createRequest(params);
|
|
|
|
|
|
return request.then(function (response) {
|
|
|
return _.map(response.results,
|
|
|
function (json) {
|
|
|
var obj = self._newObject();
|
|
|
obj._finishFetch(self._processResult(json), true);
|
|
|
return obj;
|
|
|
})[0];
|
|
|
})._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查询后返回一个Bmob.Collection
|
|
|
* @return {Bmob.Collection}
|
|
|
*/
|
|
|
collection: function (items, options) {
|
|
|
options = options || {};
|
|
|
return new Bmob.Collection(items, _.extend(options, {
|
|
|
model: this._objectClass || this.objectClass,
|
|
|
query: this
|
|
|
}));
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 在返回结果前设置跳过的结果数目。是在分页时使用的。默认是跳过0条结果。
|
|
|
* @param {Number} n 跳过的数目。
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
skip: function (n) {
|
|
|
this._skip = n;
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 限制返回结果的数目。默认限制是100,最大限制数是1000.
|
|
|
* @param {Number} n 限制的数目。
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
limit: function (n) {
|
|
|
this._limit = n;
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个equal查询(key value 形式)。
|
|
|
* @param {String} key key
|
|
|
* @param value key对应的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
equalTo: function (key, value) {
|
|
|
this._where[key] = Bmob._encode(value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Helper for condition queries
|
|
|
*/
|
|
|
_addCondition: function (key, condition, value) {
|
|
|
// Check if we already have a condition
|
|
|
if (!this._where[key]) {
|
|
|
this._where[key] = {};
|
|
|
}
|
|
|
this._where[key][condition] = Bmob._encode(value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个not equal查询(key value 形式)。
|
|
|
* @param {String} key key
|
|
|
* @param value key对应的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
notEqualTo: function (key, value) {
|
|
|
this._addCondition(key, "$ne", value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个小于查询。
|
|
|
* @param {String} key 需要检查的key.
|
|
|
* @param value key所对应的必须少于的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
lessThan: function (key, value) {
|
|
|
this._addCondition(key, "$lt", value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个大于查询。
|
|
|
* @param {String} key 需要检查的key.
|
|
|
* @param value key所对应的必须大于的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
greaterThan: function (key, value) {
|
|
|
this._addCondition(key, "$gt", value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个小于等于查询。
|
|
|
* @param {String} key 需要检查的key.
|
|
|
* @param value key所对应的必须少于等于的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
lessThanOrEqualTo: function (key, value) {
|
|
|
this._addCondition(key, "$lte", value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个大于等于查询。
|
|
|
* @param {String} key 需要检查的key.
|
|
|
* @param value key所对应的必须大于等于的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
greaterThanOrEqualTo: function (key, value) {
|
|
|
this._addCondition(key, "$gte", value);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加key中包含任意一个值查询。
|
|
|
* @param {String} key 需要检查的key.
|
|
|
* @param {Array} values 需要包含的值的数组
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
containedIn: function (key, values) {
|
|
|
this._addCondition(key, "$in", values);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加key中不包含任意一个值查询。
|
|
|
* @param {String} key 需要检查的key.
|
|
|
* @param {Array} values 不需要包含的值的数组
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
notContainedIn: function (key, values) {
|
|
|
this._addCondition(key, "$nin", values);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加key中包含全部值查询。
|
|
|
* @param {String} key 需要检查的key
|
|
|
* @param {Array} values 需要包含的全部值的数组
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
containsAll: function (key, values) {
|
|
|
this._addCondition(key, "$all", values);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加key是否存在的查询。
|
|
|
* @param {String} key 需要检查是否存在的key。
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
exists: function (key) {
|
|
|
this._addCondition(key, "$exists", true);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加key是否不存在的查询。
|
|
|
* @param {String} key 需要检查是否不存在的key。
|
|
|
* @return {Bmob.Query}返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
doesNotExist: function (key) {
|
|
|
this._addCondition(key, "$exists", false);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加正则表达式的查询。
|
|
|
* 当数据很大的时候这个操作可能很慢。
|
|
|
* @param {String} key 需要检查的key
|
|
|
* @param {RegExp} regex 需要匹配的正则表达式
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
matches: function (key, regex, modifiers) {
|
|
|
this._addCondition(key, "$regex", regex);
|
|
|
if (!modifiers) {
|
|
|
modifiers = "";
|
|
|
}
|
|
|
// Javascript regex options support mig as inline options but store them
|
|
|
// as properties of the object. We support mi & should migrate them to
|
|
|
// modifiers
|
|
|
if (regex.ignoreCase) {
|
|
|
modifiers += 'i';
|
|
|
}
|
|
|
if (regex.multiline) {
|
|
|
modifiers += 'm';
|
|
|
}
|
|
|
|
|
|
if (modifiers && modifiers.length) {
|
|
|
this._addCondition(key, "$options", modifiers);
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个Bmob.Query的匹配查询。
|
|
|
* @param {String} key 需要检查的key。
|
|
|
* @param {Bmob.Query} query 需要匹配的query
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
matchesQuery: function (key, query) {
|
|
|
var queryJSON = query.toJSON();
|
|
|
queryJSON.className = query.className;
|
|
|
this._addCondition(key, "$inQuery", queryJSON);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加一个Bmob.Query的不匹配查询。
|
|
|
* @param {String} key 需要检查的key。
|
|
|
* @param {Bmob.Query} 不需要匹配的query
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
doesNotMatchQuery: function (key, query) {
|
|
|
var queryJSON = query.toJSON();
|
|
|
queryJSON.className = query.className;
|
|
|
this._addCondition(key, "$notInQuery", queryJSON);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加查询: key's value 匹配一个对象,这个对象通过不同的Bmob.Query返回。
|
|
|
* @param {String} key 需要匹配的key值
|
|
|
* @param {String} queryKey 返回通过匹配的查询的对象的键
|
|
|
* @param {Bmob.Query} query 需要运行的查询
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
matchesKeyInQuery: function (key, queryKey, query) {
|
|
|
var queryJSON = query.toJSON();
|
|
|
queryJSON.className = query.className;
|
|
|
this._addCondition(key, "$select", {
|
|
|
key: queryKey,
|
|
|
query: queryJSON
|
|
|
});
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加查询: key's value 不匹配一个对象,这个对象通过不同的Bmob.Query返回。
|
|
|
* @param {String} key 需要匹配的key值
|
|
|
* excluded.
|
|
|
* @param {String} queryKey 返回通过不匹配的查询的对象的键
|
|
|
* @param {Bmob.Query} query 需要运行的查询
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
doesNotMatchKeyInQuery: function (key, queryKey, query) {
|
|
|
var queryJSON = query.toJSON();
|
|
|
queryJSON.className = query.className;
|
|
|
this._addCondition(key, "$dontSelect", {
|
|
|
key: queryKey,
|
|
|
query: queryJSON
|
|
|
});
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Add constraint that at least one of the passed in queries matches.
|
|
|
* @param {Array} queries
|
|
|
* @return {Bmob.Query} Returns the query, so you can chain this call.
|
|
|
*/
|
|
|
_orQuery: function (queries) {
|
|
|
var queryJSON = _.map(queries,
|
|
|
function (q) {
|
|
|
return q.toJSON().where;
|
|
|
});
|
|
|
|
|
|
this._where.$or = queryJSON;
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Converts a string into a regex that matches it.
|
|
|
* Surrounding with \Q .. \E does this, we just need to escape \E's in
|
|
|
* the text separately.
|
|
|
*/
|
|
|
_quote: function (s) {
|
|
|
return "\\Q" + s.replace("\\E", "\\E\\\\E\\Q") + "\\E";
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查找一个值中是否包含某个子串。在大量的数据中,这个操作可能很慢。
|
|
|
* @param {String} key 需要查找的值
|
|
|
* @param {String} substring 需要匹配子串
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
contains: function (key, value) {
|
|
|
this._addCondition(key, "$regex", this._quote(value));
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 检查某个值是否以特殊的字符串开头。 这查询使用了backend index,因此在大数据中也很快。
|
|
|
* @param {String} key 需要查找的值
|
|
|
* @param {String} prefix 需要匹配子串
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
startsWith: function (key, value) {
|
|
|
this._addCondition(key, "$regex", "^" + this._quote(value));
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 检查某个值是否以特殊的字符串结尾。在大量的数据中,这个操作可能很慢。
|
|
|
* @param {String} key 需要查找的值
|
|
|
* @param {String} suffix 需要匹配子串
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
endsWith: function (key, value) {
|
|
|
this._addCondition(key, "$regex", this._quote(value) + "$");
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 根据key对结果进行升序。
|
|
|
*
|
|
|
* @param {String} key 排序的key
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
ascending: function (key) {
|
|
|
|
|
|
if (Bmob._isNullOrUndefined(this._order)) {
|
|
|
this._order = key;
|
|
|
} else {
|
|
|
this._order = this._order + "," + key;
|
|
|
}
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
cleanOrder: function (key) {
|
|
|
this._order = null;
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 根据key对结果进行降序。
|
|
|
*
|
|
|
* @param {String} key 排序的key
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
descending: function (key) {
|
|
|
if (Bmob._isNullOrUndefined(this._order)) {
|
|
|
this._order = "-" + key;
|
|
|
} else {
|
|
|
this._order = this._order + ",-" + key;
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查找一个geo point 附近的坐标。
|
|
|
* @param {String} key Bmob.GeoPoint的key
|
|
|
* @param {Bmob.GeoPoint} point 指向一个 Bmob.GeoPoint
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
near: function (key, point) {
|
|
|
if (!(point instanceof Bmob.GeoPoint)) {
|
|
|
// Try to cast it to a GeoPoint, so that near("loc", [20,30]) works.
|
|
|
point = new Bmob.GeoPoint(point);
|
|
|
}
|
|
|
this._addCondition(key, "$nearSphere", point);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加用于查找附近的对象,并基于弧度给出最大距离内的点。
|
|
|
* @param {String} key Bmob.GeoPoint的key
|
|
|
* @param {Bmob.GeoPoint} point 指向一个 Bmob.GeoPoint
|
|
|
* @param maxDistance 返回的最大距离,基于弧度.
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
withinRadians: function (key, point, distance) {
|
|
|
this.near(key, point);
|
|
|
this._addCondition(key, "$maxDistance", distance);
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加用于查找附近的对象,并基于米给出最大距离内的点。
|
|
|
* @param {String} key Bmob.GeoPoint的key
|
|
|
* @param {Bmob.GeoPoint} point 指向一个 Bmob.GeoPoint
|
|
|
* @param maxDistance 返回的最大距离,基于弧度.
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
withinMiles: function (key, point, distance) {
|
|
|
return this.withinRadians(key, point, distance / 3958.8);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 添加用于查找附近的对象,并基于千米给出最大距离内的点。
|
|
|
* @param {String} key Bmob.GeoPoint的key
|
|
|
* @param {Bmob.GeoPoint} point 指向一个 Bmob.GeoPoint
|
|
|
* @param maxDistance 返回的最大距离,基于弧度.
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
withinKilometers: function (key, point, distance) {
|
|
|
return this.withinRadians(key, point, distance / 6371.0);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 在一个四边形范围内,查找某个点附近的对象
|
|
|
* @param {String} key The key to be constrained.
|
|
|
* @param {Bmob.GeoPoint} southwest 这个四边形的南西方向
|
|
|
* @param {Bmob.GeoPoint} northeast 这个四边形的东北方向
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
withinGeoBox: function (key, southwest, northeast) {
|
|
|
if (!(southwest instanceof Bmob.GeoPoint)) {
|
|
|
southwest = new Bmob.GeoPoint(southwest);
|
|
|
}
|
|
|
if (!(northeast instanceof Bmob.GeoPoint)) {
|
|
|
northeast = new Bmob.GeoPoint(northeast);
|
|
|
}
|
|
|
this._addCondition(key, '$within', {
|
|
|
'$box': [southwest, northeast]
|
|
|
});
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 当获取的Bmob.Objects有指向其子对象的Pointer类型指针Key时,你可以加入inclue选项来获取指针指向的子对象
|
|
|
* @param {String} key 需要包含的key的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
include: function () {
|
|
|
var self = this;
|
|
|
Bmob._arrayEach(arguments,
|
|
|
function (key) {
|
|
|
if (_.isArray(key)) {
|
|
|
self._include = self._include.concat(key);
|
|
|
} else {
|
|
|
self._include.push(key);
|
|
|
}
|
|
|
});
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 匹配另一个查询的返回值,如果这个函数被调用多次,每次调用时所有的keys将会被包含。
|
|
|
* @param {Array} keys 需要包含的key的值
|
|
|
* @return {Bmob.Query} 返回查询对象,因此可以使用链式调用。
|
|
|
*/
|
|
|
select: function () {
|
|
|
var self = this;
|
|
|
this._select = this._select || [];
|
|
|
Bmob._arrayEach(arguments,
|
|
|
function (key) {
|
|
|
if (_.isArray(key)) {
|
|
|
self._select = self._select.concat(key);
|
|
|
} else {
|
|
|
self._select.push(key);
|
|
|
}
|
|
|
});
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 对查询的每个结果调用回调函数。
|
|
|
* 如果callback返回promise,这个迭代器不会继续直到所有的promise调用完毕。
|
|
|
* 如果回调返回拒绝promise,迭代会停止。
|
|
|
* 所有对象将以不排序的形式处理。
|
|
|
* 查询将不会有任何的排序,同时limit 或skip 将无效。
|
|
|
* @param callback {Function} 每个结果调用的回调函数
|
|
|
* @param options {Object} 可选的 Backbone-like 带成功或失败的回调,回调将会执行当迭代结束的时候。
|
|
|
* @return {Bmob.Promise} 当迭代结束的时候A promise 将会执行一次。
|
|
|
*/
|
|
|
each: function (callback, options) {
|
|
|
options = options || {};
|
|
|
|
|
|
if (this._order || this._skip || (this._limit >= 0)) {
|
|
|
var error = "Cannot iterate on a query with sort, skip, or limit.";
|
|
|
return Bmob.Promise.error(error)._thenRunCallbacks(options);
|
|
|
}
|
|
|
|
|
|
var promise = new Bmob.Promise();
|
|
|
|
|
|
var query = new Bmob.Query(this.objectClass);
|
|
|
// We can override the batch size from the options.
|
|
|
// This is undocumented, but useful for testing.
|
|
|
query._limit = options.batchSize || 100;
|
|
|
query._where = _.clone(this._where);
|
|
|
query._include = _.clone(this._include);
|
|
|
|
|
|
query.ascending('objectId');
|
|
|
|
|
|
var finished = false;
|
|
|
return Bmob.Promise._continueWhile(function () {
|
|
|
return !finished;
|
|
|
|
|
|
},
|
|
|
function () {
|
|
|
return query.find().then(function (results) {
|
|
|
var callbacksDone = Bmob.Promise.as();
|
|
|
Bmob._.each(results,
|
|
|
function (result) {
|
|
|
callbacksDone = callbacksDone.then(function () {
|
|
|
return callback(result);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
return callbacksDone.then(function () {
|
|
|
if (results.length >= query._limit) {
|
|
|
query.greaterThan("objectId", results[results.length - 1].id);
|
|
|
} else {
|
|
|
finished = true;
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
})._thenRunCallbacks(options);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
Bmob.FriendShipQuery = Bmob.Query._extend({
|
|
|
_objectClass: Bmob.User,
|
|
|
_newObject: function () {
|
|
|
return new Bmob.User();
|
|
|
},
|
|
|
_processResult: function (json) {
|
|
|
var user = json[this._friendshipTag];
|
|
|
if (user.__type === 'Pointer' && user.className === '_User') {
|
|
|
delete user.__type;
|
|
|
delete user.className;
|
|
|
}
|
|
|
return user;
|
|
|
},
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* History serves as a global router (per frame) to handle hashchange
|
|
|
* events or pushState, match the appropriate route, and trigger
|
|
|
* callbacks. You shouldn't ever have to create one of these yourself
|
|
|
* — you should use the reference to <code>Bmob.history</code>
|
|
|
* that will be created for you automatically if you make use of
|
|
|
* Routers with routes.
|
|
|
*
|
|
|
* <p>A fork of Backbone.History, provided for your convenience. If you
|
|
|
* use this class, you must also include jQuery, or another library
|
|
|
* that provides a jQuery-compatible $ function. For more information,
|
|
|
* see the <a href="http://documentcloud.github.com/backbone/#History">
|
|
|
* Backbone documentation</a>.</p>
|
|
|
* <p><strong><em>Available in the client SDK only.</em></strong></p>
|
|
|
*/
|
|
|
Bmob.History = function () {
|
|
|
this.handlers = [];
|
|
|
_.bindAll(this, 'checkUrl');
|
|
|
};
|
|
|
|
|
|
// Cached regex for cleaning leading hashes and slashes .
|
|
|
var routeStripper = /^[#\/]/;
|
|
|
|
|
|
// Cached regex for detecting MSIE.
|
|
|
var isExplorer = /msie [\w.]+/;
|
|
|
|
|
|
// Has the history handling already been started?
|
|
|
Bmob.History.started = false;
|
|
|
|
|
|
// Set up all inheritable **Bmob.History** properties and methods.
|
|
|
_.extend(Bmob.History.prototype, Bmob.Events,
|
|
|
/** @lends Bmob.History.prototype */
|
|
|
{
|
|
|
|
|
|
// The default interval to poll for hash changes, if necessary, is
|
|
|
// twenty times a second.
|
|
|
interval: 50,
|
|
|
|
|
|
// Gets the true hash value. Cannot use location.hash directly due to bug
|
|
|
// in Firefox where location.hash will always be decoded.
|
|
|
getHash: function (windowOverride) {
|
|
|
var loc = windowOverride ? windowOverride.location : window.location;
|
|
|
var match = loc.href.match(/#(.*)$/);
|
|
|
return match ? match[1] : '';
|
|
|
},
|
|
|
|
|
|
// Get the cross-browser normalized URL fragment, either from the URL,
|
|
|
// the hash, or the override.
|
|
|
getFragment: function (fragment, forcePushState) {
|
|
|
if (Bmob._isNullOrUndefined(fragment)) {
|
|
|
if (this._hasPushState || forcePushState) {
|
|
|
fragment = window.location.pathname;
|
|
|
var search = window.location.search;
|
|
|
if (search) {
|
|
|
fragment += search;
|
|
|
}
|
|
|
} else {
|
|
|
fragment = this.getHash();
|
|
|
}
|
|
|
}
|
|
|
if (!fragment.indexOf(this.options.root)) {
|
|
|
fragment = fragment.substr(this.options.root.length);
|
|
|
}
|
|
|
return fragment.replace(routeStripper, '');
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Start the hash change handling, returning `true` if the current
|
|
|
* URL matches an existing route, and `false` otherwise.
|
|
|
*/
|
|
|
start: function (options) {
|
|
|
if (Bmob.History.started) {
|
|
|
throw new Error("Bmob.history has already been started");
|
|
|
}
|
|
|
Bmob.History.started = true;
|
|
|
|
|
|
// Figure out the initial configuration. Do we need an iframe?
|
|
|
// Is pushState desired ... is it available?
|
|
|
this.options = _.extend({},
|
|
|
{
|
|
|
root: '/'
|
|
|
},
|
|
|
this.options, options);
|
|
|
this._wantsHashChange = this.options.hashChange !== false;
|
|
|
this._wantsPushState = !!this.options.pushState;
|
|
|
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
|
|
|
var fragment = this.getFragment();
|
|
|
var docMode = document.documentMode;
|
|
|
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
|
|
|
|
|
if (oldIE) {
|
|
|
this.iframe = Bmob.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
|
|
this.navigate(fragment);
|
|
|
}
|
|
|
|
|
|
// Depending on whether we're using pushState or hashes, and whether
|
|
|
// 'onhashchange' is supported, determine how we check the URL state.
|
|
|
if (this._hasPushState) {
|
|
|
Bmob.$(window).bind('popstate', this.checkUrl);
|
|
|
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
|
|
|
Bmob.$(window).bind('hashchange', this.checkUrl);
|
|
|
} else if (this._wantsHashChange) {
|
|
|
this._checkUrlInterval = window.setInterval(this.checkUrl, this.interval);
|
|
|
}
|
|
|
|
|
|
// Determine if we need to change the base url, for a pushState link
|
|
|
// opened by a non-pushState browser.
|
|
|
this.fragment = fragment;
|
|
|
var loc = window.location;
|
|
|
var atRoot = loc.pathname === this.options.root;
|
|
|
|
|
|
// If we've started off with a route from a `pushState`-enabled browser,
|
|
|
// but we're currently in a browser that doesn't support it...
|
|
|
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
|
|
|
this.fragment = this.getFragment(null, true);
|
|
|
window.location.replace(this.options.root + '#' + this.fragment);
|
|
|
// Return immediately as browser will do redirect to new url
|
|
|
return true;
|
|
|
|
|
|
// Or if we've started out with a hash-based route, but we're currently
|
|
|
// in a browser where it could be `pushState`-based instead...
|
|
|
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
|
|
this.fragment = this.getHash().replace(routeStripper, '');
|
|
|
window.history.replaceState({},
|
|
|
document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
|
|
|
}
|
|
|
|
|
|
if (!this.options.silent) {
|
|
|
return this.loadUrl();
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// Disable Bmob.history, perhaps temporarily. Not useful in a real app,
|
|
|
// but possibly useful for unit testing Routers.
|
|
|
stop: function () {
|
|
|
Bmob.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
|
|
|
window.clearInterval(this._checkUrlInterval);
|
|
|
Bmob.History.started = false;
|
|
|
},
|
|
|
|
|
|
// Add a route to be tested when the fragment changes. Routes added later
|
|
|
// may override previous routes.
|
|
|
route: function (route, callback) {
|
|
|
this.handlers.unshift({
|
|
|
route: route,
|
|
|
callback: callback
|
|
|
});
|
|
|
},
|
|
|
|
|
|
// Checks the current URL to see if it has changed, and if it has,
|
|
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
|
|
checkUrl: function (e) {
|
|
|
var current = this.getFragment();
|
|
|
if (current === this.fragment && this.iframe) {
|
|
|
current = this.getFragment(this.getHash(this.iframe));
|
|
|
}
|
|
|
if (current === this.fragment) {
|
|
|
return false;
|
|
|
}
|
|
|
if (this.iframe) {
|
|
|
this.navigate(current);
|
|
|
}
|
|
|
if (!this.loadUrl()) {
|
|
|
this.loadUrl(this.getHash());
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// Attempt to load the current URL fragment. If a route succeeds with a
|
|
|
// match, returns `true`. If no defined routes matches the fragment,
|
|
|
// returns `false`.
|
|
|
loadUrl: function (fragmentOverride) {
|
|
|
var fragment = this.fragment = this.getFragment(fragmentOverride);
|
|
|
var matched = _.any(this.handlers,
|
|
|
function (handler) {
|
|
|
if (handler.route.test(fragment)) {
|
|
|
handler.callback(fragment);
|
|
|
return true;
|
|
|
}
|
|
|
});
|
|
|
return matched;
|
|
|
},
|
|
|
|
|
|
// Save a fragment into the hash history, or replace the URL state if the
|
|
|
// 'replace' option is passed. You are responsible for properly URL-encoding
|
|
|
// the fragment in advance.
|
|
|
//
|
|
|
// The options object can contain `trigger: true` if you wish to have the
|
|
|
// route callback be fired (not usually desirable), or `replace: true`, if
|
|
|
// you wish to modify the current URL without adding an entry to the
|
|
|
// history.
|
|
|
navigate: function (fragment, options) {
|
|
|
if (!Bmob.History.started) {
|
|
|
return false;
|
|
|
}
|
|
|
if (!options || options === true) {
|
|
|
options = {
|
|
|
trigger: options
|
|
|
};
|
|
|
}
|
|
|
var frag = (fragment || '').replace(routeStripper, '');
|
|
|
if (this.fragment === frag) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// If pushState is available, we use it to set the fragment as a real URL.
|
|
|
if (this._hasPushState) {
|
|
|
if (frag.indexOf(this.options.root) !== 0) {
|
|
|
frag = this.options.root + frag;
|
|
|
}
|
|
|
this.fragment = frag;
|
|
|
var replaceOrPush = options.replace ? 'replaceState' : 'pushState';
|
|
|
window.history[replaceOrPush]({},
|
|
|
document.title, frag);
|
|
|
|
|
|
// If hash changes haven't been explicitly disabled, update the hash
|
|
|
// fragment to store history.
|
|
|
} else if (this._wantsHashChange) {
|
|
|
this.fragment = frag;
|
|
|
this._updateHash(window.location, frag, options.replace);
|
|
|
if (this.iframe && (frag !== this.getFragment(this.getHash(this.iframe)))) {
|
|
|
// Opening and closing the iframe tricks IE7 and earlier
|
|
|
// to push a history entry on hash-tag change.
|
|
|
// When replace is true, we don't want this.
|
|
|
if (!options.replace) {
|
|
|
this.iframe.document.open().close();
|
|
|
}
|
|
|
this._updateHash(this.iframe.location, frag, options.replace);
|
|
|
}
|
|
|
|
|
|
// If you've told us that you explicitly don't want fallback hashchange-
|
|
|
// based history, then `navigate` becomes a page refresh.
|
|
|
} else {
|
|
|
window.location.assign(this.options.root + fragment);
|
|
|
}
|
|
|
if (options.trigger) {
|
|
|
this.loadUrl(fragment);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// Update the hash location, either replacing the current entry, or adding
|
|
|
// a new one to the browser history.
|
|
|
_updateHash: function (location, fragment, replace) {
|
|
|
if (replace) {
|
|
|
var s = location.toString().replace(/(javascript:|#).*$/, '');
|
|
|
location.replace(s + '#' + fragment);
|
|
|
} else {
|
|
|
location.hash = fragment;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* Routers map faux-URLs to actions, and fire events when routes are
|
|
|
* matched. Creating a new one sets its `routes` hash, if not set statically.
|
|
|
*
|
|
|
* <p>A fork of Backbone.Router, provided for your convenience.
|
|
|
* For more information, see the
|
|
|
* <a href="http://documentcloud.github.com/backbone/#Router">Backbone
|
|
|
* documentation</a>.</p>
|
|
|
* <p><strong><em>Available in the client SDK only.</em></strong></p>
|
|
|
*/
|
|
|
Bmob.Router = function (options) {
|
|
|
options = options || {};
|
|
|
if (options.routes) {
|
|
|
this.routes = options.routes;
|
|
|
}
|
|
|
this._bindRoutes();
|
|
|
this.initialize.apply(this, arguments);
|
|
|
};
|
|
|
|
|
|
// Cached regular expressions for matching named param parts and splatted
|
|
|
// parts of route strings.
|
|
|
var namedParam = /:\w+/g;
|
|
|
var splatParam = /\*\w+/g;
|
|
|
var escapeRegExp = /[\-\[\]{}()+?.,\\\^\$\|#\s]/g;
|
|
|
|
|
|
// Set up all inheritable **Bmob.Router** properties and methods.
|
|
|
_.extend(Bmob.Router.prototype, Bmob.Events,
|
|
|
/** @lends Bmob.Router.prototype */
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* Initialize is an empty function by default. Override it with your own
|
|
|
* initialization logic.
|
|
|
*/
|
|
|
initialize: function () { },
|
|
|
|
|
|
/**
|
|
|
* Manually bind a single named route to a callback. For example:
|
|
|
*
|
|
|
* <pre>this.route('search/:query/p:num', 'search', function(query, num) {
|
|
|
* ...
|
|
|
* });</pre>
|
|
|
*/
|
|
|
route: function (route, name, callback) {
|
|
|
Bmob.history = Bmob.history || new Bmob.History();
|
|
|
if (!_.isRegExp(route)) {
|
|
|
route = this._routeToRegExp(route);
|
|
|
}
|
|
|
if (!callback) {
|
|
|
callback = this[name];
|
|
|
}
|
|
|
Bmob.history.route(route, _.bind(function (fragment) {
|
|
|
var args = this._extractParameters(route, fragment);
|
|
|
if (callback) {
|
|
|
callback.apply(this, args);
|
|
|
}
|
|
|
this.trigger.apply(this, ['route:' + name].concat(args));
|
|
|
Bmob.history.trigger('route', this, name, args);
|
|
|
},
|
|
|
this));
|
|
|
return this;
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* Whenever you reach a point in your application that you'd
|
|
|
* like to save as a URL, call navigate in order to update the
|
|
|
* URL. If you wish to also call the route function, set the
|
|
|
* trigger option to true. To update the URL without creating
|
|
|
* an entry in the browser's history, set the replace option
|
|
|
* to true.
|
|
|
*/
|
|
|
navigate: function (fragment, options) {
|
|
|
Bmob.history.navigate(fragment, options);
|
|
|
},
|
|
|
|
|
|
// Bind all defined routes to `Bmob.history`. We have to reverse the
|
|
|
// order of the routes here to support behavior where the most general
|
|
|
// routes can be defined at the bottom of the route map.
|
|
|
_bindRoutes: function () {
|
|
|
if (!this.routes) {
|
|
|
return;
|
|
|
}
|
|
|
var routes = [];
|
|
|
for (var route in this.routes) {
|
|
|
if (this.routes.hasOwnProperty(route)) {
|
|
|
routes.unshift([route, this.routes[route]]);
|
|
|
}
|
|
|
}
|
|
|
for (var i = 0,
|
|
|
l = routes.length; i < l; i++) {
|
|
|
this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// Convert a route string into a regular expression, suitable for matching
|
|
|
// against the current location hash.
|
|
|
_routeToRegExp: function (route) {
|
|
|
route = route.replace(escapeRegExp, '\\$&').replace(namedParam, '([^\/]+)').replace(splatParam, '(.*?)');
|
|
|
return new RegExp('^' + route + '$');
|
|
|
},
|
|
|
|
|
|
// Given a route, and a URL fragment that it matches, return the array of
|
|
|
// extracted parameters.
|
|
|
_extractParameters: function (route, fragment) {
|
|
|
return route.exec(fragment).slice(1);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
* @function
|
|
|
* @param {Object} instanceProps Instance properties for the router.
|
|
|
* @param {Object} classProps Class properies for the router.
|
|
|
* @return {Class} A new subclass of <code>Bmob.Router</code>.
|
|
|
*/
|
|
|
Bmob.Router.extend = Bmob._extend;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @namespace 生成二维码
|
|
|
*/
|
|
|
Bmob.generateCode = Bmob.generateCode || {};
|
|
|
Bmob.generateCode = function (data, options) {
|
|
|
var request = Bmob._request("wechatApp/qr/generatecode", null, null, 'POST', Bmob._encode(data, null, true));
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @namespace 发送模板消息
|
|
|
*/
|
|
|
Bmob.sendMessage = Bmob.sendMessage || {};
|
|
|
Bmob.sendMessage = function (data, options) {
|
|
|
|
|
|
var request = Bmob._request("wechatApp/SendWeAppMessage", null, null, 'POST', Bmob._encode(data, null, true));
|
|
|
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @namespace 处理短信的函数
|
|
|
*/
|
|
|
Bmob.Sms = Bmob.Sms || {};
|
|
|
|
|
|
_.extend(Bmob.Sms,
|
|
|
/** @lends Bmob.Sms */
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* 请求发送短信内容
|
|
|
* @param {Object} 相应的参数
|
|
|
* @param {Object} Backbone-style options 对象。 options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise}
|
|
|
*/
|
|
|
requestSms: function (data, options) {
|
|
|
var request = Bmob._request("requestSms", null, null, 'POST', Bmob._encode(data, null, true));
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 请求短信验证码
|
|
|
* @param {Object} 相应的参数
|
|
|
* @param {Object} Backbone-style options 对象。 options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise}
|
|
|
*/
|
|
|
requestSmsCode: function (data, options) {
|
|
|
var request = Bmob._request("requestSmsCode", null, null, 'POST', Bmob._encode(data, null, true));
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 验证短信验证码
|
|
|
* @param {Object} 相应的参数
|
|
|
* @param {Object} Backbone-style options 对象。 options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise}
|
|
|
*/
|
|
|
verifySmsCode: function (mob, verifyCode, options) {
|
|
|
var data = {
|
|
|
"mobilePhoneNumber": mob
|
|
|
};
|
|
|
var request = Bmob._request("verifySmsCode/" + verifyCode, null, null, 'POST', Bmob._encode(data, null, true));
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查询短信状态
|
|
|
* @param {Object} 相应的参数
|
|
|
* @param {Object} Backbone-style options 对象。 options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise}
|
|
|
*/
|
|
|
querySms: function (smsId, options) {
|
|
|
var request = Bmob._request("querySms/" + smsId, null, null, 'GET', null);
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @namespace 支付功能
|
|
|
* <a href="http://docs.bmob.cn/restful/developdoc/index.html?menukey=develop_doc&key=develop_restful#index_支付服务">cloud functions</a>.
|
|
|
*/
|
|
|
Bmob.Pay = Bmob.Pay || {};
|
|
|
|
|
|
_.extend(Bmob.Pay, /** @lends Bmob.Cloud */
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
* 网页端调起小程序支付接口
|
|
|
* @param {float} 价格
|
|
|
* @param {String} 商品名称
|
|
|
* @param {String} 描述
|
|
|
* @param {String} OPEN ID
|
|
|
* @param {Object} options -style options 对象。
|
|
|
* options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise} A promise 将会处理云端代码调用的情况。
|
|
|
*/
|
|
|
wechatPay: function (price, product_name, body, openid, options) {
|
|
|
var data = { "order_price": price, "product_name": product_name, "body": body, "open_id": openid, "pay_type": 4 }
|
|
|
var request = Bmob._request("pay", null, null, 'POST',
|
|
|
Bmob._encode(data, null, true));
|
|
|
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
* 查询订单
|
|
|
* @param {String} 订单id
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise} A promise 将会处理云端代码调用的情况。
|
|
|
*/
|
|
|
queryOrder: function (orderId, options) {
|
|
|
|
|
|
var request = Bmob._request("pay/" + orderId, null, null, 'GET',
|
|
|
null);
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp);
|
|
|
})._thenRunCallbacks(options);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
* @namespace 运行云端代码
|
|
|
* <a href="cloudcode/developdoc/index.html?menukey=develop_doc&key=develop_cloudcode">cloud functions</a>.
|
|
|
*/
|
|
|
Bmob.Cloud = Bmob.Cloud || {};
|
|
|
|
|
|
_.extend(Bmob.Cloud,
|
|
|
/** @lends Bmob.Cloud */
|
|
|
{
|
|
|
/**
|
|
|
* 运行云端代码
|
|
|
* @param {String} 云端代码的函数名
|
|
|
* @param {Object} 传人云端代码的参数
|
|
|
* @param {Object} options Backbone-style options 对象。
|
|
|
* options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
* @return {Bmob.Promise} A promise 将会处理云端代码调用的情况。
|
|
|
*/
|
|
|
run: function (name, data, options) {
|
|
|
var request = Bmob._request("functions", name, null, 'POST', Bmob._encode(data, null, true));
|
|
|
|
|
|
return request.then(function (resp) {
|
|
|
return Bmob._decode(null, resp).result;
|
|
|
})._thenRunCallbacks(options);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
Bmob.Installation = Bmob.Object.extend("_Installation");
|
|
|
|
|
|
/**
|
|
|
* 包含push的函数
|
|
|
* @name Bmob.Push
|
|
|
* @namespace 推送消息
|
|
|
*/
|
|
|
Bmob.Push = Bmob.Push || {};
|
|
|
|
|
|
/**
|
|
|
* 推送消息
|
|
|
* @param {Object} data - 具体的参数请查看<a href="http://docs.bmob.cn/restful/developdoc/index.html?menukey=develop_doc&key=develop_restful">推送文档</a>.
|
|
|
* @param {Object} options options 对象。 options.success, 如果设置了,将会处理云端代码调用成功的情况。 options.error 如果设置了,将会处理云端代码调用失败的情况。 两个函数都是可选的。两个函数都只有一个参数。
|
|
|
*/
|
|
|
Bmob.Push.send = function (data, options) {
|
|
|
if (data.where) {
|
|
|
data.where = data.where.toJSON().where;
|
|
|
}
|
|
|
|
|
|
if (data.push_time) {
|
|
|
data.push_time = data.push_time.toJSON();
|
|
|
}
|
|
|
|
|
|
if (data.expiration_time) {
|
|
|
data.expiration_time = data.expiration_time.toJSON();
|
|
|
}
|
|
|
|
|
|
if (data.expiration_time && data.expiration_time_interval) {
|
|
|
throw "Both expiration_time and expiration_time_interval can't be set";
|
|
|
}
|
|
|
|
|
|
var request = Bmob._request('push', null, null, 'POST', data);
|
|
|
return request._thenRunCallbacks(options);
|
|
|
};
|
|
|
}.call(this)); |