├── .editorconfig ├── .gitignore ├── Gruntfile.js ├── README.md ├── dist └── parse-angular-sdk.js ├── lib ├── ACL.js ├── Cloud.js ├── Config.js ├── Core.js ├── Error.js ├── Events.js ├── FacebookUtils.js ├── GeoPoint.js ├── Object.js ├── Op.js ├── Query.js ├── Relation.js ├── Role.js ├── User.js └── _ParseAngular.js ├── original_sdk └── parse-1.3.4.js ├── package.json └── test ├── index.html └── test.js /.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trycom/parse-angular-sdk/f845a78795a214064071137539bb9263fd371200/.editorconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.loadNpmTasks('grunt-contrib-concat'); 4 | grunt.loadNpmTasks('grunt-contrib-uglify'); 5 | 6 | 7 | grunt.initConfig({ 8 | concat: { 9 | options: { 10 | separator: '\n' 11 | }, 12 | dist: { 13 | src: [ 14 | // 'lib/**.js' 15 | 'lib/Error.js', 16 | 'lib/Core.js', 17 | 'lib/Cloud.js', 18 | 'lib/Config.js', 19 | 'lib/Events.js', 20 | 'lib/Op.js', 21 | 'lib/Object.js', 22 | 'lib/Relation.js', 23 | 'lib/GeoPoint.js', 24 | 'lib/Query.js', 25 | 'lib/User.js', 26 | 'lib/Role.js', 27 | 'lib/ACL.js', 28 | 'lib/FacebookUtils.js', 29 | 'lib/_ParseAngular.js', 30 | 31 | ], 32 | dest: 'dist/parse-angular-sdk.js' 33 | } 34 | } 35 | }); 36 | 37 | 38 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse-angular-sdk 2 | 3 | This is an unofficial port of the Parse JavaScript SDK to a native Angular SDK. 4 | 5 | Disclaimer: we are not affiliated in any ways with Parse and we this is offered as is; we don't take responsibility for any bugs you might encouter in production. However you are encouraged to help us maintain this SDK, as the Parse SDK evolves. 6 | 7 | This version of SDK does **not** contain the following Parse. modules: 8 | 9 | - Promise (turned useless by $q) 10 | - View 11 | - Router 12 | - History 13 | - File 14 | 15 | It also drops support for callback hashes. All methods are promises only. You need to have underscore/lodash included in your app too. 16 | 17 | Feel free to include them in your app using the method we used; which is isolating every module inside an Angular component, and replacing http requests using $http, promises using $q. 18 | 19 | Usage 20 | 21 | ```javascript 22 | 23 | var module = angular.module('MyApp', ['Parse']); 24 | 25 | module.controller('MyCtrl', function($scope, ParseSDK){ 26 | 27 | var Parse = ParseSDK; 28 | 29 | var query = new Parse.Query('Monsters'); 30 | query.equalTo('name', 'Casper'); 31 | query.find() 32 | .then(function(results){ 33 | $scope.results = results; 34 | }) 35 | 36 | }); 37 | 38 | 39 | 40 | ``` -------------------------------------------------------------------------------- /lib/ACL.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('ParseAngular.ACL', [ 2 | 'ParseAngular.User', 3 | 'ParseAngular.Role', 4 | 'ParseAngular.Core' 5 | ]); 6 | 7 | module.factory('ParseACL', function(ParseUser, ParseRole, ParseCore){ 8 | 9 | var PUBLIC_KEY = "*", ParseACL; 10 | 11 | /** 12 | * Creates a new ACL. 13 | * If no argument is given, the ACL has no permissions for anyone. 14 | * If the argument is a ParseUser, the ACL will have read and write 15 | * permission for only that user. 16 | * If the argument is any other JSON object, that object will be interpretted 17 | * as a serialized ACL created with toJSON(). 18 | * @see ParseCoreObject#setACL 19 | * @class 20 | * 21 | *

An ACL, or Access Control List can be added to any 22 | * ParseCoreObject to restrict access to only a subset of users 23 | * of your application.

24 | */ 25 | ParseACL = function(arg1) { 26 | var self = this; 27 | self.permissionsById = {}; 28 | if (_.isObject(arg1)) { 29 | if (arg1 instanceof ParseUser) { 30 | self.setReadAccess(arg1, true); 31 | self.setWriteAccess(arg1, true); 32 | } 33 | else { 34 | if (_.isFunction(arg1)) { 35 | throw "ParseACL() called with a function. Did you forget ()?"; 36 | } 37 | ParseCore_objectEach(arg1, function(accessList, userId) { 38 | if (!_.isString(userId)) { 39 | throw "Tried to create an ACL with an invalid userId."; 40 | } 41 | self.permissionsById[userId] = {}; 42 | ParseCore_objectEach(accessList, function(allowed, permission) { 43 | if (permission !== "read" && permission !== "write") { 44 | throw "Tried to create an ACL with an invalid permission type."; 45 | } 46 | if (!_.isBoolean(allowed)) { 47 | throw "Tried to create an ACL with an invalid permission value."; 48 | } 49 | self.permissionsById[userId][permission] = allowed; 50 | }); 51 | }); 52 | } 53 | } 54 | }; 55 | 56 | /** 57 | * Returns a JSON-encoded version of the ACL. 58 | * @return {Object} 59 | */ 60 | ParseACL.prototype.toJSON = function() { 61 | return _.clone(this.permissionsById); 62 | }; 63 | 64 | ParseACL.prototype._setAccess = function(accessType, userId, allowed) { 65 | if (userId instanceof ParseUser) { 66 | userId = userId.id; 67 | } else if (userId instanceof ParseRole) { 68 | userId = "role:" + userId.getName(); 69 | } 70 | if (!_.isString(userId)) { 71 | throw "userId must be a string."; 72 | } 73 | if (!_.isBoolean(allowed)) { 74 | throw "allowed must be either true or false."; 75 | } 76 | var permissions = this.permissionsById[userId]; 77 | if (!permissions) { 78 | if (!allowed) { 79 | // The user already doesn't have this permission, so no action needed. 80 | return; 81 | } else { 82 | permissions = {}; 83 | this.permissionsById[userId] = permissions; 84 | } 85 | } 86 | 87 | if (allowed) { 88 | this.permissionsById[userId][accessType] = true; 89 | } else { 90 | delete permissions[accessType]; 91 | if (_.isEmpty(permissions)) { 92 | delete permissions[userId]; 93 | } 94 | } 95 | }; 96 | 97 | ParseACL.prototype._getAccess = function(accessType, userId) { 98 | if (userId instanceof ParseUser) { 99 | userId = userId.id; 100 | } else if (userId instanceof ParseRole) { 101 | userId = "role:" + userId.getName(); 102 | } 103 | var permissions = this.permissionsById[userId]; 104 | if (!permissions) { 105 | return false; 106 | } 107 | return permissions[accessType] ? true : false; 108 | }; 109 | 110 | /** 111 | * Set whether the given user is allowed to read this object. 112 | * @param userId An instance of ParseUser or its objectId. 113 | * @param {Boolean} allowed Whether that user should have read access. 114 | */ 115 | ParseACL.prototype.setReadAccess = function(userId, allowed) { 116 | this._setAccess("read", userId, allowed); 117 | }; 118 | 119 | /** 120 | * Get whether the given user id is *explicitly* allowed to read this object. 121 | * Even if this returns false, the user may still be able to access it if 122 | * getPublicReadAccess returns true or a role that the user belongs to has 123 | * write access. 124 | * @param userId An instance of ParseUser or its objectId, or a ParseRole. 125 | * @return {Boolean} 126 | */ 127 | ParseACL.prototype.getReadAccess = function(userId) { 128 | return this._getAccess("read", userId); 129 | }; 130 | 131 | /** 132 | * Set whether the given user id is allowed to write this object. 133 | * @param userId An instance of ParseUser or its objectId, or a ParseRole.. 134 | * @param {Boolean} allowed Whether that user should have write access. 135 | */ 136 | ParseACL.prototype.setWriteAccess = function(userId, allowed) { 137 | this._setAccess("write", userId, allowed); 138 | }; 139 | 140 | /** 141 | * Get whether the given user id is *explicitly* allowed to write this object. 142 | * Even if this returns false, the user may still be able to write it if 143 | * getPublicWriteAccess returns true or a role that the user belongs to has 144 | * write access. 145 | * @param userId An instance of ParseUser or its objectId, or a ParseRole. 146 | * @return {Boolean} 147 | */ 148 | ParseACL.prototype.getWriteAccess = function(userId) { 149 | return this._getAccess("write", userId); 150 | }; 151 | 152 | /** 153 | * Set whether the public is allowed to read this object. 154 | * @param {Boolean} allowed 155 | */ 156 | ParseACL.prototype.setPublicReadAccess = function(allowed) { 157 | this.setReadAccess(PUBLIC_KEY, allowed); 158 | }; 159 | 160 | /** 161 | * Get whether the public is allowed to read this object. 162 | * @return {Boolean} 163 | */ 164 | ParseACL.prototype.getPublicReadAccess = function() { 165 | return this.getReadAccess(PUBLIC_KEY); 166 | }; 167 | 168 | /** 169 | * Set whether the public is allowed to write this object. 170 | * @param {Boolean} allowed 171 | */ 172 | ParseACL.prototype.setPublicWriteAccess = function(allowed) { 173 | this.setWriteAccess(PUBLIC_KEY, allowed); 174 | }; 175 | 176 | /** 177 | * Get whether the public is allowed to write this object. 178 | * @return {Boolean} 179 | */ 180 | ParseACL.prototype.getPublicWriteAccess = function() { 181 | return this.getWriteAccess(PUBLIC_KEY); 182 | }; 183 | 184 | /** 185 | * Get whether users belonging to the given role are allowed 186 | * to read this object. Even if this returns false, the role may 187 | * still be able to write it if a parent role has read access. 188 | * 189 | * @param role The name of the role, or a ParseRole object. 190 | * @return {Boolean} true if the role has read access. false otherwise. 191 | * @throws {String} If role is neither a ParseRole nor a String. 192 | */ 193 | ParseACL.prototype.getRoleReadAccess = function(role) { 194 | if (role instanceof ParseRole) { 195 | // Normalize to the String name 196 | role = role.getName(); 197 | } 198 | if (_.isString(role)) { 199 | return this.getReadAccess("role:" + role); 200 | } 201 | throw "role must be a ParseRole or a String"; 202 | }; 203 | 204 | /** 205 | * Get whether users belonging to the given role are allowed 206 | * to write this object. Even if this returns false, the role may 207 | * still be able to write it if a parent role has write access. 208 | * 209 | * @param role The name of the role, or a ParseRole object. 210 | * @return {Boolean} true if the role has write access. false otherwise. 211 | * @throws {String} If role is neither a ParseRole nor a String. 212 | */ 213 | ParseACL.prototype.getRoleWriteAccess = function(role) { 214 | if (role instanceof ParseRole) { 215 | // Normalize to the String name 216 | role = role.getName(); 217 | } 218 | if (_.isString(role)) { 219 | return this.getWriteAccess("role:" + role); 220 | } 221 | throw "role must be a ParseRole or a String"; 222 | }; 223 | 224 | /** 225 | * Set whether users belonging to the given role are allowed 226 | * to read this object. 227 | * 228 | * @param role The name of the role, or a ParseRole object. 229 | * @param {Boolean} allowed Whether the given role can read this object. 230 | * @throws {String} If role is neither a ParseRole nor a String. 231 | */ 232 | ParseACL.prototype.setRoleReadAccess = function(role, allowed) { 233 | if (role instanceof ParseRole) { 234 | // Normalize to the String name 235 | role = role.getName(); 236 | } 237 | if (_.isString(role)) { 238 | this.setReadAccess("role:" + role, allowed); 239 | return; 240 | } 241 | throw "role must be a ParseRole or a String"; 242 | }; 243 | 244 | /** 245 | * Set whether users belonging to the given role are allowed 246 | * to write this object. 247 | * 248 | * @param role The name of the role, or a ParseRole object. 249 | * @param {Boolean} allowed Whether the given role can write this object. 250 | * @throws {String} If role is neither a ParseRole nor a String. 251 | */ 252 | ParseACL.prototype.setRoleWriteAccess = function(role, allowed) { 253 | if (role instanceof ParseRole) { 254 | // Normalize to the String name 255 | role = role.getName(); 256 | } 257 | if (_.isString(role)) { 258 | this.setWriteAccess("role:" + role, allowed); 259 | return; 260 | } 261 | throw "role must be a ParseRole or a String"; 262 | }; 263 | 264 | return ParseACL; 265 | 266 | }); 267 | -------------------------------------------------------------------------------- /lib/Cloud.js: -------------------------------------------------------------------------------- 1 | 2 | var module = angular.module('ParseAngular.Cloud', ['ParseAngular.Core']); 3 | module.factory('ParseCloud', function(ParseCore){ 4 | /** 5 | * @namespace Contains functions for calling and declaring 6 | * cloud functions. 7 | *

8 | * Some functions are only available from Cloud Code. 9 | *

10 | */ 11 | var ParseCloud = {}; 12 | 13 | _.extend(ParseCloud, /** @lends Parse.Cloud */ { 14 | /** 15 | * Makes a call to a cloud function. 16 | * @param {String} name The function name. 17 | * @param {Object} data The parameters to send to the cloud function. 18 | * @param {Object} options A Backbone-style options object 19 | * options.success, if set, should be a function to handle a successful 20 | * call to a cloud function. options.error should be a function that 21 | * handles an error running the cloud function. Both functions are 22 | * optional. Both functions take a single argument. 23 | * @return {Parse.Promise} A promise that will be resolved with the result 24 | * of the function. 25 | */ 26 | run: function(name, data, options) { 27 | options = options || {}; 28 | 29 | var request = ParseCore._request({ 30 | route: "functions", 31 | className: name, 32 | method: 'POST', 33 | useMasterKey: options.useMasterKey, 34 | data: ParseCore._encode(data, null, true) 35 | }); 36 | 37 | return request.then(function(resp) { 38 | return ParseCore._decode(null, resp).result; 39 | }) 40 | // ._thenRunCallbacks(options); 41 | } 42 | }); 43 | 44 | return ParseCloud; 45 | }); -------------------------------------------------------------------------------- /lib/Config.js: -------------------------------------------------------------------------------- 1 | var module = angular.module("ParseAngular.Config", [ 2 | 'ParseAngular.Core', 3 | 'ParseAngular.Error' 4 | ]); 5 | 6 | module.factory('ParseConfig', function(ParseCore, ParseError, $q){ 7 | /** 8 | * @class ParseConfig is a local representation of configuration data that 9 | * can be set from the Parse dashboard. 10 | */ 11 | ParseConfig = function() { 12 | this.attributes = {}; 13 | this._escapedAttributes = {}; 14 | }; 15 | 16 | /** 17 | * Retrieves the most recently-fetched configuration object, either from 18 | * memory or from local storage if necessary. 19 | * 20 | * @return {ParseConfig} The most recently-fetched ParseConfig if it 21 | * exists, else an empty ParseConfig. 22 | */ 23 | ParseConfig.current = function() { 24 | if (ParseConfig._currentConfig) { 25 | return ParseConfig._currentConfig; 26 | } 27 | 28 | var configData = ParseCore.localStorage.getItem( 29 | ParseCore._getParsePath(ParseConfig._CURRENT_CONFIG_KEY) 30 | ); 31 | 32 | var config = new ParseConfig(); 33 | if (configData) { 34 | config._finishFetch(JSON.parse(configData)); 35 | ParseConfig._currentConfig = config; 36 | } 37 | return config; 38 | }; 39 | 40 | /** 41 | * Gets a new configuration object from the server. 42 | * 43 | * @return {ParseCore.Promise} A promise that is resolved with a newly-created 44 | * configuration object when the get completes. 45 | */ 46 | ParseConfig.get = function() { 47 | 48 | var request = ParseCore._request({ 49 | route: "config", 50 | method: "GET", 51 | }); 52 | 53 | return request.then(function(response) { 54 | if (!response || !response.params) { 55 | var errorObject = new ParseError( 56 | ParseCore.Error.INVALID_JSON, 57 | "Config JSON response invalid." 58 | ); 59 | return $q.reject(errorObject); 60 | } 61 | 62 | var config = new ParseConfig(); 63 | config._finishFetch(response); 64 | ParseConfig._currentConfig = config; 65 | return config; 66 | }) 67 | // ._thenRunCallbacks(options); 68 | }; 69 | 70 | ParseConfig.prototype = { 71 | 72 | /** 73 | * Gets the HTML-escaped value of an attribute. 74 | */ 75 | escape: function(attr) { 76 | var html = this._escapedAttributes[attr]; 77 | if (html) { 78 | return html; 79 | } 80 | var val = this.attributes[attr]; 81 | var escaped; 82 | if (ParseCore._isNullOrUndefined(val)) { 83 | escaped = ''; 84 | } else { 85 | escaped = _.escape(val.toString()); 86 | } 87 | this._escapedAttributes[attr] = escaped; 88 | return escaped; 89 | }, 90 | 91 | /** 92 | * Gets the value of an attribute. 93 | * @param {String} attr The name of an attribute. 94 | */ 95 | get: function(attr) { 96 | return this.attributes[attr]; 97 | }, 98 | 99 | _finishFetch: function(serverData) { 100 | this.attributes = ParseCore._decode(null, _.clone(serverData.params)); 101 | ParseCore.localStorage.setItem( 102 | ParseCore._getParsePath(ParseConfig._CURRENT_CONFIG_KEY), 103 | JSON.stringify(serverData) 104 | ); 105 | } 106 | }; 107 | 108 | ParseConfig._currentConfig = null; 109 | 110 | ParseConfig._CURRENT_CONFIG_KEY = "currentConfig"; 111 | 112 | return ParseConfig; 113 | 114 | }); -------------------------------------------------------------------------------- /lib/Core.js: -------------------------------------------------------------------------------- 1 | var module = angular.module("ParseAngular.Core", [ 2 | 'ParseAngular.Error' 3 | ]); 4 | 5 | module.factory('ParseCore', function($http, $q, ParseError, $injector){ 6 | 7 | var ParseCore = {}; 8 | /** 9 | * Contains all Parse API classes and functions. 10 | * @name Parse 11 | * @namespace 12 | * 13 | * Contains all Parse API classes and functions. 14 | */ 15 | 16 | 17 | // Load references to other dependencies 18 | if (typeof(localStorage) !== 'undefined') { 19 | ParseCore.localStorage = localStorage; 20 | } 21 | 22 | // if (typeof(XMLHttpRequest) !== 'undefined') { 23 | // ParseCore.XMLHttpRequest = XMLHttpRequest; 24 | // } else if (typeof(require) !== 'undefined') { 25 | // ParseCore.XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; 26 | // } 27 | // Import Parse's local copy of underscore. 28 | // if (typeof(exports) !== 'undefined' && exports._) { 29 | // // We're running in a CommonJS environment 30 | // _ = exports._.noConflict(); 31 | // exports.Parse = Parse; 32 | // } else { 33 | // _ = _.noConflict(); 34 | // } 35 | 36 | // If jQuery or Zepto has been included, grab a reference to it. 37 | if (typeof($) !== "undefined") { 38 | ParseCore.$ = $; 39 | } 40 | 41 | // Helpers 42 | // ------- 43 | 44 | // Shared empty constructor function to aid in prototype-chain creation. 45 | var EmptyConstructor = function() {}; 46 | 47 | 48 | // Helper function to correctly set up the prototype chain, for subclasses. 49 | // Similar to `goog.inherits`, but uses a hash of prototype properties and 50 | // class properties to be extended. 51 | var inherits = function(parent, protoProps, staticProps) { 52 | var child; 53 | 54 | // The constructor function for the new subclass is either defined by you 55 | // (the "constructor" property in your `extend` definition), or defaulted 56 | // by us to simply call the parent's constructor. 57 | if (protoProps && protoProps.hasOwnProperty('constructor')) { 58 | child = protoProps.constructor; 59 | } else { 60 | /** @ignore */ 61 | child = function(){ parent.apply(this, arguments); }; 62 | } 63 | 64 | // Inherit class (static) properties from parent. 65 | _.extend(child, parent); 66 | 67 | // Set the prototype chain to inherit from `parent`, without calling 68 | // `parent`'s constructor function. 69 | EmptyConstructor.prototype = parent.prototype; 70 | child.prototype = new EmptyConstructor(); 71 | 72 | // Add prototype properties (instance properties) to the subclass, 73 | // if supplied. 74 | if (protoProps) { 75 | _.extend(child.prototype, protoProps); 76 | } 77 | 78 | // Add static properties to the constructor function, if supplied. 79 | if (staticProps) { 80 | _.extend(child, staticProps); 81 | } 82 | 83 | // Correctly set child's `prototype.constructor`. 84 | child.prototype.constructor = child; 85 | 86 | // Set a convenience property in case the parent's prototype is 87 | // needed later. 88 | child.__super__ = parent.prototype; 89 | 90 | return child; 91 | }; 92 | 93 | // Set the server for Parse to talk to. 94 | ParseCore.serverURL = "https://api.parse.com"; 95 | 96 | /** 97 | * Call this method first to set up your authentication tokens for ParseCore. 98 | * You can get your keys from the Data Browser on ParseCore.com. 99 | * @param {String} applicationId Your Parse Application ID. 100 | * @param {String} javaScriptKey Your Parse JavaScript Key. 101 | * @param {String} masterKey (optional) Your Parse Master Key. (Node.js only!) 102 | */ 103 | ParseCore.initialize = function(applicationId, javaScriptKey, masterKey) { 104 | if (masterKey) { 105 | throw "ParseCore.initialize() was passed a Master Key, which is only " + 106 | "allowed from within Node.js."; 107 | } 108 | _initialize(applicationId, javaScriptKey); 109 | }; 110 | 111 | /** 112 | * Call this method first to set up master authentication tokens for ParseCore. 113 | * This method is for Parse's own private use. 114 | * @param {String} applicationId Your Parse Application ID. 115 | * @param {String} javaScriptKey Your Parse JavaScript Key. 116 | * @param {String} masterKey Your Parse Master Key. 117 | */ 118 | var _initialize = function(applicationId, javaScriptKey, masterKey) { 119 | ParseCore.applicationId = applicationId; 120 | ParseCore.javaScriptKey = javaScriptKey; 121 | ParseCore.masterKey = masterKey; 122 | _useMasterKey = false; 123 | }; 124 | 125 | 126 | /** 127 | * Returns prefix for localStorage keys used by this instance of ParseCore. 128 | * @param {String} path The relative suffix to append to it. 129 | * null or undefined is treated as the empty string. 130 | * @return {String} The full key name. 131 | */ 132 | ParseCore._getParsePath = _getParsePath = function(path) { 133 | if (!ParseCore.applicationId) { 134 | throw "You need to call ParseCore.initialize before using ParseCore."; 135 | } 136 | if (!path) { 137 | path = ""; 138 | } 139 | if (!_.isString(path)) { 140 | throw "Tried to get a localStorage path that wasn't a String."; 141 | } 142 | if (path[0] === "/") { 143 | path = path.substring(1); 144 | } 145 | return "Parse/" + ParseCore.applicationId + "/" + path; 146 | }; 147 | 148 | /** 149 | * Returns the unique string for this app on this machine. 150 | * Gets reset when localStorage is cleared. 151 | */ 152 | var _installationId = null; 153 | ParseCore._getInstallationId = function() { 154 | // See if it's cached in RAM. 155 | if (_installationId) { 156 | return _installationId; 157 | } 158 | 159 | // Try to get it from localStorage. 160 | var path = _getParsePath("installationId"); 161 | _installationId = ParseCore.localStorage.getItem(path); 162 | 163 | if (!_installationId || _installationId === "") { 164 | // It wasn't in localStorage, so create a new one. 165 | var hexOctet = function() { 166 | return Math.floor((1+Math.random())*0x10000).toString(16).substring(1); 167 | }; 168 | _installationId = ( 169 | hexOctet() + hexOctet() + "-" + 170 | hexOctet() + "-" + 171 | hexOctet() + "-" + 172 | hexOctet() + "-" + 173 | hexOctet() + hexOctet() + hexOctet()); 174 | ParseCore.localStorage.setItem(path, _installationId); 175 | } 176 | 177 | return _installationId; 178 | }; 179 | 180 | ParseCore._parseDate = function(iso8601) { 181 | var regexp = new RegExp( 182 | "^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})" + "T" + 183 | "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})" + 184 | "(.([0-9]+))?" + "Z$"); 185 | var match = regexp.exec(iso8601); 186 | if (!match) { 187 | return null; 188 | } 189 | 190 | var year = match[1] || 0; 191 | var month = (match[2] || 1) - 1; 192 | var day = match[3] || 0; 193 | var hour = match[4] || 0; 194 | var minute = match[5] || 0; 195 | var second = match[6] || 0; 196 | var milli = match[8] || 0; 197 | 198 | return new Date(Date.UTC(year, month, day, hour, minute, second, milli)); 199 | }; 200 | 201 | 202 | // $http takes care of the compatibility for us. 203 | // _ajaxIE8 = function(method, url, data) { 204 | // var promise = new ParseCore.Promise(); 205 | // var xdr = new XDomainRequest(); 206 | // xdr.onload = function() { 207 | // var response; 208 | // try { 209 | // response = JSON.parse(xdr.responseText); 210 | // } catch (e) { 211 | // promise.reject(e); 212 | // } 213 | // if (response) { 214 | // promise.resolve(response); 215 | // } 216 | // }; 217 | // xdr.onerror = xdr.ontimeout = function() { 218 | // // Let's fake a real error message. 219 | // var fakeResponse = { 220 | // responseText: JSON.stringify({ 221 | // code: ParseError.X_DOMAIN_REQUEST, 222 | // error: "IE's XDomainRequest does not supply error info." 223 | // }) 224 | // }; 225 | 226 | // promise.reject(fakeResponse); 227 | // }; 228 | // xdr.onprogress = function() {}; 229 | // xdr.open(method, url); 230 | // xdr.send(data); 231 | // return promise; 232 | // }; 233 | 234 | // _useXDomainRequest = function() { 235 | // if (typeof(XDomainRequest) !== "undefined") { 236 | // // We're in IE 8+. 237 | // if ('withCredentials' in new XMLHttpRequest()) { 238 | // // We're in IE 10+. 239 | // return false; 240 | // } 241 | // return true; 242 | // } 243 | // return false; 244 | // }; 245 | 246 | 247 | // Updated _ajax function for angular 248 | var _ajax; 249 | ParseCore._ajax = _ajax = function(method, url, data, success, error) { 250 | return $http({ 251 | method: method, 252 | url: url, 253 | data: data, 254 | headers: { 255 | "Content-Type": "text/plain" 256 | } 257 | }) 258 | .then(function(httpResult){ 259 | return httpResult.data; 260 | }, 261 | function(err){ 262 | return $q.reject(httpResult.data); 263 | }); 264 | }; 265 | 266 | // A self-propagating extend function. 267 | ParseCore._extend = function(protoProps, classProps) { 268 | var child = inherits(this, protoProps, classProps); 269 | child.extend = this.extend; 270 | return child; 271 | }; 272 | 273 | /** 274 | * Options: 275 | * route: is classes, users, login, etc. 276 | * objectId: null if there is no associated objectId. 277 | * method: the http method for the REST API. 278 | * dataObject: the payload as an object, or null if there is none. 279 | * useMasterKey: overrides whether to use the master key if set. 280 | * @ignore 281 | */ 282 | ParseCore._request = function(options) { 283 | var route = options.route; 284 | var className = options.className; 285 | var objectId = options.objectId; 286 | var method = options.method; 287 | var useMasterKey = options.useMasterKey; 288 | var sessionToken = options.sessionToken; 289 | var dataObject = options.data; 290 | 291 | if (!ParseCore.applicationId) { 292 | throw "You must specify your applicationId using ParseCore.initialize."; 293 | } 294 | 295 | if (!ParseCore.javaScriptKey && !ParseCore.masterKey) { 296 | throw "You must specify a key using ParseCore.initialize."; 297 | } 298 | 299 | 300 | if (!sessionToken) { 301 | 302 | var ParseUser = $injector.get('ParseUser'); 303 | // Use the current user session token if none was provided. 304 | var currentUser = ParseUser.current(); 305 | if (currentUser && currentUser._sessionToken) { 306 | sessionToken = currentUser._sessionToken; 307 | } 308 | } 309 | 310 | 311 | if (route !== "batch" && 312 | route !== "classes" && 313 | route !== "events" && 314 | route !== "files" && 315 | route !== "functions" && 316 | route !== "login" && 317 | route !== "push" && 318 | route !== "requestPasswordReset" && 319 | route !== "rest_verify_analytics" && 320 | route !== "users" && 321 | route !== "jobs" && 322 | route !== "config") { 323 | throw "Bad route: '" + route + "'."; 324 | } 325 | 326 | var url = ParseCore.serverURL; 327 | if (url.charAt(url.length - 1) !== "/") { 328 | url += "/"; 329 | } 330 | url += "1/" + route; 331 | if (className) { 332 | url += "/" + className; 333 | } 334 | if (objectId) { 335 | url += "/" + objectId; 336 | } 337 | 338 | dataObject = _.clone(dataObject || {}); 339 | if (method !== "POST") { 340 | dataObject._method = method; 341 | method = "POST"; 342 | } 343 | 344 | if (_.isUndefined(useMasterKey)) { 345 | useMasterKey = _useMasterKey; 346 | } 347 | 348 | dataObject._ApplicationId = ParseCore.applicationId; 349 | 350 | if (!useMasterKey) { 351 | dataObject._JavaScriptKey = ParseCore.javaScriptKey; 352 | } else { 353 | dataObject._MasterKey = ParseCore.masterKey; 354 | } 355 | 356 | dataObject._ClientVersion = ParseCore.VERSION; 357 | dataObject._InstallationId = ParseCore._getInstallationId(); 358 | if (sessionToken) { 359 | dataObject._SessionToken = sessionToken; 360 | } 361 | var data = JSON.stringify(dataObject); 362 | 363 | return _ajax(method, url, data).then(null, function(response) { 364 | // Transform the error into an instance of ParseError by trying to parse 365 | // the error string as JSON. 366 | var error; 367 | if (response && response.responseText) { 368 | try { 369 | var errorJSON = JSON.parse(response.responseText); 370 | error = new ParseError(errorJSON.code, errorJSON.error); 371 | } catch (e) { 372 | // If we fail to parse the error text, that's okay. 373 | error = new ParseError( 374 | ParseError.INVALID_JSON, 375 | "Received an error with invalid JSON from Parse: " + 376 | response.responseText 377 | ); 378 | } 379 | } else { 380 | error = new ParseError( 381 | ParseError.CONNECTION_FAILED, 382 | "XMLHttpRequest failed: " + JSON.stringify(response) 383 | ); 384 | } 385 | // By explicitly returning a rejected Promise, this will work with 386 | // either jQuery or Promises/A semantics. 387 | return $q.reject(error); 388 | }); 389 | }; 390 | 391 | // Helper function to get a value from a Backbone object as a property 392 | // or as a function. 393 | ParseCore._getValue = function(object, prop) { 394 | if (!(object && object[prop])) { 395 | return null; 396 | } 397 | return _.isFunction(object[prop]) ? object[prop]() : object[prop]; 398 | }; 399 | 400 | /** 401 | * Converts a value in a Parse Object into the appropriate representation. 402 | * This is the JS equivalent of Java's ParseCore.maybeReferenceAndEncode(Object) 403 | * if seenObjects is falsey. Otherwise any ParseObjects not in 404 | * seenObjects will be fully embedded rather than encoded 405 | * as a pointer. This array will be used to prevent going into an infinite 406 | * loop because we have circular references. If seenObjects 407 | * is set, then none of the Parse Objects that are serialized can be dirty. 408 | */ 409 | ParseCore._encode = _encode = function(value, seenObjects, disallowObjects) { 410 | 411 | var ParseObject = $injector.get('ParseObject'), 412 | ParseACL = $injector.get('ParseACL'), 413 | ParseGeoPoint = $injector.get('ParseGeoPoint'), 414 | ParseRelation = $injector.get('ParseRelation'), 415 | ParseOp = $injector.get('ParseOp'); 416 | 417 | 418 | if (value instanceof ParseObject) { 419 | if (disallowObjects) { 420 | throw "ParseObjects not allowed here"; 421 | } 422 | if (!seenObjects || _.include(seenObjects, value) || !value._hasData) { 423 | return value._toPointer(); 424 | } 425 | if (!value.dirty()) { 426 | seenObjects = seenObjects.concat(value); 427 | return _encode(value._toFullJSON(seenObjects), 428 | seenObjects, 429 | disallowObjects); 430 | } 431 | throw "Tried to save an object with a pointer to a new, unsaved object."; 432 | } 433 | if (value instanceof ParseACL) { 434 | return value.toJSON(); 435 | } 436 | if (_.isDate(value)) { 437 | return { "__type": "Date", "iso": value.toJSON() }; 438 | } 439 | if (value instanceof ParseGeoPoint) { 440 | return value.toJSON(); 441 | } 442 | if (_.isArray(value)) { 443 | return _.map(value, function(x) { 444 | return _encode(x, seenObjects, disallowObjects); 445 | }); 446 | } 447 | if (_.isRegExp(value)) { 448 | return value.source; 449 | } 450 | if (value instanceof ParseRelation) { 451 | return value.toJSON(); 452 | } 453 | if (value instanceof ParseOp) { 454 | return value.toJSON(); 455 | } 456 | // if (value instanceof ParseCore.File) { 457 | // if (!value.url()) { 458 | // throw "Tried to save an object containing an unsaved file."; 459 | // } 460 | 461 | // return { 462 | // __type: "File", 463 | // name: value.name(), 464 | // url: value.url() 465 | // }; 466 | // } 467 | if (_.isObject(value)) { 468 | var output = {}; 469 | ParseCore._objectEach(value, function(v, k) { 470 | output[k] = _encode(v, seenObjects, disallowObjects); 471 | }); 472 | return output; 473 | } 474 | 475 | return value; 476 | }; 477 | 478 | /** 479 | * The inverse function of _encode. 480 | * TODO: make decode not mutate value. 481 | */ 482 | ParseCore._decode = _decode = function(key, value) { 483 | 484 | var ParseObject = $injector.get('ParseObject'), 485 | ParseACL = $injector.get('ParseACL'), 486 | ParseGeoPoint = $injector.get('ParseGeoPoint'), 487 | ParseRelation = $injector.get('ParseRelation'), 488 | ParseOp = $injector.get('ParseOp'); 489 | 490 | if (!_.isObject(value)) { 491 | return value; 492 | } 493 | if (_.isArray(value)) { 494 | ParseCore._arrayEach(value, function(v, k) { 495 | value[k] = _decode(k, v); 496 | }); 497 | return value; 498 | } 499 | if (value instanceof ParseObject) { 500 | return value; 501 | } 502 | // if (value instanceof ParseCore.File) { 503 | // return value; 504 | // } 505 | if (value instanceof ParseOp) { 506 | return value; 507 | } 508 | if (value.__op) { 509 | return ParseOp._decode(value); 510 | } 511 | if (value.__type === "Pointer" && value.className) { 512 | var pointer = ParseObject._create(value.className); 513 | pointer._finishFetch({ objectId: value.objectId }, false); 514 | return pointer; 515 | } 516 | if (value.__type === "Object" && value.className) { 517 | // It's an Object included in a query result. 518 | var className = value.className; 519 | delete value.__type; 520 | delete value.className; 521 | var object = ParseObject._create(className); 522 | object._finishFetch(value, true); 523 | return object; 524 | } 525 | if (value.__type === "Date") { 526 | return ParseCore._parseDate(value.iso); 527 | } 528 | if (value.__type === "GeoPoint") { 529 | return new ParseGeoPoint({ 530 | latitude: value.latitude, 531 | longitude: value.longitude 532 | }); 533 | } 534 | if (key === "ACL") { 535 | if (value instanceof ParseACL) { 536 | return value; 537 | } 538 | return new ParseACL(value); 539 | } 540 | if (value.__type === "Relation") { 541 | var relation = new ParseRelation(null, key); 542 | relation.targetClassName = value.className; 543 | return relation; 544 | } 545 | 546 | // if (value.__type === "File") { 547 | // var file = new ParseCore.File(value.name); 548 | // file._url = value.url; 549 | // return file; 550 | // } 551 | 552 | ParseCore._objectEach(value, function(v, k) { 553 | value[k] = _decode(k, v); 554 | }); 555 | return value; 556 | }; 557 | 558 | ParseCore._arrayEach = _.each; 559 | 560 | /** 561 | * Does a deep traversal of every item in object, calling func on every one. 562 | * @param {Object} object The object or array to traverse deeply. 563 | * @param {Function} func The function to call for every item. It will 564 | * be passed the item as an argument. If it returns a truthy value, that 565 | * value will replace the item in its parent container. 566 | * @returns {} the result of calling func on the top-level object itself. 567 | */ 568 | ParseCore._traverse = _traverse = function(object, func, seen) { 569 | 570 | var ParseObject = $injector.get('ParseObject'), 571 | ParseACL = $injector.get('ParseACL'), 572 | ParseGeoPoint = $injector.get('ParseGeoPoint'), 573 | ParseRelation = $injector.get('ParseRelation'), 574 | ParseOp = $injector.get('ParseOp'); 575 | 576 | if (object instanceof ParseObject) { 577 | seen = seen || []; 578 | if (_.indexOf(seen, object) >= 0) { 579 | // We've already visited this object in this call. 580 | return; 581 | } 582 | seen.push(object); 583 | _traverse(object.attributes, func, seen); 584 | return func(object); 585 | } 586 | if (object instanceof ParseRelation) { 587 | // Nothing needs to be done, but we don't want to recurse into the 588 | // object's parent infinitely, so we catch this case. 589 | return func(object); 590 | } 591 | if (_.isArray(object)) { 592 | _.each(object, function(child, index) { 593 | var newChild = _traverse(child, func, seen); 594 | if (newChild) { 595 | object[index] = newChild; 596 | } 597 | }); 598 | return func(object); 599 | } 600 | if (_.isObject(object)) { 601 | _each(object, function(child, key) { 602 | var newChild = _traverse(child, func, seen); 603 | if (newChild) { 604 | object[key] = newChild; 605 | } 606 | }); 607 | return func(object); 608 | } 609 | return func(object); 610 | }; 611 | 612 | /** 613 | * This is like _.each, except: 614 | * * it doesn't work for so-called array-like objects, 615 | * * it does work for dictionaries with a "length" attribute. 616 | */ 617 | ParseCore._objectEach = _each = function(obj, callback) { 618 | if (_.isObject(obj)) { 619 | _.each(_.keys(obj), function(key) { 620 | callback(obj[key], key); 621 | }); 622 | } else { 623 | _.each(obj, callback); 624 | } 625 | }; 626 | 627 | // Helper function to check null or undefined. 628 | ParseCore._isNullOrUndefined = _isNullOrUndefined = function(x) { 629 | return _.isNull(x) || _.isUndefined(x); 630 | }; 631 | 632 | 633 | ParseCore._continueWhile = _continueWhile = function(predicate, asyncFunction) { 634 | if (predicate()) { 635 | return asyncFunction().then(function() { 636 | return _continueWhile(predicate, asyncFunction); 637 | }); 638 | } 639 | var defer = $q.defer(); 640 | defer.resolve(); 641 | return defer.promise; 642 | }; 643 | 644 | 645 | 646 | return ParseCore; 647 | 648 | }); -------------------------------------------------------------------------------- /lib/Error.js: -------------------------------------------------------------------------------- 1 | var module = angular.module("ParseAngular.Error", []); 2 | 3 | module.factory('ParseError', function(){ 4 | 5 | /** 6 | * Constructs a new Parse.Error object with the given code and message. 7 | * @param {Number} code An error code constant from Parse.Error. 8 | * @param {String} message A detailed description of the error. 9 | * @class 10 | * 11 | *

Class used for all objects passed to error callbacks.

12 | */ 13 | var ParseError = function(code, message) { 14 | this.code = code; 15 | this.message = message; 16 | }; 17 | 18 | _.extend(ParseError, /** @lends Parse.Error */ { 19 | /** 20 | * Error code indicating some error other than those enumerated here. 21 | * @constant 22 | */ 23 | OTHER_CAUSE: -1, 24 | 25 | /** 26 | * Error code indicating that something has gone wrong with the server. 27 | * If you get this error code, it is Parse's fault. Contact us at 28 | * https://parse.com/help 29 | * @constant 30 | */ 31 | INTERNAL_SERVER_ERROR: 1, 32 | 33 | /** 34 | * Error code indicating the connection to the Parse servers failed. 35 | * @constant 36 | */ 37 | CONNECTION_FAILED: 100, 38 | 39 | /** 40 | * Error code indicating the specified object doesn't exist. 41 | * @constant 42 | */ 43 | OBJECT_NOT_FOUND: 101, 44 | 45 | /** 46 | * Error code indicating you tried to query with a datatype that doesn't 47 | * support it, like exact matching an array or object. 48 | * @constant 49 | */ 50 | INVALID_QUERY: 102, 51 | 52 | /** 53 | * Error code indicating a missing or invalid classname. Classnames are 54 | * case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the 55 | * only valid characters. 56 | * @constant 57 | */ 58 | INVALID_CLASS_NAME: 103, 59 | 60 | /** 61 | * Error code indicating an unspecified object id. 62 | * @constant 63 | */ 64 | MISSING_OBJECT_ID: 104, 65 | 66 | /** 67 | * Error code indicating an invalid key name. Keys are case-sensitive. They 68 | * must start with a letter, and a-zA-Z0-9_ are the only valid characters. 69 | * @constant 70 | */ 71 | INVALID_KEY_NAME: 105, 72 | 73 | /** 74 | * Error code indicating a malformed pointer. You should not see this unless 75 | * you have been mucking about changing internal Parse code. 76 | * @constant 77 | */ 78 | INVALID_POINTER: 106, 79 | 80 | /** 81 | * Error code indicating that badly formed JSON was received upstream. This 82 | * either indicates you have done something unusual with modifying how 83 | * things encode to JSON, or the network is failing badly. 84 | * @constant 85 | */ 86 | INVALID_JSON: 107, 87 | 88 | /** 89 | * Error code indicating that the feature you tried to access is only 90 | * available internally for testing purposes. 91 | * @constant 92 | */ 93 | COMMAND_UNAVAILABLE: 108, 94 | 95 | /** 96 | * You must call Parse.initialize before using the Parse library. 97 | * @constant 98 | */ 99 | NOT_INITIALIZED: 109, 100 | 101 | /** 102 | * Error code indicating that a field was set to an inconsistent type. 103 | * @constant 104 | */ 105 | INCORRECT_TYPE: 111, 106 | 107 | /** 108 | * Error code indicating an invalid channel name. A channel name is either 109 | * an empty string (the broadcast channel) or contains only a-zA-Z0-9_ 110 | * characters and starts with a letter. 111 | * @constant 112 | */ 113 | INVALID_CHANNEL_NAME: 112, 114 | 115 | /** 116 | * Error code indicating that push is misconfigured. 117 | * @constant 118 | */ 119 | PUSH_MISCONFIGURED: 115, 120 | 121 | /** 122 | * Error code indicating that the object is too large. 123 | * @constant 124 | */ 125 | OBJECT_TOO_LARGE: 116, 126 | 127 | /** 128 | * Error code indicating that the operation isn't allowed for clients. 129 | * @constant 130 | */ 131 | OPERATION_FORBIDDEN: 119, 132 | 133 | /** 134 | * Error code indicating the result was not found in the cache. 135 | * @constant 136 | */ 137 | CACHE_MISS: 120, 138 | 139 | /** 140 | * Error code indicating that an invalid key was used in a nested 141 | * JSONObject. 142 | * @constant 143 | */ 144 | INVALID_NESTED_KEY: 121, 145 | 146 | /** 147 | * Error code indicating that an invalid filename was used for ParseFile. 148 | * A valid file name contains only a-zA-Z0-9_. characters and is between 1 149 | * and 128 characters. 150 | * @constant 151 | */ 152 | INVALID_FILE_NAME: 122, 153 | 154 | /** 155 | * Error code indicating an invalid ACL was provided. 156 | * @constant 157 | */ 158 | INVALID_ACL: 123, 159 | 160 | /** 161 | * Error code indicating that the request timed out on the server. Typically 162 | * this indicates that the request is too expensive to run. 163 | * @constant 164 | */ 165 | TIMEOUT: 124, 166 | 167 | /** 168 | * Error code indicating that the email address was invalid. 169 | * @constant 170 | */ 171 | INVALID_EMAIL_ADDRESS: 125, 172 | 173 | /** 174 | * Error code indicating a missing content type. 175 | * @constant 176 | */ 177 | MISSING_CONTENT_TYPE: 126, 178 | 179 | /** 180 | * Error code indicating a missing content length. 181 | * @constant 182 | */ 183 | MISSING_CONTENT_LENGTH: 127, 184 | 185 | /** 186 | * Error code indicating an invalid content length. 187 | * @constant 188 | */ 189 | INVALID_CONTENT_LENGTH: 128, 190 | 191 | /** 192 | * Error code indicating a file that was too large. 193 | * @constant 194 | */ 195 | FILE_TOO_LARGE: 129, 196 | 197 | /** 198 | * Error code indicating an error saving a file. 199 | * @constant 200 | */ 201 | FILE_SAVE_ERROR: 130, 202 | 203 | /** 204 | * Error code indicating that a unique field was given a value that is 205 | * already taken. 206 | * @constant 207 | */ 208 | DUPLICATE_VALUE: 137, 209 | 210 | /** 211 | * Error code indicating that a role's name is invalid. 212 | * @constant 213 | */ 214 | INVALID_ROLE_NAME: 139, 215 | 216 | /** 217 | * Error code indicating that an application quota was exceeded. Upgrade to 218 | * resolve. 219 | * @constant 220 | */ 221 | EXCEEDED_QUOTA: 140, 222 | 223 | /** 224 | * Error code indicating that a Cloud Code script failed. 225 | * @constant 226 | */ 227 | SCRIPT_FAILED: 141, 228 | 229 | /** 230 | * Error code indicating that a Cloud Code validation failed. 231 | * @constant 232 | */ 233 | VALIDATION_ERROR: 142, 234 | 235 | /** 236 | * Error code indicating that invalid image data was provided. 237 | * @constant 238 | */ 239 | INVALID_IMAGE_DATA: 150, 240 | 241 | /** 242 | * Error code indicating an unsaved file. 243 | * @constant 244 | */ 245 | UNSAVED_FILE_ERROR: 151, 246 | 247 | /** 248 | * Error code indicating an invalid push time. 249 | */ 250 | INVALID_PUSH_TIME_ERROR: 152, 251 | 252 | /** 253 | * Error code indicating an error deleting a file. 254 | * @constant 255 | */ 256 | FILE_DELETE_ERROR: 153, 257 | 258 | /** 259 | * Error code indicating that the application has exceeded its request 260 | * limit. 261 | * @constant 262 | */ 263 | REQUEST_LIMIT_EXCEEDED: 155, 264 | 265 | /** 266 | * Error code indicating an invalid event name. 267 | */ 268 | INVALID_EVENT_NAME: 160, 269 | 270 | /** 271 | * Error code indicating that the username is missing or empty. 272 | * @constant 273 | */ 274 | USERNAME_MISSING: 200, 275 | 276 | /** 277 | * Error code indicating that the password is missing or empty. 278 | * @constant 279 | */ 280 | PASSWORD_MISSING: 201, 281 | 282 | /** 283 | * Error code indicating that the username has already been taken. 284 | * @constant 285 | */ 286 | USERNAME_TAKEN: 202, 287 | 288 | /** 289 | * Error code indicating that the email has already been taken. 290 | * @constant 291 | */ 292 | EMAIL_TAKEN: 203, 293 | 294 | /** 295 | * Error code indicating that the email is missing, but must be specified. 296 | * @constant 297 | */ 298 | EMAIL_MISSING: 204, 299 | 300 | /** 301 | * Error code indicating that a user with the specified email was not found. 302 | * @constant 303 | */ 304 | EMAIL_NOT_FOUND: 205, 305 | 306 | /** 307 | * Error code indicating that a user object without a valid session could 308 | * not be altered. 309 | * @constant 310 | */ 311 | SESSION_MISSING: 206, 312 | 313 | /** 314 | * Error code indicating that a user can only be created through signup. 315 | * @constant 316 | */ 317 | MUST_CREATE_USER_THROUGH_SIGNUP: 207, 318 | 319 | /** 320 | * Error code indicating that an an account being linked is already linked 321 | * to another user. 322 | * @constant 323 | */ 324 | ACCOUNT_ALREADY_LINKED: 208, 325 | 326 | /** 327 | * Error code indicating that a user cannot be linked to an account because 328 | * that account's id could not be found. 329 | * @constant 330 | */ 331 | LINKED_ID_MISSING: 250, 332 | 333 | /** 334 | * Error code indicating that a user with a linked (e.g. Facebook) account 335 | * has an invalid session. 336 | * @constant 337 | */ 338 | INVALID_LINKED_SESSION: 251, 339 | 340 | /** 341 | * Error code indicating that a service being linked (e.g. Facebook or 342 | * Twitter) is unsupported. 343 | * @constant 344 | */ 345 | UNSUPPORTED_SERVICE: 252, 346 | 347 | /** 348 | * Error code indicating that there were multiple errors. Aggregate errors 349 | * have an "errors" property, which is an array of error objects with more 350 | * detail about each error that occurred. 351 | * @constant 352 | */ 353 | AGGREGATE_ERROR: 600, 354 | 355 | /** 356 | * Error code indicating the client was unable to read an input file. 357 | * @constant 358 | */ 359 | FILE_READ_ERROR: 601, 360 | 361 | /** 362 | * Error code indicating a real error code is unavailable because 363 | * we had to use an XDomainRequest object to allow CORS requests in 364 | * Internet Explorer, which strips the body from HTTP responses that have 365 | * a non-2XX status code. 366 | * @constant 367 | */ 368 | X_DOMAIN_REQUEST: 602 369 | }); 370 | 371 | return ParseError; 372 | }); -------------------------------------------------------------------------------- /lib/Events.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('ParseAngular.Events', []); 2 | 3 | module.factory('ParseEvents', function(){ 4 | var ParseEvents; 5 | 6 | var eventSplitter = /\s+/; 7 | var slice = Array.prototype.slice; 8 | 9 | /** 10 | * @class 11 | * 12 | *

ParseEvents is a fork of Backbone's Events module, provided for your 13 | * convenience.

14 | * 15 | *

A module that can be mixed in to any object in order to provide 16 | * it with custom events. You may bind callback functions to an event 17 | * with `on`, or remove these functions with `off`. 18 | * Triggering an event fires all callbacks in the order that `on` was 19 | * called. 20 | * 21 | *

 22 |     *     var object = {};
 23 |     *     _.extend(object, ParseEvents);
 24 |     *     object.on('expand', function(){ alert('expanded'); });
 25 |     *     object.trigger('expand');

26 | * 27 | *

For more information, see the 28 | * Backbone 29 | * documentation.

30 | */ 31 | ParseEvents = { 32 | /** 33 | * Bind one or more space separated events, `events`, to a `callback` 34 | * function. Passing `"all"` will bind the callback to all events fired. 35 | */ 36 | on: function(events, callback, context) { 37 | 38 | var calls, event, node, tail, list; 39 | if (!callback) { 40 | return this; 41 | } 42 | events = events.split(eventSplitter); 43 | calls = this._callbacks || (this._callbacks = {}); 44 | 45 | // Create an immutable callback list, allowing traversal during 46 | // modification. The tail is an empty object that will always be used 47 | // as the next node. 48 | event = events.shift(); 49 | while (event) { 50 | list = calls[event]; 51 | node = list ? list.tail : {}; 52 | node.next = tail = {}; 53 | node.context = context; 54 | node.callback = callback; 55 | calls[event] = {tail: tail, next: list ? list.next : node}; 56 | event = events.shift(); 57 | } 58 | 59 | return this; 60 | }, 61 | 62 | /** 63 | * Remove one or many callbacks. If `context` is null, removes all callbacks 64 | * with that function. If `callback` is null, removes all callbacks for the 65 | * event. If `events` is null, removes all bound callbacks for all events. 66 | */ 67 | off: function(events, callback, context) { 68 | var event, calls, node, tail, cb, ctx; 69 | 70 | // No events, or removing *all* events. 71 | if (!(calls = this._callbacks)) { 72 | return; 73 | } 74 | if (!(events || callback || context)) { 75 | delete this._callbacks; 76 | return this; 77 | } 78 | 79 | // Loop through the listed events and contexts, splicing them out of the 80 | // linked list of callbacks if appropriate. 81 | events = events ? events.split(eventSplitter) : _.keys(calls); 82 | event = events.shift(); 83 | while (event) { 84 | node = calls[event]; 85 | delete calls[event]; 86 | if (!node || !(callback || context)) { 87 | event = events.shift(); 88 | continue; 89 | } 90 | // Create a new list, omitting the indicated callbacks. 91 | tail = node.tail; 92 | node = node.next; 93 | while (node !== tail) { 94 | cb = node.callback; 95 | ctx = node.context; 96 | if ((callback && cb !== callback) || (context && ctx !== context)) { 97 | this.on(event, cb, ctx); 98 | } 99 | node = node.next; 100 | } 101 | event = events.shift(); 102 | } 103 | 104 | return this; 105 | }, 106 | 107 | /** 108 | * Trigger one or many events, firing all bound callbacks. Callbacks are 109 | * passed the same arguments as `trigger` is, apart from the event name 110 | * (unless you're listening on `"all"`, which will cause your callback to 111 | * receive the true name of the event as the first argument). 112 | */ 113 | trigger: function(events) { 114 | var event, node, calls, tail, args, all, rest; 115 | if (!(calls = this._callbacks)) { 116 | return this; 117 | } 118 | all = calls.all; 119 | events = events.split(eventSplitter); 120 | rest = slice.call(arguments, 1); 121 | 122 | // For each event, walk through the linked list of callbacks twice, 123 | // first to trigger the event, then to trigger any `"all"` callbacks. 124 | event = events.shift(); 125 | while (event) { 126 | node = calls[event]; 127 | if (node) { 128 | tail = node.tail; 129 | while ((node = node.next) !== tail) { 130 | node.callback.apply(node.context || this, rest); 131 | } 132 | } 133 | node = all; 134 | if (node) { 135 | tail = node.tail; 136 | args = [event].concat(rest); 137 | while ((node = node.next) !== tail) { 138 | node.callback.apply(node.context || this, args); 139 | } 140 | } 141 | event = events.shift(); 142 | } 143 | 144 | return this; 145 | } 146 | }; 147 | 148 | /** 149 | * @function 150 | */ 151 | ParseEvents.bind = ParseEvents.on; 152 | 153 | /** 154 | * @function 155 | */ 156 | ParseEvents.unbind = ParseEvents.off; 157 | 158 | return ParseEvents; 159 | 160 | }); -------------------------------------------------------------------------------- /lib/FacebookUtils.js: -------------------------------------------------------------------------------- 1 | var module = angular.module("ParseAngular.FacebookUtils", [ 2 | 'ParseAngular.User', 3 | 'ParseAngular.Core' 4 | ]); 5 | 6 | module.factory('ParseFacebookUtils', function(ParseUser, ParseCore){ 7 | 8 | var PUBLIC_KEY = "*", ParseFacebookUtils; 9 | 10 | var initialized = false; 11 | var requestedPermissions; 12 | var initOptions; 13 | var provider = { 14 | authenticate: function(options) { 15 | var self = this; 16 | FB.login(function(response) { 17 | if (response.authResponse) { 18 | if (options.success) { 19 | options.success(self, { 20 | id: response.authResponse.userID, 21 | access_token: response.authResponse.accessToken, 22 | expiration_date: new Date(response.authResponse.expiresIn * 1000 + 23 | (new Date()).getTime()).toJSON() 24 | }); 25 | } 26 | } else { 27 | if (options.error) { 28 | options.error(self, response); 29 | } 30 | } 31 | }, { 32 | scope: requestedPermissions 33 | }); 34 | }, 35 | restoreAuthentication: function(authData) { 36 | if (authData) { 37 | var authResponse = { 38 | userID: authData.id, 39 | accessToken: authData.access_token, 40 | expiresIn: (ParseCore._parseDate(authData.expiration_date).getTime() - 41 | (new Date()).getTime()) / 1000 42 | }; 43 | var newOptions = _.clone(initOptions); 44 | newOptions.authResponse = authResponse; 45 | 46 | // Suppress checks for login status from the browser. 47 | newOptions.status = false; 48 | 49 | // If the user doesn't match the one known by the FB SDK, log out. 50 | // Most of the time, the users will match -- it's only in cases where 51 | // the FB SDK knows of a different user than the one being restored 52 | // from a Parse User that logged in with username/password. 53 | var existingResponse = FB.getAuthResponse(); 54 | if (existingResponse && 55 | existingResponse.userID !== authResponse.userID) { 56 | FB.logout(); 57 | } 58 | 59 | FB.init(newOptions); 60 | } 61 | return true; 62 | }, 63 | getAuthType: function() { 64 | return "facebook"; 65 | }, 66 | deauthenticate: function() { 67 | this.restoreAuthentication(null); 68 | } 69 | }; 70 | 71 | /** 72 | * Provides a set of utilities for using Parse with Facebook. 73 | * @namespace 74 | * Provides a set of utilities for using Parse with Facebook. 75 | */ 76 | ParseFacebookUtils = { 77 | /** 78 | * Initializes Parse Facebook integration. Call this function after you 79 | * have loaded the Facebook Javascript SDK with the same parameters 80 | * as you would pass to 81 | * 83 | * FB.init(). ParseFacebookUtils will invoke FB.init() for you 84 | * with these arguments. 85 | * 86 | * @param {Object} options Facebook options argument as described here: 87 | * 89 | * FB.init(). The status flag will be coerced to 'false' because it 90 | * interferes with Parse Facebook integration. Call FB.getLoginStatus() 91 | * explicitly if this behavior is required by your application. 92 | */ 93 | init: function(options) { 94 | if (typeof(FB) === 'undefined') { 95 | throw "The Facebook JavaScript SDK must be loaded before calling init."; 96 | } 97 | initOptions = _.clone(options) || {}; 98 | if (initOptions.status && typeof(console) !== "undefined") { 99 | var warn = console.warn || console.log || function() {}; 100 | warn.call(console, "The 'status' flag passed into" + 101 | " FB.init, when set to true, can interfere with Parse Facebook" + 102 | " integration, so it has been suppressed. Please call" + 103 | " FB.getLoginStatus() explicitly if you require this behavior."); 104 | } 105 | initOptions.status = false; 106 | FB.init(initOptions); 107 | ParseUser._registerAuthenticationProvider(provider); 108 | initialized = true; 109 | }, 110 | 111 | /** 112 | * Gets whether the user has their account linked to Facebook. 113 | * 114 | * @param {Parse.User} user User to check for a facebook link. 115 | * The user must be logged in on this device. 116 | * @return {Boolean} true if the user has their account 117 | * linked to Facebook. 118 | */ 119 | isLinked: function(user) { 120 | return user._isLinked("facebook"); 121 | }, 122 | 123 | /** 124 | * Logs in a user using Facebook. This method delegates to the Facebook 125 | * SDK to authenticate the user, and then automatically logs in (or 126 | * creates, in the case where it is a new user) a Parse.User. 127 | * 128 | * @param {String, Object} permissions The permissions required for Facebook 129 | * log in. This is a comma-separated string of permissions. 130 | * Alternatively, supply a Facebook authData object as described in our 131 | * REST API docs if you want to handle getting facebook auth tokens 132 | * yourself. 133 | * @param {Object} options Standard options object with success and error 134 | * callbacks. 135 | */ 136 | logIn: function(permissions, options) { 137 | if (!permissions || _.isString(permissions)) { 138 | if (!initialized) { 139 | throw "You must initialize FacebookUtils before calling logIn."; 140 | } 141 | requestedPermissions = permissions; 142 | return ParseUser._logInWith("facebook", options); 143 | } else { 144 | var newOptions = _.clone(options) || {}; 145 | newOptions.authData = permissions; 146 | return ParseUser._logInWith("facebook", newOptions); 147 | } 148 | }, 149 | 150 | /** 151 | * Links Facebook to an existing PFUser. This method delegates to the 152 | * Facebook SDK to authenticate the user, and then automatically links 153 | * the account to the Parse.User. 154 | * 155 | * @param {Parse.User} user User to link to Facebook. This must be the 156 | * current user. 157 | * @param {String, Object} permissions The permissions required for Facebook 158 | * log in. This is a comma-separated string of permissions. 159 | * Alternatively, supply a Facebook authData object as described in our 160 | * REST API docs if you want to handle getting facebook auth tokens 161 | * yourself. 162 | * @param {Object} options Standard options object with success and error 163 | * callbacks. 164 | */ 165 | link: function(user, permissions, options) { 166 | if (!permissions || _.isString(permissions)) { 167 | if (!initialized) { 168 | throw "You must initialize FacebookUtils before calling link."; 169 | } 170 | requestedPermissions = permissions; 171 | return user._linkWith("facebook", options); 172 | } else { 173 | var newOptions = _.clone(options) || {}; 174 | newOptions.authData = permissions; 175 | return user._linkWith("facebook", newOptions); 176 | } 177 | }, 178 | 179 | /** 180 | * Unlinks the Parse.User from a Facebook account. 181 | * 182 | * @param {Parse.User} user User to unlink from Facebook. This must be the 183 | * current user. 184 | * @param {Object} options Standard options object with success and error 185 | * callbacks. 186 | */ 187 | unlink: function(user, options) { 188 | if (!initialized) { 189 | throw "You must initialize FacebookUtils before calling unlink."; 190 | } 191 | return user._unlinkFrom("facebook", options); 192 | } 193 | }; 194 | 195 | return ParseFacebookUtils; 196 | 197 | }); -------------------------------------------------------------------------------- /lib/GeoPoint.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('ParseAngular.GeoPoint', []); 2 | 3 | module.factory('ParseGeoPoint', function($q){ 4 | 5 | var ParseGeoPoint; 6 | 7 | /** 8 | * Creates a new GeoPoint with any of the following forms:
9 | *
 10 |     *   new GeoPoint(otherGeoPoint)
 11 |     *   new GeoPoint(30, 30)
 12 |     *   new GeoPoint([30, 30])
 13 |     *   new GeoPoint({latitude: 30, longitude: 30})
 14 |     *   new GeoPoint()  // defaults to (0, 0)
 15 |     *   
16 | * @class 17 | * 18 | *

Represents a latitude / longitude point that may be associated 19 | * with a key in a ParseObject or used as a reference point for geo queries. 20 | * This allows proximity-based queries on the key.

21 | * 22 | *

Only one key in a class may contain a GeoPoint.

23 | * 24 | *

Example:

 25 |     *   var point = new ParseGeoPoint(30.0, -20.0);
 26 |     *   var object = new Parse.Object("PlaceObject");
 27 |     *   object.set("location", point);
 28 |     *   object.save();

29 | */ 30 | ParseGeoPoint = function(arg1, arg2) { 31 | if (_.isArray(arg1)) { 32 | ParseGeoPoint._validate(arg1[0], arg1[1]); 33 | this.latitude = arg1[0]; 34 | this.longitude = arg1[1]; 35 | } else if (_.isObject(arg1)) { 36 | ParseGeoPoint._validate(arg1.latitude, arg1.longitude); 37 | this.latitude = arg1.latitude; 38 | this.longitude = arg1.longitude; 39 | } else if (_.isNumber(arg1) && _.isNumber(arg2)) { 40 | ParseGeoPoint._validate(arg1, arg2); 41 | this.latitude = arg1; 42 | this.longitude = arg2; 43 | } else { 44 | this.latitude = 0; 45 | this.longitude = 0; 46 | } 47 | 48 | // Add properties so that anyone using Webkit or Mozilla will get an error 49 | // if they try to set values that are out of bounds. 50 | var self = this; 51 | if (this.__defineGetter__ && this.__defineSetter__) { 52 | // Use _latitude and _longitude to actually store the values, and add 53 | // getters and setters for latitude and longitude. 54 | this._latitude = this.latitude; 55 | this._longitude = this.longitude; 56 | this.__defineGetter__("latitude", function() { 57 | return self._latitude; 58 | }); 59 | this.__defineGetter__("longitude", function() { 60 | return self._longitude; 61 | }); 62 | this.__defineSetter__("latitude", function(val) { 63 | ParseGeoPoint._validate(val, self.longitude); 64 | self._latitude = val; 65 | }); 66 | this.__defineSetter__("longitude", function(val) { 67 | ParseGeoPoint._validate(self.latitude, val); 68 | self._longitude = val; 69 | }); 70 | } 71 | }; 72 | 73 | /** 74 | * @lends ParseGeoPoint.prototype 75 | * @property {float} latitude North-south portion of the coordinate, in range 76 | * [-90, 90]. Throws an exception if set out of range in a modern browser. 77 | * @property {float} longitude East-west portion of the coordinate, in range 78 | * [-180, 180]. Throws if set out of range in a modern browser. 79 | */ 80 | 81 | /** 82 | * Throws an exception if the given lat-long is out of bounds. 83 | */ 84 | ParseGeoPoint._validate = function(latitude, longitude) { 85 | if (latitude < -90.0) { 86 | throw "ParseGeoPoint latitude " + latitude + " < -90.0."; 87 | } 88 | if (latitude > 90.0) { 89 | throw "ParseGeoPoint latitude " + latitude + " > 90.0."; 90 | } 91 | if (longitude < -180.0) { 92 | throw "ParseGeoPoint longitude " + longitude + " < -180.0."; 93 | } 94 | if (longitude > 180.0) { 95 | throw "ParseGeoPoint longitude " + longitude + " > 180.0."; 96 | } 97 | }; 98 | 99 | /** 100 | * Creates a GeoPoint with the user's current location, if available. 101 | * Calls options.success with a new GeoPoint instance or calls options.error. 102 | * @param {Object} options An object with success and error callbacks. 103 | */ 104 | ParseGeoPoint.current = function() { 105 | var defer = $q.defer(); 106 | navigator.geolocation.getCurrentPosition(function(location) { 107 | defer.resolve(new ParseGeoPoint({ 108 | latitude: location.coords.latitude, 109 | longitude: location.coords.longitude 110 | })); 111 | }, function(error) { 112 | defer.reject(error); 113 | }); 114 | 115 | return defer.promise; 116 | }; 117 | 118 | ParseGeoPoint.prototype = { 119 | /** 120 | * Returns a JSON representation of the GeoPoint, suitable for Parse. 121 | * @return {Object} 122 | */ 123 | toJSON: function() { 124 | ParseGeoPoint._validate(this.latitude, this.longitude); 125 | return { 126 | "__type": "GeoPoint", 127 | latitude: this.latitude, 128 | longitude: this.longitude 129 | }; 130 | }, 131 | 132 | /** 133 | * Returns the distance from this GeoPoint to another in radians. 134 | * @param {ParseGeoPoint} point the other ParseGeoPoint. 135 | * @return {Number} 136 | */ 137 | radiansTo: function(point) { 138 | var d2r = Math.PI / 180.0; 139 | var lat1rad = this.latitude * d2r; 140 | var long1rad = this.longitude * d2r; 141 | var lat2rad = point.latitude * d2r; 142 | var long2rad = point.longitude * d2r; 143 | var deltaLat = lat1rad - lat2rad; 144 | var deltaLong = long1rad - long2rad; 145 | var sinDeltaLatDiv2 = Math.sin(deltaLat / 2); 146 | var sinDeltaLongDiv2 = Math.sin(deltaLong / 2); 147 | // Square of half the straight line chord distance between both points. 148 | var a = ((sinDeltaLatDiv2 * sinDeltaLatDiv2) + 149 | (Math.cos(lat1rad) * Math.cos(lat2rad) * 150 | sinDeltaLongDiv2 * sinDeltaLongDiv2)); 151 | a = Math.min(1.0, a); 152 | return 2 * Math.asin(Math.sqrt(a)); 153 | }, 154 | 155 | /** 156 | * Returns the distance from this GeoPoint to another in kilometers. 157 | * @param {ParseGeoPoint} point the other ParseGeoPoint. 158 | * @return {Number} 159 | */ 160 | kilometersTo: function(point) { 161 | return this.radiansTo(point) * 6371.0; 162 | }, 163 | 164 | /** 165 | * Returns the distance from this GeoPoint to another in miles. 166 | * @param {ParseGeoPoint} point the other ParseGeoPoint. 167 | * @return {Number} 168 | */ 169 | milesTo: function(point) { 170 | return this.radiansTo(point) * 3958.8; 171 | } 172 | }; 173 | 174 | return ParseGeoPoint; 175 | 176 | }); -------------------------------------------------------------------------------- /lib/Op.js: -------------------------------------------------------------------------------- 1 | var module = angular.module("ParseAngular.Op", [ 2 | 'ParseAngular.Core' 3 | ]); 4 | 5 | module.factory('ParseOp', function(ParseCore, $injector){ 6 | 7 | var ParseOp; 8 | /** 9 | * @class 10 | * A ParseOp is an atomic operation that can be applied to a field in a 11 | * ParseObject. For example, calling object.set("foo", "bar") 12 | * is an example of a ParseOp.Set. Calling object.unset("foo") 13 | * is a ParseOp.Unset. These operations are stored in a ParseObject and 14 | * sent to the server as part of object.save() operations. 15 | * Instances of ParseOp should be immutable. 16 | * 17 | * You should not create subclasses of ParseOp or instantiate ParseOp 18 | * directly. 19 | */ 20 | 21 | var isInstanceOfParseObject = function(o) { 22 | var ParseObject = $injector.get('ParseObject'); 23 | return o instanceof ParseObject; 24 | }; 25 | 26 | 27 | ParseOp = function() { 28 | this._initialize.apply(this, arguments); 29 | }; 30 | 31 | ParseOp.prototype = { 32 | _initialize: function() {} 33 | }; 34 | 35 | _.extend(ParseOp, { 36 | /** 37 | * To create a new Op, call ParseOp._extend(); 38 | */ 39 | _extend: ParseCore._extend, 40 | 41 | // A map of __op string to decoder function. 42 | _opDecoderMap: {}, 43 | 44 | /** 45 | * Registers a function to convert a json object with an __op field into an 46 | * instance of a subclass of ParseOp. 47 | */ 48 | _registerDecoder: function(opName, decoder) { 49 | ParseOp._opDecoderMap[opName] = decoder; 50 | }, 51 | 52 | /** 53 | * Converts a json object into an instance of a subclass of ParseOp. 54 | */ 55 | _decode: function(json) { 56 | var decoder = ParseOp._opDecoderMap[json.__op]; 57 | if (decoder) { 58 | return decoder(json); 59 | } else { 60 | return undefined; 61 | } 62 | } 63 | }); 64 | 65 | /* 66 | * Add a handler for Batch ops. 67 | */ 68 | ParseOp._registerDecoder("Batch", function(json) { 69 | var op = null; 70 | ParseCore._arrayEach(json.ops, function(nextOp) { 71 | nextOp = ParseOp._decode(nextOp); 72 | op = nextOp._mergeWithPrevious(op); 73 | }); 74 | return op; 75 | }); 76 | 77 | /** 78 | * @class 79 | * A Set operation indicates that either the field was changed using 80 | * ParseObject.set, or it is a mutable container that was detected as being 81 | * changed. 82 | */ 83 | ParseOp.Set = ParseOp._extend(/** @lends ParseOp.Set.prototype */ { 84 | _initialize: function(value) { 85 | this._value = value; 86 | }, 87 | 88 | /** 89 | * Returns the new value of this field after the set. 90 | */ 91 | value: function() { 92 | return this._value; 93 | }, 94 | 95 | /** 96 | * Returns a JSON version of the operation suitable for sending to Parse. 97 | * @return {Object} 98 | */ 99 | toJSON: function() { 100 | return ParseCore._encode(this.value()); 101 | }, 102 | 103 | _mergeWithPrevious: function(previous) { 104 | return this; 105 | }, 106 | 107 | _estimate: function(oldValue) { 108 | return this.value(); 109 | } 110 | }); 111 | 112 | /** 113 | * A sentinel value that is returned by ParseOp.Unset._estimate to 114 | * indicate the field should be deleted. Basically, if you find _UNSET as a 115 | * value in your object, you should remove that key. 116 | */ 117 | ParseOp._UNSET = {}; 118 | 119 | /** 120 | * @class 121 | * An Unset operation indicates that this field has been deleted from the 122 | * object. 123 | */ 124 | ParseOp.Unset = ParseOp._extend(/** @lends ParseOp.Unset.prototype */ { 125 | /** 126 | * Returns a JSON version of the operation suitable for sending to Parse. 127 | * @return {Object} 128 | */ 129 | toJSON: function() { 130 | return { __op: "Delete" }; 131 | }, 132 | 133 | _mergeWithPrevious: function(previous) { 134 | return this; 135 | }, 136 | 137 | _estimate: function(oldValue) { 138 | return ParseOp._UNSET; 139 | } 140 | }); 141 | 142 | ParseOp._registerDecoder("Delete", function(json) { 143 | return new ParseOp.Unset(); 144 | }); 145 | 146 | /** 147 | * @class 148 | * An Increment is an atomic operation where the numeric value for the field 149 | * will be increased by a given amount. 150 | */ 151 | ParseOp.Increment = ParseOp._extend( 152 | /** @lends ParseOp.Increment.prototype */ { 153 | 154 | _initialize: function(amount) { 155 | this._amount = amount; 156 | }, 157 | 158 | /** 159 | * Returns the amount to increment by. 160 | * @return {Number} the amount to increment by. 161 | */ 162 | amount: function() { 163 | return this._amount; 164 | }, 165 | 166 | /** 167 | * Returns a JSON version of the operation suitable for sending to Parse. 168 | * @return {Object} 169 | */ 170 | toJSON: function() { 171 | return { __op: "Increment", amount: this._amount }; 172 | }, 173 | 174 | _mergeWithPrevious: function(previous) { 175 | if (!previous) { 176 | return this; 177 | } else if (previous instanceof ParseOp.Unset) { 178 | return new ParseOp.Set(this.amount()); 179 | } else if (previous instanceof ParseOp.Set) { 180 | return new ParseOp.Set(previous.value() + this.amount()); 181 | } else if (previous instanceof ParseOp.Increment) { 182 | return new ParseOp.Increment(this.amount() + previous.amount()); 183 | } else { 184 | throw "Op is invalid after previous op."; 185 | } 186 | }, 187 | 188 | _estimate: function(oldValue) { 189 | if (!oldValue) { 190 | return this.amount(); 191 | } 192 | return oldValue + this.amount(); 193 | } 194 | }); 195 | 196 | ParseOp._registerDecoder("Increment", function(json) { 197 | return new ParseOp.Increment(json.amount); 198 | }); 199 | 200 | /** 201 | * @class 202 | * Add is an atomic operation where the given objects will be appended to the 203 | * array that is stored in this field. 204 | */ 205 | ParseOp.Add = ParseOp._extend(/** @lends ParseOp.Add.prototype */ { 206 | _initialize: function(objects) { 207 | this._objects = objects; 208 | }, 209 | 210 | /** 211 | * Returns the objects to be added to the array. 212 | * @return {Array} The objects to be added to the array. 213 | */ 214 | objects: function() { 215 | return this._objects; 216 | }, 217 | 218 | /** 219 | * Returns a JSON version of the operation suitable for sending to Parse. 220 | * @return {Object} 221 | */ 222 | toJSON: function() { 223 | return { __op: "Add", objects: ParseCore._encode(this.objects()) }; 224 | }, 225 | 226 | _mergeWithPrevious: function(previous) { 227 | if (!previous) { 228 | return this; 229 | } else if (previous instanceof ParseOp.Unset) { 230 | return new ParseOp.Set(this.objects()); 231 | } else if (previous instanceof ParseOp.Set) { 232 | return new ParseOp.Set(this._estimate(previous.value())); 233 | } else if (previous instanceof ParseOp.Add) { 234 | return new ParseOp.Add(previous.objects().concat(this.objects())); 235 | } else { 236 | throw "Op is invalid after previous op."; 237 | } 238 | }, 239 | 240 | _estimate: function(oldValue) { 241 | if (!oldValue) { 242 | return _.clone(this.objects()); 243 | } else { 244 | return oldValue.concat(this.objects()); 245 | } 246 | } 247 | }); 248 | 249 | ParseOp._registerDecoder("Add", function(json) { 250 | return new ParseOp.Add(ParseCore._decode(undefined, json.objects)); 251 | }); 252 | 253 | /** 254 | * @class 255 | * AddUnique is an atomic operation where the given items will be appended to 256 | * the array that is stored in this field only if they were not already 257 | * present in the array. 258 | */ 259 | ParseOp.AddUnique = ParseOp._extend( 260 | /** @lends ParseOp.AddUnique.prototype */ { 261 | 262 | _initialize: function(objects) { 263 | this._objects = _.uniq(objects); 264 | }, 265 | 266 | /** 267 | * Returns the objects to be added to the array. 268 | * @return {Array} The objects to be added to the array. 269 | */ 270 | objects: function() { 271 | return this._objects; 272 | }, 273 | 274 | /** 275 | * Returns a JSON version of the operation suitable for sending to Parse. 276 | * @return {Object} 277 | */ 278 | toJSON: function() { 279 | return { __op: "AddUnique", objects: ParseCore._encode(this.objects()) }; 280 | }, 281 | 282 | _mergeWithPrevious: function(previous) { 283 | if (!previous) { 284 | return this; 285 | } else if (previous instanceof ParseOp.Unset) { 286 | return new ParseOp.Set(this.objects()); 287 | } else if (previous instanceof ParseOp.Set) { 288 | return new ParseOp.Set(this._estimate(previous.value())); 289 | } else if (previous instanceof ParseOp.AddUnique) { 290 | return new ParseOp.AddUnique(this._estimate(previous.objects())); 291 | } else { 292 | throw "Op is invalid after previous op."; 293 | } 294 | }, 295 | 296 | _estimate: function(oldValue) { 297 | if (!oldValue) { 298 | return _.clone(this.objects()); 299 | } else { 300 | // We can't just take the _.uniq(_.union(...)) of oldValue and 301 | // this.objects, because the uniqueness may not apply to oldValue 302 | // (especially if the oldValue was set via .set()) 303 | var newValue = _.clone(oldValue); 304 | ParseCore._arrayEach(this.objects(), function(obj) { 305 | if (isInstanceOfParseObject(obj) && obj.id) { 306 | var matchingObj = _.find(newValue, function(anObj) { 307 | return (isInstanceOfParseObject(anObj)) && (anObj.id === obj.id); 308 | }); 309 | if (!matchingObj) { 310 | newValue.push(obj); 311 | } else { 312 | var index = _.indexOf(newValue, matchingObj); 313 | newValue[index] = obj; 314 | } 315 | } else if (!_.contains(newValue, obj)) { 316 | newValue.push(obj); 317 | } 318 | }); 319 | return newValue; 320 | } 321 | } 322 | }); 323 | 324 | ParseOp._registerDecoder("AddUnique", function(json) { 325 | return new ParseOp.AddUnique(ParseCore._decode(undefined, json.objects)); 326 | }); 327 | 328 | /** 329 | * @class 330 | * Remove is an atomic operation where the given objects will be removed from 331 | * the array that is stored in this field. 332 | */ 333 | ParseOp.Remove = ParseOp._extend(/** @lends ParseOp.Remove.prototype */ { 334 | _initialize: function(objects) { 335 | this._objects = _.uniq(objects); 336 | }, 337 | 338 | /** 339 | * Returns the objects to be removed from the array. 340 | * @return {Array} The objects to be removed from the array. 341 | */ 342 | objects: function() { 343 | return this._objects; 344 | }, 345 | 346 | /** 347 | * Returns a JSON version of the operation suitable for sending to Parse. 348 | * @return {Object} 349 | */ 350 | toJSON: function() { 351 | return { __op: "Remove", objects: ParseCore._encode(this.objects()) }; 352 | }, 353 | 354 | _mergeWithPrevious: function(previous) { 355 | if (!previous) { 356 | return this; 357 | } else if (previous instanceof ParseOp.Unset) { 358 | return previous; 359 | } else if (previous instanceof ParseOp.Set) { 360 | return new ParseOp.Set(this._estimate(previous.value())); 361 | } else if (previous instanceof ParseOp.Remove) { 362 | return new ParseOp.Remove(_.union(previous.objects(), this.objects())); 363 | } else { 364 | throw "Op is invalid after previous op."; 365 | } 366 | }, 367 | 368 | _estimate: function(oldValue) { 369 | if (!oldValue) { 370 | return []; 371 | } else { 372 | var newValue = _.difference(oldValue, this.objects()); 373 | // If there are saved Parse Objects being removed, also remove them. 374 | ParseCore._arrayEach(this.objects(), function(obj) { 375 | if (isInstanceOfParseObject(obj) && obj.id) { 376 | newValue = _.reject(newValue, function(other) { 377 | return (isInstanceOfParseObject(other)) && (other.id === obj.id); 378 | }); 379 | } 380 | }); 381 | return newValue; 382 | } 383 | } 384 | }); 385 | 386 | ParseOp._registerDecoder("Remove", function(json) { 387 | return new ParseOp.Remove(ParseCore._decode(undefined, json.objects)); 388 | }); 389 | 390 | /** 391 | * @class 392 | * A Relation operation indicates that the field is an instance of 393 | * $injector.get('ParseRelation'), and objects are being added to, or removed from, that 394 | * relation. 395 | */ 396 | ParseOp.Relation = ParseOp._extend( 397 | /** @lends ParseOp.Relation.prototype */ { 398 | 399 | _initialize: function(adds, removes) { 400 | this._targetClassName = null; 401 | 402 | var self = this; 403 | 404 | var pointerToId = function(object) { 405 | if (isInstanceOfParseObject(object)) { 406 | if (!object.id) { 407 | throw "You can't add an unsaved ParseObject to a relation."; 408 | } 409 | if (!self._targetClassName) { 410 | self._targetClassName = object.className; 411 | } 412 | if (self._targetClassName !== object.className) { 413 | throw "Tried to create a $injector.get('ParseRelation') with 2 different types: " + 414 | self._targetClassName + " and " + object.className + "."; 415 | } 416 | return object.id; 417 | } 418 | return object; 419 | }; 420 | 421 | this.relationsToAdd = _.uniq(_.map(adds, pointerToId)); 422 | this.relationsToRemove = _.uniq(_.map(removes, pointerToId)); 423 | }, 424 | 425 | /** 426 | * Returns an array of unfetched ParseObject that are being added to the 427 | * relation. 428 | * @return {Array} 429 | */ 430 | added: function() { 431 | var self = this; 432 | var ParseObject = $injector.get('ParseObject'); 433 | return _.map(this.relationsToAdd, function(objectId) { 434 | var object = ParseObject._create(self._targetClassName); 435 | object.id = objectId; 436 | return object; 437 | }); 438 | }, 439 | 440 | /** 441 | * Returns an array of unfetched ParseObject that are being removed from 442 | * the relation. 443 | * @return {Array} 444 | */ 445 | removed: function() { 446 | var self = this; 447 | var ParseObject = $injector.get('ParseObject'); 448 | 449 | return _.map(this.relationsToRemove, function(objectId) { 450 | var object = ParseObject._create(self._targetClassName); 451 | object.id = objectId; 452 | return object; 453 | }); 454 | }, 455 | 456 | /** 457 | * Returns a JSON version of the operation suitable for sending to Parse. 458 | * @return {Object} 459 | */ 460 | toJSON: function() { 461 | var adds = null; 462 | var removes = null; 463 | var self = this; 464 | var idToPointer = function(id) { 465 | return { __type: 'Pointer', 466 | className: self._targetClassName, 467 | objectId: id }; 468 | }; 469 | var pointers = null; 470 | if (this.relationsToAdd.length > 0) { 471 | pointers = _.map(this.relationsToAdd, idToPointer); 472 | adds = { "__op": "AddRelation", "objects": pointers }; 473 | } 474 | 475 | if (this.relationsToRemove.length > 0) { 476 | pointers = _.map(this.relationsToRemove, idToPointer); 477 | removes = { "__op": "RemoveRelation", "objects": pointers }; 478 | } 479 | 480 | if (adds && removes) { 481 | return { "__op": "Batch", "ops": [adds, removes]}; 482 | } 483 | 484 | return adds || removes || {}; 485 | }, 486 | 487 | _mergeWithPrevious: function(previous) { 488 | if (!previous) { 489 | return this; 490 | } else if (previous instanceof ParseOp.Unset) { 491 | throw "You can't modify a relation after deleting it."; 492 | } else if (previous instanceof ParseOp.Relation) { 493 | if (previous._targetClassName && 494 | previous._targetClassName !== this._targetClassName) { 495 | throw "Related object must be of class " + previous._targetClassName + 496 | ", but " + this._targetClassName + " was passed in."; 497 | } 498 | var newAdd = _.union(_.difference(previous.relationsToAdd, 499 | this.relationsToRemove), 500 | this.relationsToAdd); 501 | var newRemove = _.union(_.difference(previous.relationsToRemove, 502 | this.relationsToAdd), 503 | this.relationsToRemove); 504 | 505 | var newRelation = new ParseOp.Relation(newAdd, newRemove); 506 | newRelation._targetClassName = this._targetClassName; 507 | return newRelation; 508 | } else { 509 | throw "Op is invalid after previous op."; 510 | } 511 | }, 512 | 513 | _estimate: function(oldValue, object, key) { 514 | var ParseRelation = $injector.get('ParseRelation'); 515 | if (!oldValue) { 516 | var relation = new (ParseRelation)(object, key); 517 | relation.targetClassName = this._targetClassName; 518 | } else if (oldValue instanceof ParseRelation) { 519 | if (this._targetClassName) { 520 | if (oldValue.targetClassName) { 521 | if (oldValue.targetClassName !== this._targetClassName) { 522 | throw "Related object must be a " + oldValue.targetClassName + 523 | ", but a " + this._targetClassName + " was passed in."; 524 | } 525 | } else { 526 | oldValue.targetClassName = this._targetClassName; 527 | } 528 | } 529 | return oldValue; 530 | } else { 531 | throw "Op is invalid after previous op."; 532 | } 533 | } 534 | }); 535 | 536 | ParseOp._registerDecoder("AddRelation", function(json) { 537 | return new ParseOp.Relation(ParseCore._decode(undefined, json.objects), []); 538 | }); 539 | ParseOp._registerDecoder("RemoveRelation", function(json) { 540 | return new ParseOp.Relation([], ParseCore._decode(undefined, json.objects)); 541 | }); 542 | 543 | return ParseOp; 544 | 545 | }); 546 | 547 | -------------------------------------------------------------------------------- /lib/Query.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('ParseAngular.Query', [ 2 | 'ParseAngular.Object', 3 | 'ParseAngular.Core', 4 | 'ParseAngular.GeoPoint', 5 | 'ParseAngular.Error' 6 | ]); 7 | module.factory('ParseQuery', function(ParseObject, ParseCore, ParseGeoPoint, ParseError, $q, $injector){ 8 | 9 | var ParseQuery; 10 | 11 | /** 12 | * Creates a new parse ParseQuery for the given ParseObject subclass. 13 | * @param objectClass - 14 | * An instance of a subclass of ParseObject, or a Parse className string. 15 | * @class 16 | * 17 | *

ParseQuery defines a query that is used to fetch ParseObjects. The 18 | * most common use case is finding all objects that match a query through the 19 | * find method. For example, this sample code fetches all objects 20 | * of class MyClass. It calls a different function depending on 21 | * whether the fetch succeeded or not. 22 | * 23 | *

 24 |     * var query = new ParseQuery(MyClass);
 25 |     * query.find({
 26 |     *   success: function(results) {
 27 |     *     // results is an array of ParseObject.
 28 |     *   },
 29 |     *
 30 |     *   error: function(error) {
 31 |     *     // error is an instance of ParseError.
 32 |     *   }
 33 |     * });

34 | * 35 | *

A ParseQuery can also be used to retrieve a single object whose id is 36 | * known, through the get method. For example, this sample code fetches an 37 | * object of class MyClass and id myId. It calls a 38 | * different function depending on whether the fetch succeeded or not. 39 | * 40 | *

 41 |     * var query = new ParseQuery(MyClass);
 42 |     * query.get(myId, {
 43 |     *   success: function(object) {
 44 |     *     // object is an instance of ParseObject.
 45 |     *   },
 46 |     *
 47 |     *   error: function(object, error) {
 48 |     *     // error is an instance of ParseError.
 49 |     *   }
 50 |     * });

51 | * 52 | *

A ParseQuery can also be used to count the number of objects that match 53 | * the query without retrieving all of those objects. For example, this 54 | * sample code counts the number of objects of the class MyClass 55 | *

 56 |     * var query = new ParseQuery(MyClass);
 57 |     * query.count({
 58 |     *   success: function(number) {
 59 |     *     // There are number instances of MyClass.
 60 |     *   },
 61 |     *
 62 |     *   error: function(error) {
 63 |     *     // error is an instance of ParseError.
 64 |     *   }
 65 |     * });

66 | */ 67 | ParseQuery = function(objectClass) { 68 | if (_.isString(objectClass)) { 69 | objectClass = ParseObject._getSubclass(objectClass); 70 | } 71 | 72 | this.objectClass = objectClass; 73 | 74 | this.className = objectClass.prototype.className; 75 | 76 | this._where = {}; 77 | this._include = []; 78 | this._limit = -1; // negative limit means, do not send a limit 79 | this._skip = 0; 80 | this._extraOptions = {}; 81 | }; 82 | 83 | /** 84 | * Constructs a ParseQuery that is the OR of the passed in queries. For 85 | * example: 86 | *
var compoundQuery = ParseQuery.or(query1, query2, query3);
87 | * 88 | * will create a compoundQuery that is an or of the query1, query2, and 89 | * query3. 90 | * @param {...ParseQuery} var_args The list of queries to OR. 91 | * @return {ParseQuery} The query that is the OR of the passed in queries. 92 | */ 93 | ParseQuery.or = function() { 94 | var queries = _.toArray(arguments); 95 | var className = null; 96 | ParseCore._arrayEach(queries, function(q) { 97 | if (_.isNull(className)) { 98 | className = q.className; 99 | } 100 | 101 | if (className !== q.className) { 102 | throw "All queries must be for the same class"; 103 | } 104 | }); 105 | var query = new ParseQuery(className); 106 | query._orQuery(queries); 107 | return query; 108 | }; 109 | 110 | ParseQuery.prototype = { 111 | /** 112 | * Constructs a ParseObject whose id is already known by fetching data from 113 | * the server. Either options.success or options.error is called when the 114 | * find completes. 115 | * 116 | * @param {String} objectId The id of the object to be fetched. 117 | * @param {Object} options A Backbone-style options object. 118 | * Valid options are: 124 | */ 125 | get: function(objectId, options) { 126 | var self = this; 127 | self.equalTo('objectId', objectId); 128 | 129 | var firstOptions = {}; 130 | if (options && _.has(options, 'useMasterKey')) { 131 | firstOptions = { useMasterKey: options.useMasterKey }; 132 | } 133 | 134 | return self.first(firstOptions).then(function(response) { 135 | if (response) { 136 | return response; 137 | } 138 | var errorObject = new ParseError(ParseError.OBJECT_NOT_FOUND, 139 | "Object not found."); 140 | return $q.reject(errorObject); 141 | 142 | }) 143 | // ._thenRunCallbacks(options, null); 144 | }, 145 | 146 | /** 147 | * Returns a JSON representation of this query. 148 | * @return {Object} The JSON representation of the query. 149 | */ 150 | toJSON: function() { 151 | var params = { 152 | where: this._where 153 | }; 154 | 155 | if (this._include.length > 0) { 156 | params.include = this._include.join(","); 157 | } 158 | if (this._select) { 159 | params.keys = this._select.join(","); 160 | } 161 | if (this._limit >= 0) { 162 | params.limit = this._limit; 163 | } 164 | if (this._skip > 0) { 165 | params.skip = this._skip; 166 | } 167 | if (this._order !== undefined) { 168 | params.order = this._order.join(","); 169 | } 170 | 171 | ParseCore._objectEach(this._extraOptions, function(v, k) { 172 | params[k] = v; 173 | }); 174 | 175 | return params; 176 | }, 177 | 178 | /** 179 | * Retrieves a list of ParseObjects that satisfy this query. 180 | * Either options.success or options.error is called when the find 181 | * completes. 182 | * 183 | * @param {Object} options A Backbone-style options object. Valid options 184 | * are: 190 | * 191 | * @return {Parse.Promise} A promise that is resolved with the results when 192 | * the query completes. 193 | */ 194 | find: function(options) { 195 | var self = this; 196 | options = options || {}; 197 | 198 | var request = ParseCore._request({ 199 | route: "classes", 200 | className: this.className, 201 | method: "GET", 202 | useMasterKey: options.useMasterKey, 203 | data: this.toJSON() 204 | }); 205 | 206 | return request.then(function(response) { 207 | return _.map(response.results, function(json) { 208 | var obj; 209 | if (response.className) { 210 | obj = new ParseObject(response.className); 211 | } else { 212 | obj = new self.objectClass(); 213 | } 214 | obj._finishFetch(json, true); 215 | return obj; 216 | }); 217 | }) 218 | // ._thenRunCallbacks(options); 219 | }, 220 | 221 | /** 222 | * Counts the number of objects that match this query. 223 | * Either options.success or options.error is called when the count 224 | * completes. 225 | * 226 | * @param {Object} options A Backbone-style options object. Valid options 227 | * are: 233 | * 234 | * @return {Parse.Promise} A promise that is resolved with the count when 235 | * the query completes. 236 | */ 237 | count: function(options) { 238 | var self = this; 239 | options = options || {}; 240 | 241 | var params = this.toJSON(); 242 | params.limit = 0; 243 | params.count = 1; 244 | var request = ParseCore._request({ 245 | route: "classes", 246 | className: self.className, 247 | method: "GET", 248 | useMasterKey: options.useMasterKey, 249 | data: params 250 | }); 251 | 252 | return request.then(function(response) { 253 | return response.count; 254 | }) 255 | // ._thenRunCallbacks(options); 256 | }, 257 | 258 | /** 259 | * Retrieves at most one ParseObject that satisfies this query. 260 | * 261 | * Either options.success or options.error is called when it completes. 262 | * success is passed the object if there is one. otherwise, undefined. 263 | * 264 | * @param {Object} options A Backbone-style options object. Valid options 265 | * are: 271 | * 272 | * @return {Parse.Promise} A promise that is resolved with the object when 273 | * the query completes. 274 | */ 275 | first: function(options) { 276 | var self = this; 277 | options = options || {}; 278 | 279 | var params = this.toJSON(); 280 | params.limit = 1; 281 | var request = ParseCore._request({ 282 | route: "classes", 283 | className: this.className, 284 | method: "GET", 285 | useMasterKey: options.useMasterKey, 286 | data: params 287 | }); 288 | 289 | return request.then(function(response) { 290 | return _.map(response.results, function(json) { 291 | var obj; 292 | if (response.className) { 293 | obj = new ParseObject(response.className); 294 | } else { 295 | obj = new self.objectClass(); 296 | } 297 | obj._finishFetch(json, true); 298 | return obj; 299 | })[0]; 300 | }) 301 | // ._thenRunCallbacks(options); 302 | }, 303 | 304 | /** 305 | * Returns a new instance of ParseCollection backed by this query. 306 | * @param {Array} items An array of instances of ParseObject 307 | * with which to start this Collection. 308 | * @param {Object} options An optional object with Backbone-style options. 309 | * Valid options are: 314 | * @return {ParseCollection} 315 | */ 316 | collection: function(items, options) { 317 | options = options || {}; 318 | var ParseCollection = $injector.get('ParseCollection'); 319 | return new ParseCollection(items, _.extend(options, { 320 | model: this.objectClass, 321 | query: this 322 | })); 323 | }, 324 | 325 | /** 326 | * Sets the number of results to skip before returning any results. 327 | * This is useful for pagination. 328 | * Default is to skip zero results. 329 | * @param {Number} n the number of results to skip. 330 | * @return {ParseQuery} Returns the query, so you can chain this call. 331 | */ 332 | skip: function(n) { 333 | this._skip = n; 334 | return this; 335 | }, 336 | 337 | /** 338 | * Sets the limit of the number of results to return. The default limit is 339 | * 100, with a maximum of 1000 results being returned at a time. 340 | * @param {Number} n the number of results to limit to. 341 | * @return {ParseQuery} Returns the query, so you can chain this call. 342 | */ 343 | limit: function(n) { 344 | this._limit = n; 345 | return this; 346 | }, 347 | 348 | /** 349 | * Add a constraint to the query that requires a particular key's value to 350 | * be equal to the provided value. 351 | * @param {String} key The key to check. 352 | * @param value The value that the ParseObject must contain. 353 | * @return {ParseQuery} Returns the query, so you can chain this call. 354 | */ 355 | equalTo: function(key, value) { 356 | if (_.isUndefined(value)) { 357 | return this.doesNotExist(key); 358 | } 359 | 360 | this._where[key] = ParseCore._encode(value); 361 | return this; 362 | }, 363 | 364 | /** 365 | * Helper for condition queries 366 | */ 367 | _addCondition: function(key, condition, value) { 368 | // Check if we already have a condition 369 | if (!this._where[key]) { 370 | this._where[key] = {}; 371 | } 372 | this._where[key][condition] = ParseCore._encode(value); 373 | return this; 374 | }, 375 | 376 | /** 377 | * Add a constraint to the query that requires a particular key's value to 378 | * be not equal to the provided value. 379 | * @param {String} key The key to check. 380 | * @param value The value that must not be equalled. 381 | * @return {ParseQuery} Returns the query, so you can chain this call. 382 | */ 383 | notEqualTo: function(key, value) { 384 | this._addCondition(key, "$ne", value); 385 | return this; 386 | }, 387 | 388 | /** 389 | * Add a constraint to the query that requires a particular key's value to 390 | * be less than the provided value. 391 | * @param {String} key The key to check. 392 | * @param value The value that provides an upper bound. 393 | * @return {ParseQuery} Returns the query, so you can chain this call. 394 | */ 395 | lessThan: function(key, value) { 396 | this._addCondition(key, "$lt", value); 397 | return this; 398 | }, 399 | 400 | /** 401 | * Add a constraint to the query that requires a particular key's value to 402 | * be greater than the provided value. 403 | * @param {String} key The key to check. 404 | * @param value The value that provides an lower bound. 405 | * @return {ParseQuery} Returns the query, so you can chain this call. 406 | */ 407 | greaterThan: function(key, value) { 408 | this._addCondition(key, "$gt", value); 409 | return this; 410 | }, 411 | 412 | /** 413 | * Add a constraint to the query that requires a particular key's value to 414 | * be less than or equal to the provided value. 415 | * @param {String} key The key to check. 416 | * @param value The value that provides an upper bound. 417 | * @return {ParseQuery} Returns the query, so you can chain this call. 418 | */ 419 | lessThanOrEqualTo: function(key, value) { 420 | this._addCondition(key, "$lte", value); 421 | return this; 422 | }, 423 | 424 | /** 425 | * Add a constraint to the query that requires a particular key's value to 426 | * be greater than or equal to the provided value. 427 | * @param {String} key The key to check. 428 | * @param value The value that provides an lower bound. 429 | * @return {ParseQuery} Returns the query, so you can chain this call. 430 | */ 431 | greaterThanOrEqualTo: function(key, value) { 432 | this._addCondition(key, "$gte", value); 433 | return this; 434 | }, 435 | 436 | /** 437 | * Add a constraint to the query that requires a particular key's value to 438 | * be contained in the provided list of values. 439 | * @param {String} key The key to check. 440 | * @param {Array} values The values that will match. 441 | * @return {ParseQuery} Returns the query, so you can chain this call. 442 | */ 443 | containedIn: function(key, values) { 444 | this._addCondition(key, "$in", values); 445 | return this; 446 | }, 447 | 448 | /** 449 | * Add a constraint to the query that requires a particular key's value to 450 | * not be contained in the provided list of values. 451 | * @param {String} key The key to check. 452 | * @param {Array} values The values that will not match. 453 | * @return {ParseQuery} Returns the query, so you can chain this call. 454 | */ 455 | notContainedIn: function(key, values) { 456 | this._addCondition(key, "$nin", values); 457 | return this; 458 | }, 459 | 460 | /** 461 | * Add a constraint to the query that requires a particular key's value to 462 | * contain each one of the provided list of values. 463 | * @param {String} key The key to check. This key's value must be an array. 464 | * @param {Array} values The values that will match. 465 | * @return {ParseQuery} Returns the query, so you can chain this call. 466 | */ 467 | containsAll: function(key, values) { 468 | this._addCondition(key, "$all", values); 469 | return this; 470 | }, 471 | 472 | 473 | /** 474 | * Add a constraint for finding objects that contain the given key. 475 | * @param {String} key The key that should exist. 476 | * @return {ParseQuery} Returns the query, so you can chain this call. 477 | */ 478 | exists: function(key) { 479 | this._addCondition(key, "$exists", true); 480 | return this; 481 | }, 482 | 483 | /** 484 | * Add a constraint for finding objects that do not contain a given key. 485 | * @param {String} key The key that should not exist 486 | * @return {ParseQuery} Returns the query, so you can chain this call. 487 | */ 488 | doesNotExist: function(key) { 489 | this._addCondition(key, "$exists", false); 490 | return this; 491 | }, 492 | 493 | /** 494 | * Add a regular expression constraint for finding string values that match 495 | * the provided regular expression. 496 | * This may be slow for large datasets. 497 | * @param {String} key The key that the string to match is stored in. 498 | * @param {RegExp} regex The regular expression pattern to match. 499 | * @return {ParseQuery} Returns the query, so you can chain this call. 500 | */ 501 | matches: function(key, regex, modifiers) { 502 | this._addCondition(key, "$regex", regex); 503 | if (!modifiers) { modifiers = ""; } 504 | // Javascript regex options support mig as inline options but store them 505 | // as properties of the object. We support mi & should migrate them to 506 | // modifiers 507 | if (regex.ignoreCase) { modifiers += 'i'; } 508 | if (regex.multiline) { modifiers += 'm'; } 509 | 510 | if (modifiers && modifiers.length) { 511 | this._addCondition(key, "$options", modifiers); 512 | } 513 | return this; 514 | }, 515 | 516 | /** 517 | * Add a constraint that requires that a key's value matches a ParseQuery 518 | * constraint. 519 | * @param {String} key The key that the contains the object to match the 520 | * query. 521 | * @param {ParseQuery} query The query that should match. 522 | * @return {ParseQuery} Returns the query, so you can chain this call. 523 | */ 524 | matchesQuery: function(key, query) { 525 | var queryJSON = query.toJSON(); 526 | queryJSON.className = query.className; 527 | this._addCondition(key, "$inQuery", queryJSON); 528 | return this; 529 | }, 530 | 531 | /** 532 | * Add a constraint that requires that a key's value not matches a 533 | * ParseQuery constraint. 534 | * @param {String} key The key that the contains the object to match the 535 | * query. 536 | * @param {ParseQuery} query The query that should not match. 537 | * @return {ParseQuery} Returns the query, so you can chain this call. 538 | */ 539 | doesNotMatchQuery: function(key, query) { 540 | var queryJSON = query.toJSON(); 541 | queryJSON.className = query.className; 542 | this._addCondition(key, "$notInQuery", queryJSON); 543 | return this; 544 | }, 545 | 546 | 547 | /** 548 | * Add a constraint that requires that a key's value matches a value in 549 | * an object returned by a different ParseQuery. 550 | * @param {String} key The key that contains the value that is being 551 | * matched. 552 | * @param {String} queryKey The key in the objects returned by the query to 553 | * match against. 554 | * @param {ParseQuery} query The query to run. 555 | * @return {ParseQuery} Returns the query, so you can chain this call. 556 | */ 557 | matchesKeyInQuery: function(key, queryKey, query) { 558 | var queryJSON = query.toJSON(); 559 | queryJSON.className = query.className; 560 | this._addCondition(key, "$select", 561 | { key: queryKey, query: queryJSON }); 562 | return this; 563 | }, 564 | 565 | /** 566 | * Add a constraint that requires that a key's value not match a value in 567 | * an object returned by a different ParseQuery. 568 | * @param {String} key The key that contains the value that is being 569 | * excluded. 570 | * @param {String} queryKey The key in the objects returned by the query to 571 | * match against. 572 | * @param {ParseQuery} query The query to run. 573 | * @return {ParseQuery} Returns the query, so you can chain this call. 574 | */ 575 | doesNotMatchKeyInQuery: function(key, queryKey, query) { 576 | var queryJSON = query.toJSON(); 577 | queryJSON.className = query.className; 578 | this._addCondition(key, "$dontSelect", 579 | { key: queryKey, query: queryJSON }); 580 | return this; 581 | }, 582 | 583 | /** 584 | * Add constraint that at least one of the passed in queries matches. 585 | * @param {Array} queries 586 | * @return {ParseQuery} Returns the query, so you can chain this call. 587 | */ 588 | _orQuery: function(queries) { 589 | var queryJSON = _.map(queries, function(q) { 590 | return q.toJSON().where; 591 | }); 592 | 593 | this._where.$or = queryJSON; 594 | return this; 595 | }, 596 | 597 | /** 598 | * Converts a string into a regex that matches it. 599 | * Surrounding with \Q .. \E does this, we just need to escape \E's in 600 | * the text separately. 601 | */ 602 | _quote: function(s) { 603 | return "\\Q" + s.replace("\\E", "\\E\\\\E\\Q") + "\\E"; 604 | }, 605 | 606 | /** 607 | * Add a constraint for finding string values that contain a provided 608 | * string. This may be slow for large datasets. 609 | * @param {String} key The key that the string to match is stored in. 610 | * @param {String} substring The substring that the value must contain. 611 | * @return {ParseQuery} Returns the query, so you can chain this call. 612 | */ 613 | contains: function(key, value) { 614 | this._addCondition(key, "$regex", this._quote(value)); 615 | return this; 616 | }, 617 | 618 | /** 619 | * Add a constraint for finding string values that start with a provided 620 | * string. This query will use the backend index, so it will be fast even 621 | * for large datasets. 622 | * @param {String} key The key that the string to match is stored in. 623 | * @param {String} prefix The substring that the value must start with. 624 | * @return {ParseQuery} Returns the query, so you can chain this call. 625 | */ 626 | startsWith: function(key, value) { 627 | this._addCondition(key, "$regex", "^" + this._quote(value)); 628 | return this; 629 | }, 630 | 631 | /** 632 | * Add a constraint for finding string values that end with a provided 633 | * string. This will be slow for large datasets. 634 | * @param {String} key The key that the string to match is stored in. 635 | * @param {String} suffix The substring that the value must end with. 636 | * @return {ParseQuery} Returns the query, so you can chain this call. 637 | */ 638 | endsWith: function(key, value) { 639 | this._addCondition(key, "$regex", this._quote(value) + "$"); 640 | return this; 641 | }, 642 | 643 | /** 644 | * Sorts the results in ascending order by the given key. 645 | * 646 | * @param {(String|String[]|...String} key The key to order by, which is a 647 | * string of comma separated values, or an Array of keys, or multiple keys. 648 | * @return {ParseQuery} Returns the query, so you can chain this call. 649 | */ 650 | ascending: function() { 651 | this._order = []; 652 | return this.addAscending.apply(this, arguments); 653 | }, 654 | 655 | /** 656 | * Sorts the results in ascending order by the given key, 657 | * but can also add secondary sort descriptors without overwriting _order. 658 | * 659 | * @param {(String|String[]|...String} key The key to order by, which is a 660 | * string of comma separated values, or an Array of keys, or multiple keys. 661 | * @return {ParseQuery} Returns the query, so you can chain this call. 662 | */ 663 | addAscending: function(key) { 664 | var self = this; 665 | if (!this._order) { 666 | this._order = []; 667 | } 668 | ParseCore._arrayEach(arguments, function(key) { 669 | if (Array.isArray(key)) { 670 | key = key.join(); 671 | } 672 | self._order = self._order.concat(key.replace(/\s/g, "").split(",")); 673 | }); 674 | return this; 675 | }, 676 | 677 | /** 678 | * Sorts the results in descending order by the given key. 679 | * 680 | * @param {(String|String[]|...String} key The key to order by, which is a 681 | * string of comma separated values, or an Array of keys, or multiple keys. 682 | * @return {ParseQuery} Returns the query, so you can chain this call. 683 | */ 684 | descending: function(key) { 685 | this._order = []; 686 | return this.addDescending.apply(this, arguments); 687 | }, 688 | 689 | /** 690 | * Sorts the results in descending order by the given key, 691 | * but can also add secondary sort descriptors without overwriting _order. 692 | * 693 | * @param {(String|String[]|...String} key The key to order by, which is a 694 | * string of comma separated values, or an Array of keys, or multiple keys. 695 | * @return {ParseQuery} Returns the query, so you can chain this call. 696 | */ 697 | addDescending: function(key) { 698 | var self = this; 699 | if (!this._order) { 700 | this._order = []; 701 | } 702 | ParseCore._arrayEach(arguments, function(key) { 703 | if (Array.isArray(key)) { 704 | key = key.join(); 705 | } 706 | self._order = self._order.concat( 707 | _.map(key.replace(/\s/g, "").split(","), 708 | function(k) { return "-" + k; })); 709 | }); 710 | return this; 711 | }, 712 | 713 | /** 714 | * Add a proximity based constraint for finding objects with key point 715 | * values near the point given. 716 | * @param {String} key The key that the ParseGeoPoint is stored in. 717 | * @param {ParseGeoPoint} point The reference ParseGeoPoint that is used. 718 | * @return {ParseQuery} Returns the query, so you can chain this call. 719 | */ 720 | near: function(key, point) { 721 | if (!(point instanceof ParseGeoPoint)) { 722 | // Try to cast it to a GeoPoint, so that near("loc", [20,30]) works. 723 | point = new ParseGeoPoint(point); 724 | } 725 | this._addCondition(key, "$nearSphere", point); 726 | return this; 727 | }, 728 | 729 | /** 730 | * Add a proximity based constraint for finding objects with key point 731 | * values near the point given and within the maximum distance given. 732 | * @param {String} key The key that the ParseGeoPoint is stored in. 733 | * @param {ParseGeoPoint} point The reference ParseGeoPoint that is used. 734 | * @param {Number} maxDistance Maximum distance (in radians) of results to 735 | * return. 736 | * @return {ParseQuery} Returns the query, so you can chain this call. 737 | */ 738 | withinRadians: function(key, point, distance) { 739 | this.near(key, point); 740 | this._addCondition(key, "$maxDistance", distance); 741 | return this; 742 | }, 743 | 744 | /** 745 | * Add a proximity based constraint for finding objects with key point 746 | * values near the point given and within the maximum distance given. 747 | * Radius of earth used is 3958.8 miles. 748 | * @param {String} key The key that the ParseGeoPoint is stored in. 749 | * @param {ParseGeoPoint} point The reference ParseGeoPoint that is used. 750 | * @param {Number} maxDistance Maximum distance (in miles) of results to 751 | * return. 752 | * @return {ParseQuery} Returns the query, so you can chain this call. 753 | */ 754 | withinMiles: function(key, point, distance) { 755 | return this.withinRadians(key, point, distance / 3958.8); 756 | }, 757 | 758 | /** 759 | * Add a proximity based constraint for finding objects with key point 760 | * values near the point given and within the maximum distance given. 761 | * Radius of earth used is 6371.0 kilometers. 762 | * @param {String} key The key that the ParseGeoPoint is stored in. 763 | * @param {ParseGeoPoint} point The reference ParseGeoPoint that is used. 764 | * @param {Number} maxDistance Maximum distance (in kilometers) of results 765 | * to return. 766 | * @return {ParseQuery} Returns the query, so you can chain this call. 767 | */ 768 | withinKilometers: function(key, point, distance) { 769 | return this.withinRadians(key, point, distance / 6371.0); 770 | }, 771 | 772 | /** 773 | * Add a constraint to the query that requires a particular key's 774 | * coordinates be contained within a given rectangular geographic bounding 775 | * box. 776 | * @param {String} key The key to be constrained. 777 | * @param {ParseGeoPoint} southwest 778 | * The lower-left inclusive corner of the box. 779 | * @param {ParseGeoPoint} northeast 780 | * The upper-right inclusive corner of the box. 781 | * @return {ParseQuery} Returns the query, so you can chain this call. 782 | */ 783 | withinGeoBox: function(key, southwest, northeast) { 784 | if (!(southwest instanceof ParseGeoPoint)) { 785 | southwest = new ParseGeoPoint(southwest); 786 | } 787 | if (!(northeast instanceof ParseGeoPoint)) { 788 | northeast = new ParseGeoPoint(northeast); 789 | } 790 | this._addCondition(key, '$within', { '$box': [southwest, northeast] }); 791 | return this; 792 | }, 793 | 794 | /** 795 | * Include nested ParseObjects for the provided key. You can use dot 796 | * notation to specify which fields in the included object are also fetch. 797 | * @param {String} key The name of the key to include. 798 | * @return {ParseQuery} Returns the query, so you can chain this call. 799 | */ 800 | include: function() { 801 | var self = this; 802 | ParseCore._arrayEach(arguments, function(key) { 803 | if (_.isArray(key)) { 804 | self._include = self._include.concat(key); 805 | } else { 806 | self._include.push(key); 807 | } 808 | }); 809 | return this; 810 | }, 811 | 812 | /** 813 | * Restrict the fields of the returned ParseObjects to include only the 814 | * provided keys. If this is called multiple times, then all of the keys 815 | * specified in each of the calls will be included. 816 | * @param {Array} keys The names of the keys to include. 817 | * @return {ParseQuery} Returns the query, so you can chain this call. 818 | */ 819 | select: function() { 820 | var self = this; 821 | this._select = this._select || []; 822 | ParseCore._arrayEach(arguments, function(key) { 823 | if (_.isArray(key)) { 824 | self._select = self._select.concat(key); 825 | } else { 826 | self._select.push(key); 827 | } 828 | }); 829 | return this; 830 | }, 831 | 832 | /** 833 | * Iterates over each result of a query, calling a callback for each one. If 834 | * the callback returns a promise, the iteration will not continue until 835 | * that promise has been fulfilled. If the callback returns a rejected 836 | * promise, then iteration will stop with that error. The items are 837 | * processed in an unspecified order. The query may not have any sort order, 838 | * and may not use limit or skip. 839 | * @param {Function} callback Callback that will be called with each result 840 | * of the query. 841 | * @param {Object} options An optional Backbone-like options object with 842 | * success and error callbacks that will be invoked once the iteration 843 | * has finished. 844 | * @return {Parse.Promise} A promise that will be fulfilled once the 845 | * iteration has completed. 846 | */ 847 | each: function(callback, options) { 848 | options = options || {}; 849 | 850 | if (this._order || this._skip || (this._limit >= 0)) { 851 | var error = 852 | "Cannot iterate on a query with sort, skip, or limit."; 853 | return $q.reject(error) 854 | // ._thenRunCallbacks(options); 855 | } 856 | 857 | var defer = $q.defer(), 858 | promise = defer.promise; 859 | 860 | var query = new ParseQuery(this.objectClass); 861 | // We can override the batch size from the options. 862 | // This is undocumented, but useful for testing. 863 | query._limit = options.batchSize || 100; 864 | query._where = _.clone(this._where); 865 | query._include = _.clone(this._include); 866 | 867 | query.ascending('objectId'); 868 | 869 | var findOptions = {}; 870 | if (_.has(options, "useMasterKey")) { 871 | findOptions.useMasterKey = options.useMasterKey; 872 | } 873 | 874 | var finished = false; 875 | return ParseCore._continueWhile(function() { 876 | return !finished; 877 | }, function() { 878 | return query.find(findOptions).then(function(results) { 879 | var defer = $q.defer(); 880 | defer.resolve(); 881 | var callbacksDone = defer.promise; 882 | 883 | _.each(results, function(result) { 884 | callbacksDone = callbacksDone.then(function() { 885 | return callback(result); 886 | }); 887 | }); 888 | 889 | return callbacksDone.then(function() { 890 | if (results.length >= query._limit) { 891 | query.greaterThan("objectId", results[results.length - 1].id); 892 | } else { 893 | finished = true; 894 | } 895 | }); 896 | }); 897 | }) 898 | // ._thenRunCallbacks(options); 899 | } 900 | }; 901 | 902 | 903 | return ParseQuery; 904 | }); -------------------------------------------------------------------------------- /lib/Relation.js: -------------------------------------------------------------------------------- 1 | var module = angular.module("ParseAngular.Relation", [ 2 | 'ParseAngular.Query', 3 | 'ParseAngular.Object', 4 | 'ParseAngular.Op' 5 | ]); 6 | 7 | module.factory('ParseRelation', function(ParseQuery, ParseObject, ParseOp){ 8 | 9 | var ParseRelation; 10 | /** 11 | * Creates a new Relation for the given parent object and key. This 12 | * constructor should rarely be used directly, but rather created by 13 | * ParseObject.relation. 14 | * @param {ParseObject} parent The parent of this relation. 15 | * @param {String} key The key for this relation on the parent. 16 | * @see ParseObject#relation 17 | * @class 18 | * 19 | *

20 | * A class that is used to access all of the children of a many-to-many 21 | * relationship. Each instance of ParseRelation is associated with a 22 | * particular parent object and key. 23 | *

24 | */ 25 | ParseRelation = function(parent, key) { 26 | this.parent = parent; 27 | this.key = key; 28 | this.targetClassName = null; 29 | }; 30 | 31 | ParseRelation.prototype = { 32 | /** 33 | * Makes sure that this relation has the right parent and key. 34 | */ 35 | _ensureParentAndKey: function(parent, key) { 36 | this.parent = this.parent || parent; 37 | this.key = this.key || key; 38 | if (this.parent !== parent) { 39 | throw "Internal Error. Relation retrieved from two different Objects."; 40 | } 41 | if (this.key !== key) { 42 | throw "Internal Error. Relation retrieved from two different keys."; 43 | } 44 | }, 45 | 46 | /** 47 | * Adds a ParseObject or an array of ParseObjects to the relation. 48 | * @param {} objects The item or items to add. 49 | */ 50 | add: function(objects) { 51 | if (!_.isArray(objects)) { 52 | objects = [objects]; 53 | } 54 | 55 | var change = new ParseOp.Relation(objects, []); 56 | this.parent.set(this.key, change); 57 | this.targetClassName = change._targetClassName; 58 | }, 59 | 60 | /** 61 | * Removes a ParseObject or an array of ParseObjects from this relation. 62 | * @param {} objects The item or items to remove. 63 | */ 64 | remove: function(objects) { 65 | if (!_.isArray(objects)) { 66 | objects = [objects]; 67 | } 68 | 69 | var change = new ParseOp.Relation([], objects); 70 | this.parent.set(this.key, change); 71 | this.targetClassName = change._targetClassName; 72 | }, 73 | 74 | /** 75 | * Returns a JSON version of the object suitable for saving to disk. 76 | * @return {Object} 77 | */ 78 | toJSON: function() { 79 | return { "__type": "Relation", "className": this.targetClassName }; 80 | }, 81 | 82 | /** 83 | * Returns a ParseQuery that is limited to objects in this 84 | * relation. 85 | * @return {ParseQuery} 86 | */ 87 | query: function() { 88 | var targetClass; 89 | var query; 90 | if (!this.targetClassName) { 91 | targetClass = ParseObject._getSubclass(this.parent.className); 92 | query = new ParseQuery(targetClass); 93 | query._extraOptions.redirectClassNameForKey = this.key; 94 | } else { 95 | targetClass = ParseObject._getSubclass(this.targetClassName); 96 | query = new ParseQuery(targetClass); 97 | } 98 | query._addCondition("$relatedTo", "object", this.parent._toPointer()); 99 | query._addCondition("$relatedTo", "key", this.key); 100 | 101 | return query; 102 | } 103 | }; 104 | 105 | return ParseRelation; 106 | 107 | }); -------------------------------------------------------------------------------- /lib/Role.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('ParseAngular.Role', [ 2 | 'ParseAngular.Object', 3 | 'ParseAngular.Error', 4 | 'ParseAngular.ACL' 5 | ]); 6 | 7 | module.factory('ParseRole', function(ParseObject, ParseError, $injector){ 8 | 9 | /** 10 | * Represents a Role on the Parse server. Roles represent groupings of 11 | * Users for the purposes of granting permissions (e.g. specifying an ACL 12 | * for an Object). Roles are specified by their sets of child users and 13 | * child roles, all of which are granted any permissions that the parent 14 | * role has. 15 | * 16 | *

Roles must have a name (which cannot be changed after creation of the 17 | * role), and must specify an ACL.

18 | * @class 19 | * A ParseRole is a local representation of a role persisted to the Parse 20 | * cloud. 21 | */ 22 | ParseRole = ParseObject.extend("_Role", /** @lends ParseRole.prototype */ { 23 | // Instance Methods 24 | 25 | /** 26 | * Constructs a new ParseRole with the given name and ACL. 27 | * 28 | * @param {String} name The name of the Role to create. 29 | * @param {Parse.ACL} acl The ACL for this role. Roles must have an ACL. 30 | */ 31 | constructor: function(name, acl) { 32 | var ParseACL = $injector.get('ParseACL'); 33 | if (_.isString(name) && (acl instanceof ParseACL)) { 34 | ParseObject.prototype.constructor.call(this, null, null); 35 | this.setName(name); 36 | this.setACL(acl); 37 | } else { 38 | ParseObject.prototype.constructor.call(this, name, acl); 39 | } 40 | }, 41 | 42 | /** 43 | * Gets the name of the role. You can alternatively call role.get("name") 44 | * 45 | * @return {String} the name of the role. 46 | */ 47 | getName: function() { 48 | return this.get("name"); 49 | }, 50 | 51 | /** 52 | * Sets the name for a role. This value must be set before the role has 53 | * been saved to the server, and cannot be set once the role has been 54 | * saved. 55 | * 56 | *

57 | * A role's name can only contain alphanumeric characters, _, -, and 58 | * spaces. 59 | *

60 | * 61 | *

This is equivalent to calling role.set("name", name)

62 | * 63 | * @param {String} name The name of the role. 64 | * @param {Object} options Standard options object with success and error 65 | * callbacks. 66 | */ 67 | setName: function(name, options) { 68 | return this.set("name", name, options); 69 | }, 70 | 71 | /** 72 | * Gets the Parse.Relation for the Parse.Users that are direct 73 | * children of this role. These users are granted any privileges that this 74 | * role has been granted (e.g. read or write access through ACLs). You can 75 | * add or remove users from the role through this relation. 76 | * 77 | *

This is equivalent to calling role.relation("users")

78 | * 79 | * @return {Parse.Relation} the relation for the users belonging to this 80 | * role. 81 | */ 82 | getUsers: function() { 83 | return this.relation("users"); 84 | }, 85 | 86 | /** 87 | * Gets the Parse.Relation for the ParseRoles that are direct 88 | * children of this role. These roles' users are granted any privileges that 89 | * this role has been granted (e.g. read or write access through ACLs). You 90 | * can add or remove child roles from this role through this relation. 91 | * 92 | *

This is equivalent to calling role.relation("roles")

93 | * 94 | * @return {Parse.Relation} the relation for the roles belonging to this 95 | * role. 96 | */ 97 | getRoles: function() { 98 | return this.relation("roles"); 99 | }, 100 | 101 | /** 102 | * @ignore 103 | */ 104 | validate: function(attrs, options) { 105 | if ("name" in attrs && attrs.name !== this.getName()) { 106 | var newName = attrs.name; 107 | if (this.id && this.id !== attrs.objectId) { 108 | // Check to see if the objectId being set matches this.id. 109 | // This happens during a fetch -- the id is set before calling fetch. 110 | // Let the name be set in this case. 111 | return new ParseError(ParseError.OTHER_CAUSE, 112 | "A role's name can only be set before it has been saved."); 113 | } 114 | if (!_.isString(newName)) { 115 | return new ParseError(ParseError.OTHER_CAUSE, 116 | "A role's name must be a String."); 117 | } 118 | if (!(/^[0-9a-zA-Z\-_ ]+$/).test(newName)) { 119 | return new ParseError(ParseError.OTHER_CAUSE, 120 | "A role's name can only contain alphanumeric characters, _," + 121 | " -, and spaces."); 122 | } 123 | } 124 | if (ParseObject.prototype.validate) { 125 | return ParseObject.prototype.validate.call(this, attrs, options); 126 | } 127 | return false; 128 | } 129 | }); 130 | 131 | return ParseRole; 132 | }); -------------------------------------------------------------------------------- /lib/User.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('ParseAngular.User', [ 2 | 'ParseAngular.Object', 3 | 'ParseAngular.Core' 4 | ]); 5 | 6 | module.factory('ParseUser', function(ParseObject, ParseCore){ 7 | 8 | ParseUser = ParseObject.extend("_User", /** @lends ParseUser.prototype */ { 9 | // Instance Variables 10 | _isCurrentUser: false, 11 | 12 | 13 | // Instance Methods 14 | 15 | /** 16 | * Merges another object's attributes into this object. 17 | */ 18 | _mergeFromObject: function(other) { 19 | if (other.getSessionToken()) { 20 | this._sessionToken = other.getSessionToken(); 21 | } 22 | ParseUser.__super__._mergeFromObject.call(this, other); 23 | }, 24 | 25 | /** 26 | * Internal method to handle special fields in a _User response. 27 | */ 28 | _mergeMagicFields: function(attrs) { 29 | if (attrs.sessionToken) { 30 | this._sessionToken = attrs.sessionToken; 31 | delete attrs.sessionToken; 32 | } 33 | ParseUser.__super__._mergeMagicFields.call(this, attrs); 34 | }, 35 | 36 | /** 37 | * Removes null values from authData (which exist temporarily for 38 | * unlinking) 39 | */ 40 | _cleanupAuthData: function() { 41 | if (!this.isCurrent()) { 42 | return; 43 | } 44 | var authData = this.get('authData'); 45 | if (!authData) { 46 | return; 47 | } 48 | ParseCore._objectEach(this.get('authData'), function(value, key) { 49 | if (!authData[key]) { 50 | delete authData[key]; 51 | } 52 | }); 53 | }, 54 | 55 | /** 56 | * Synchronizes authData for all providers. 57 | */ 58 | _synchronizeAllAuthData: function() { 59 | var authData = this.get('authData'); 60 | if (!authData) { 61 | return; 62 | } 63 | 64 | var self = this; 65 | ParseCore._objectEach(this.get('authData'), function(value, key) { 66 | self._synchronizeAuthData(key); 67 | }); 68 | }, 69 | 70 | /** 71 | * Synchronizes auth data for a provider (e.g. puts the access token in the 72 | * right place to be used by the Facebook SDK). 73 | */ 74 | _synchronizeAuthData: function(provider) { 75 | if (!this.isCurrent()) { 76 | return; 77 | } 78 | var authType; 79 | if (_.isString(provider)) { 80 | authType = provider; 81 | provider = ParseUser._authProviders[authType]; 82 | } else { 83 | authType = provider.getAuthType(); 84 | } 85 | var authData = this.get('authData'); 86 | if (!authData || !provider) { 87 | return; 88 | } 89 | var success = provider.restoreAuthentication(authData[authType]); 90 | if (!success) { 91 | this._unlinkFrom(provider); 92 | } 93 | }, 94 | 95 | _handleSaveResult: function(makeCurrent) { 96 | // Clean up and synchronize the authData object, removing any unset values 97 | if (makeCurrent) { 98 | this._isCurrentUser = true; 99 | } 100 | this._cleanupAuthData(); 101 | this._synchronizeAllAuthData(); 102 | // Don't keep the password around. 103 | delete this._serverData.password; 104 | this._rebuildEstimatedDataForKey("password"); 105 | this._refreshCache(); 106 | if (makeCurrent || this.isCurrent()) { 107 | ParseUser._saveCurrentUser(this); 108 | } 109 | }, 110 | 111 | /** 112 | * Unlike in the Android/iOS SDKs, logInWith is unnecessary, since you can 113 | * call linkWith on the user (even if it doesn't exist yet on the server). 114 | */ 115 | _linkWith: function(provider, options) { 116 | var authType; 117 | if (_.isString(provider)) { 118 | authType = provider; 119 | provider = ParseUser._authProviders[provider]; 120 | } else { 121 | authType = provider.getAuthType(); 122 | } 123 | if (_.has(options, 'authData')) { 124 | var authData = this.get('authData') || {}; 125 | authData[authType] = options.authData; 126 | this.set('authData', authData); 127 | 128 | // Overridden so that the user can be made the current user. 129 | var newOptions = _.clone(options) || {}; 130 | newOptions.success = function(model) { 131 | model._handleSaveResult(true); 132 | if (options.success) { 133 | options.success.apply(this, arguments); 134 | } 135 | }; 136 | return this.save({'authData': authData}, newOptions); 137 | } else { 138 | var self = this; 139 | var defer = $q.defer(); 140 | var promise = defer.promise; 141 | provider.authenticate({ 142 | success: function(provider, result) { 143 | self._linkWith(provider, { 144 | authData: result, 145 | success: options.success, 146 | error: options.error 147 | }).then(function() { 148 | promise.resolve(self); 149 | }); 150 | }, 151 | error: function(provider, error) { 152 | if (options.error) { 153 | options.error(self, error); 154 | } 155 | promise.reject(error); 156 | } 157 | }); 158 | return promise; 159 | } 160 | }, 161 | 162 | /** 163 | * Unlinks a user from a service. 164 | */ 165 | _unlinkFrom: function(provider, options) { 166 | var authType; 167 | if (_.isString(provider)) { 168 | authType = provider; 169 | provider = ParseUser._authProviders[provider]; 170 | } else { 171 | authType = provider.getAuthType(); 172 | } 173 | var newOptions = _.clone(options); 174 | var self = this; 175 | newOptions.authData = null; 176 | newOptions.success = function(model) { 177 | self._synchronizeAuthData(provider); 178 | if (options.success) { 179 | options.success.apply(this, arguments); 180 | } 181 | }; 182 | return this._linkWith(provider, newOptions); 183 | }, 184 | 185 | /** 186 | * Checks whether a user is linked to a service. 187 | */ 188 | _isLinked: function(provider) { 189 | var authType; 190 | if (_.isString(provider)) { 191 | authType = provider; 192 | } else { 193 | authType = provider.getAuthType(); 194 | } 195 | var authData = this.get('authData') || {}; 196 | return !!authData[authType]; 197 | }, 198 | 199 | /** 200 | * Deauthenticates all providers. 201 | */ 202 | _logOutWithAll: function() { 203 | var authData = this.get('authData'); 204 | if (!authData) { 205 | return; 206 | } 207 | var self = this; 208 | ParseCore._objectEach(this.get('authData'), function(value, key) { 209 | self._logOutWith(key); 210 | }); 211 | }, 212 | 213 | /** 214 | * Deauthenticates a single provider (e.g. removing access tokens from the 215 | * Facebook SDK). 216 | */ 217 | _logOutWith: function(provider) { 218 | if (!this.isCurrent()) { 219 | return; 220 | } 221 | if (_.isString(provider)) { 222 | provider = ParseUser._authProviders[provider]; 223 | } 224 | if (provider && provider.deauthenticate) { 225 | provider.deauthenticate(); 226 | } 227 | }, 228 | 229 | /** 230 | * Signs up a new user. You should call this instead of save for 231 | * new ParseUsers. This will create a new ParseUser on the server, and 232 | * also persist the session on disk so that you can access the user using 233 | * current. 234 | * 235 | *

A username and password must be set before calling signUp.

236 | * 237 | *

Calls options.success or options.error on completion.

238 | * 239 | * @param {Object} attrs Extra fields to set on the new user, or null. 240 | * @param {Object} options A Backbone-style options object. 241 | * @return {Parse.Promise} A promise that is fulfilled when the signup 242 | * finishes. 243 | * @see ParseUser.signUp 244 | */ 245 | signUp: function(attrs, options) { 246 | var error; 247 | options = options || {}; 248 | 249 | var username = (attrs && attrs.username) || this.get("username"); 250 | if (!username || (username === "")) { 251 | error = new ParseError( 252 | ParseError.OTHER_CAUSE, 253 | "Cannot sign up user with an empty name."); 254 | return $q.reject(error); 255 | } 256 | 257 | var password = (attrs && attrs.password) || this.get("password"); 258 | if (!password || (password === "")) { 259 | error = new ParseError( 260 | ParseError.OTHER_CAUSE, 261 | "Cannot sign up user with an empty password."); 262 | if (options && options.error) { 263 | options.error(this, error); 264 | } 265 | return $q.reject(error); 266 | } 267 | 268 | return this.save(attrs) 269 | .then(function(model){ 270 | model._handleSaveResult(true); 271 | return model; 272 | }) 273 | }, 274 | 275 | /** 276 | * Logs in a ParseUser. On success, this saves the session to localStorage, 277 | * so you can retrieve the currently logged in user using 278 | * current. 279 | * 280 | *

A username and password must be set before calling logIn.

281 | * 282 | *

Calls options.success or options.error on completion.

283 | * 284 | * @param {Object} options A Backbone-style options object. 285 | * @see ParseUser.logIn 286 | * @return {Parse.Promise} A promise that is fulfilled with the user when 287 | * the login is complete. 288 | */ 289 | logIn: function(options) { 290 | var model = this; 291 | options = options || {}; 292 | var request = ParseCore._request({ 293 | route: "login", 294 | method: "GET", 295 | useMasterKey: options.useMasterKey, 296 | data: this.toJSON() 297 | }); 298 | return request.then(function(resp, status, xhr) { 299 | var serverAttrs = model.parse(resp, status, xhr); 300 | model._finishFetch(serverAttrs); 301 | model._handleSaveResult(true); 302 | return model; 303 | }) 304 | // ._thenRunCallbacks(options, this); 305 | }, 306 | 307 | /** 308 | * @see ParseObject#save 309 | */ 310 | save: function(arg1, arg2, arg3) { 311 | var i, attrs, current, options, saved; 312 | if (_.isObject(arg1) || _.isNull(arg1) || _.isUndefined(arg1)) { 313 | attrs = arg1; 314 | options = arg2; 315 | } else { 316 | attrs = {}; 317 | attrs[arg1] = arg2; 318 | options = arg3; 319 | } 320 | options = options || {}; 321 | 322 | return ParseObject.prototype.save.call(this, attrs, newOptions) 323 | .then(function(model){ 324 | model._handleSaveResult(false); 325 | return model; 326 | }); 327 | }, 328 | 329 | /** 330 | * @see ParseObject#fetch 331 | */ 332 | fetch: function(options) { 333 | return ParseObject.prototype.fetch.call(this, newOptions) 334 | .then(function(model){ 335 | model._handleSaveResult(false); 336 | return model; 337 | }); 338 | }, 339 | 340 | /** 341 | * Returns true if current would return this user. 342 | * @see ParseUser#current 343 | */ 344 | isCurrent: function() { 345 | return this._isCurrentUser; 346 | }, 347 | 348 | /** 349 | * Returns get("username"). 350 | * @return {String} 351 | * @see ParseObject#get 352 | */ 353 | getUsername: function() { 354 | return this.get("username"); 355 | }, 356 | 357 | /** 358 | * Calls set("username", username, options) and returns the result. 359 | * @param {String} username 360 | * @param {Object} options A Backbone-style options object. 361 | * @return {Boolean} 362 | * @see ParseObject.set 363 | */ 364 | setUsername: function(username, options) { 365 | return this.set("username", username, options); 366 | }, 367 | 368 | /** 369 | * Calls set("password", password, options) and returns the result. 370 | * @param {String} password 371 | * @param {Object} options A Backbone-style options object. 372 | * @return {Boolean} 373 | * @see ParseObject.set 374 | */ 375 | setPassword: function(password, options) { 376 | return this.set("password", password, options); 377 | }, 378 | 379 | /** 380 | * Returns get("email"). 381 | * @return {String} 382 | * @see ParseObject#get 383 | */ 384 | getEmail: function() { 385 | return this.get("email"); 386 | }, 387 | 388 | /** 389 | * Calls set("email", email, options) and returns the result. 390 | * @param {String} email 391 | * @param {Object} options A Backbone-style options object. 392 | * @return {Boolean} 393 | * @see ParseObject.set 394 | */ 395 | setEmail: function(email, options) { 396 | return this.set("email", email, options); 397 | }, 398 | 399 | /** 400 | * Checks whether this user is the current user and has been authenticated. 401 | * @return (Boolean) whether this user is the current user and is logged in. 402 | */ 403 | authenticated: function() { 404 | return !!this._sessionToken && 405 | (ParseUser.current() && ParseUser.current().id === this.id); 406 | }, 407 | 408 | /** 409 | * Returns the session token for this user, if the user has been logged in, 410 | * or if it is the result of a query with the master key. Otherwise, returns 411 | * undefined. 412 | * @return {String} the session token, or undefined 413 | */ 414 | getSessionToken: function() { 415 | return this._sessionToken; 416 | } 417 | 418 | }, /** @lends ParseUser */ { 419 | // Class Variables 420 | 421 | // The currently logged-in user. 422 | _currentUser: null, 423 | 424 | // Whether currentUser is known to match the serialized version on disk. 425 | // This is useful for saving a localstorage check if you try to load 426 | // _currentUser frequently while there is none stored. 427 | _currentUserMatchesDisk: false, 428 | 429 | // The localStorage key suffix that the current user is stored under. 430 | _CURRENT_USER_KEY: "currentUser", 431 | 432 | // The mapping of auth provider names to actual providers 433 | _authProviders: {}, 434 | 435 | // Whether to rewrite className User to _User 436 | _performUserRewrite: true, 437 | 438 | 439 | // Class Methods 440 | 441 | /** 442 | * Signs up a new user with a username (or email) and password. 443 | * This will create a new ParseUser on the server, and also persist the 444 | * session in localStorage so that you can access the user using 445 | * {@link #current}. 446 | * 447 | *

Calls options.success or options.error on completion.

448 | * 449 | * @param {String} username The username (or email) to sign up with. 450 | * @param {String} password The password to sign up with. 451 | * @param {Object} attrs Extra fields to set on the new user. 452 | * @param {Object} options A Backbone-style options object. 453 | * @return {Parse.Promise} A promise that is fulfilled with the user when 454 | * the signup completes. 455 | * @see ParseUser#signUp 456 | */ 457 | signUp: function(username, password, attrs, options) { 458 | attrs = attrs || {}; 459 | attrs.username = username; 460 | attrs.password = password; 461 | var user = ParseObject._create("_User"); 462 | return user.signUp(attrs, options); 463 | }, 464 | 465 | /** 466 | * Logs in a user with a username (or email) and password. On success, this 467 | * saves the session to disk, so you can retrieve the currently logged in 468 | * user using current. 469 | * 470 | *

Calls options.success or options.error on completion.

471 | * 472 | * @param {String} username The username (or email) to log in with. 473 | * @param {String} password The password to log in with. 474 | * @param {Object} options A Backbone-style options object. 475 | * @return {Parse.Promise} A promise that is fulfilled with the user when 476 | * the login completes. 477 | * @see ParseUser#logIn 478 | */ 479 | logIn: function(username, password, options) { 480 | var user = ParseObject._create("_User"); 481 | user._finishFetch({ username: username, password: password }); 482 | return user.logIn(options); 483 | }, 484 | 485 | /** 486 | * Logs in a user with a session token. On success, this saves the session 487 | * to disk, so you can retrieve the currently logged in user using 488 | * current. 489 | * 490 | *

Calls options.success or options.error on completion.

491 | * 492 | * @param {String} sessionToken The sessionToken to log in with. 493 | * @param {Object} options A Backbone-style options object. 494 | * @return {Parse.Promise} A promise that is fulfilled with the user when 495 | * the login completes. 496 | */ 497 | become: function(sessionToken, options) { 498 | options = options || {}; 499 | 500 | var user = ParseObject._create("_User"); 501 | return ParseCore._request({ 502 | route: "users", 503 | className: "me", 504 | method: "GET", 505 | useMasterKey: options.useMasterKey, 506 | sessionToken: sessionToken 507 | }).then(function(resp, status, xhr) { 508 | var serverAttrs = user.parse(resp, status, xhr); 509 | user._finishFetch(serverAttrs); 510 | user._handleSaveResult(true); 511 | return user; 512 | }) 513 | // ._thenRunCallbacks(options, user); 514 | }, 515 | 516 | /** 517 | * Logs out the currently logged in user session. This will remove the 518 | * session from disk, log out of linked services, and future calls to 519 | * current will return null. 520 | */ 521 | logOut: function() { 522 | if (ParseUser._currentUser !== null) { 523 | ParseUser._currentUser._logOutWithAll(); 524 | ParseUser._currentUser._isCurrentUser = false; 525 | } 526 | ParseUser._currentUserMatchesDisk = true; 527 | ParseUser._currentUser = null; 528 | ParseCore.localStorage.removeItem( 529 | ParseCore._getParsePath(ParseUser._CURRENT_USER_KEY)); 530 | }, 531 | 532 | /** 533 | * Requests a password reset email to be sent to the specified email address 534 | * associated with the user account. This email allows the user to securely 535 | * reset their password on the Parse site. 536 | * 537 | *

Calls options.success or options.error on completion.

538 | * 539 | * @param {String} email The email address associated with the user that 540 | * forgot their password. 541 | * @param {Object} options A Backbone-style options object. 542 | */ 543 | requestPasswordReset: function(email, options) { 544 | options = options || {}; 545 | var request = ParseCore._request({ 546 | route: "requestPasswordReset", 547 | method: "POST", 548 | useMasterKey: options.useMasterKey, 549 | data: { email: email } 550 | }); 551 | return request 552 | // ._thenRunCallbacks(options); 553 | }, 554 | 555 | /** 556 | * Retrieves the currently logged in ParseUser with a valid session, 557 | * either from memory or localStorage, if necessary. 558 | * @return {ParseObject} The currently logged in ParseUser. 559 | */ 560 | current: function() { 561 | if (ParseUser._currentUser) { 562 | return ParseUser._currentUser; 563 | } 564 | 565 | if (ParseUser._currentUserMatchesDisk) { 566 | return ParseUser._currentUser; 567 | } 568 | 569 | // Load the user from local storage. 570 | ParseUser._currentUserMatchesDisk = true; 571 | 572 | var userData = ParseCore.localStorage.getItem(ParseCore._getParsePath( 573 | ParseUser._CURRENT_USER_KEY) 574 | ); 575 | if (!userData) { 576 | return null; 577 | } 578 | ParseUser._currentUser = ParseObject._create("_User"); 579 | ParseUser._currentUser._isCurrentUser = true; 580 | 581 | var json = JSON.parse(userData); 582 | ParseUser._currentUser.id = json._id; 583 | delete json._id; 584 | ParseUser._currentUser._sessionToken = json._sessionToken; 585 | delete json._sessionToken; 586 | ParseUser._currentUser._finishFetch(json); 587 | 588 | ParseUser._currentUser._synchronizeAllAuthData(); 589 | ParseUser._currentUser._refreshCache(); 590 | ParseUser._currentUser._opSetQueue = [{}]; 591 | return ParseUser._currentUser; 592 | }, 593 | 594 | /** 595 | * Allow someone to define a custom User class without className 596 | * being rewritten to _User. The default behavior is to rewrite 597 | * User to _User for legacy reasons. This allows developers to 598 | * override that behavior. 599 | * 600 | * @param {Boolean} isAllowed Whether or not to allow custom User class 601 | */ 602 | allowCustomUserClass: function(isAllowed) { 603 | this._performUserRewrite = !isAllowed; 604 | }, 605 | 606 | /** 607 | * Persists a user as currentUser to localStorage, and into the singleton. 608 | */ 609 | _saveCurrentUser: function(user) { 610 | if (ParseUser._currentUser !== user) { 611 | ParseUser.logOut(); 612 | } 613 | user._isCurrentUser = true; 614 | ParseUser._currentUser = user; 615 | ParseUser._currentUserMatchesDisk = true; 616 | 617 | var json = user.toJSON(); 618 | json._id = user.id; 619 | json._sessionToken = user._sessionToken; 620 | ParseCore.localStorage.setItem( 621 | ParseCore._getParsePath(ParseUser._CURRENT_USER_KEY), 622 | JSON.stringify(json)); 623 | }, 624 | 625 | _registerAuthenticationProvider: function(provider) { 626 | ParseUser._authProviders[provider.getAuthType()] = provider; 627 | // Synchronize the current user with the auth provider. 628 | if (ParseUser.current()) { 629 | ParseUser.current()._synchronizeAuthData(provider.getAuthType()); 630 | } 631 | }, 632 | 633 | _logInWith: function(provider, options) { 634 | var user = ParseObject._create("_User"); 635 | return user._linkWith(provider, options); 636 | } 637 | 638 | }); 639 | 640 | return ParseUser; 641 | 642 | }); -------------------------------------------------------------------------------- /lib/_ParseAngular.js: -------------------------------------------------------------------------------- 1 | var module = angular.module('Parse', [ 2 | 'ParseAngular.ACL', 3 | 'ParseAngular.Cloud', 4 | 'ParseAngular.Config', 5 | 'ParseAngular.Core', 6 | // 'ParseAngular.Error', 7 | // 'ParseAngular.Events', 8 | 'ParseAngular.FacebookUtils', 9 | 'ParseAngular.GeoPoint', 10 | 'ParseAngular.Object', 11 | // 'ParseAngular.Op', 12 | 'ParseAngular.Query', 13 | 'ParseAngular.Relation', 14 | 'ParseAngular.User' 15 | ]); 16 | 17 | 18 | module.factory('ParseSDK', function( 19 | ParseACL, 20 | ParseCloud, 21 | ParseConfig, 22 | ParseCore, 23 | ParseFacebookUtils, 24 | ParseGeoPoint, 25 | ParseObject, 26 | ParseQuery, 27 | ParseRelation, 28 | ParseUser 29 | ){ 30 | 31 | var ParseSDK = ParseCore; 32 | 33 | var toExtend = { 34 | ACL: ParseACL, 35 | Cloud: ParseCloud, 36 | Config: ParseConfig, 37 | FacebookUtils: ParseFacebookUtils, 38 | GeoPoint: ParseGeoPoint, 39 | Object: ParseObject, 40 | Query: ParseQuery, 41 | Relation: ParseRelation, 42 | User: ParseUser 43 | }; 44 | 45 | // Augment the Core with extra modules 46 | _.extend(ParseSDK, toExtend); 47 | 48 | return ParseSDK; 49 | 50 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-angular-sdk", 3 | "version": "1.0.0", 4 | "description": "Parse Angular custom SDK", 5 | "main": "dist/parse-angular-sdk.min.js", 6 | "scripts": { 7 | "test": "npm test" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "grunt": "^0.4.5", 13 | "grunt-contrib-concat": "^0.5.0", 14 | "grunt-contrib-uglify": "^0.7.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Try.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('app', [ 2 | 'Parse' 3 | ]); 4 | 5 | app.run(function(ParseSDK){ 6 | 7 | 8 | ParseSDK.initialize('BASVF7j1qlgnpgIhSAm6xs3oE6hDLc1SKsYGijw5', 'uXH3hwH6LQI3gQoRvBhWYU23EZRD1zNxIsrHaFYD'); 9 | 10 | var obj = new ParseSDK.Object("Monster"); 11 | obj.set('test', 'here'); 12 | obj.set('time', new Date()); 13 | obj.save() 14 | .then(function(o){ 15 | 16 | 17 | var q = new ParseSDK.Query('Monster'); 18 | q.limit(10); 19 | q.descending('createdAt'); 20 | q.find() 21 | .then(function(results){ 22 | console.log(results); 23 | }) 24 | 25 | }); 26 | 27 | }); --------------------------------------------------------------------------------