├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── PATENTS ├── README.md ├── build_releases.sh ├── gulpfile.js ├── index.js ├── node.js ├── package.json ├── react-native.js ├── setup-jest.js ├── src ├── .flowconfig ├── Analytics.js ├── Cloud.js ├── CoreManager.js ├── FacebookUtils.js ├── InstallationController.js ├── ObjectState.js ├── Parse.js ├── ParseACL.js ├── ParseConfig.js ├── ParseError.js ├── ParseFile.js ├── ParseGeoPoint.js ├── ParseInstallation.js ├── ParseObject.js ├── ParseOp.js ├── ParsePromise.js ├── ParseQuery.js ├── ParseRelation.js ├── ParseRole.js ├── ParseSession.js ├── ParseUser.js ├── Push.js ├── RESTController.js ├── ReduxActionCreators.js ├── ReduxCacheHelper.js ├── ReduxReducers.js ├── ReduxStore.js ├── Storage.js ├── StorageController.browser.js ├── StorageController.default.js ├── StorageController.react-native.js ├── TaskQueue.js ├── __mocks__ │ └── react-native.js ├── __tests__ │ ├── Analytics-test.js │ ├── Cloud-test.js │ ├── CoreManager-test.js │ ├── InstallationController-test.js │ ├── ObjectState-test.js │ ├── Parse-test.js │ ├── ParseACL-test.js │ ├── ParseConfig-test.js │ ├── ParseFile-test.js │ ├── ParseGeoPoint-test.js │ ├── ParseObject-test.js │ ├── ParseOp-test.js │ ├── ParsePromise-test.js │ ├── ParseQuery-test.js │ ├── ParseRelation-test.js │ ├── ParseRole-test.js │ ├── ParseSession-test.js │ ├── ParseUser-test.js │ ├── Push-test.js │ ├── RESTController-test.js │ ├── Storage-test.js │ ├── TaskQueue-test.js │ ├── arrayContainsObject-test.js │ ├── canBeSerialized-test.js │ ├── decode-test.js │ ├── encode-test.js │ ├── equals-test.js │ ├── escape-test.js │ ├── parseDate-test.js │ ├── test_helpers │ │ ├── asyncHelper.js │ │ └── mockXHR.js │ ├── unique-test.js │ └── unsavedChildren-test.js ├── arrayContainsObject.js ├── canBeSerialized.js ├── decode.js ├── encode.js ├── equals.js ├── escape.js ├── interfaces │ ├── package.json.js │ ├── react-native.js │ └── xmlhttprequest.js ├── isRevocableSession.js ├── parseDate.js ├── unique.js └── unsavedChildren.js └── vendor ├── README.md └── babel-plugin-dead-code-elimination ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | lib 4 | node_modules 5 | test_output 6 | *~ 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | 6 | branches: 7 | only: 8 | - master 9 | 10 | after_script: cat ./coverage/coverage-final.json | ./node_modules/codecov.io/bin/codecov.io.js && rm -rf ./coverage 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Parse JavaScript SDK 2 | We want to make contributing to this project as easy and transparent as possible. 3 | 4 | If you're looking to get started, but want to ease yourself into the codebase, look for issues tagged [good first bug](https://github.com/ParsePlatform/Parse-SDK-JS/labels/good%20first%20bug). These are simple yet valuable tasks that should be easy to get started. 5 | 6 | ## Code of Conduct 7 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. 8 | 9 | ## Our Development Process 10 | Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed. 11 | 12 | ### `master` is unsafe 13 | Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment. 14 | 15 | ### Pull Requests 16 | We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. 17 | 18 | 1. Fork the repo and create your branch from `master`. 19 | 2. Add unit tests for any new code you add. 20 | 3. If you've changed APIs, update the documentation. 21 | 4. Ensure the test suite passes. 22 | 5. Make sure your code lints. 23 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 24 | 25 | ### Contributor License Agreement ("CLA") 26 | In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. 27 | 28 | Complete your CLA here: 29 | 30 | 31 | ## Bugs 32 | Although we try to keep developing on Parse easy, you still may run into some issues. General questions should be asked on [Google Groups][google-group], technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues. 33 | 34 | ### Known Issues 35 | We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. 36 | 37 | ### Reporting New Issues 38 | Not all issues are SDK issues. If you're unsure whether your bug is with the SDK or backend, you can test to see if it reproduces with our [REST API][rest-api] and [Parse API Console][parse-api-console]. If it does, you can report backend bugs [here][bug-reports]. 39 | 40 | Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits: 41 | 42 | * A description. What did you expect to happen and what actually happened? Why do you think that was wrong? 43 | * A simple unit test that fails. Refer [here][tests-dir] for examples of existing unit tests. See our [README](README.md#usage) for how to run unit tests. You can submit a pull request with your failing unit test so that our CI verifies that the test fails. 44 | * What version does this reproduce on? What version did it last work on? 45 | * [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer. 46 | * Anything else you find relevant. 47 | 48 | ### Security Bugs 49 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. 50 | 51 | ## Coding Style 52 | * Most importantly, match the existing code style as much as possible. 53 | * We use [Flow](http://flowtype.org/) and ES6 for this codebase. Use modern syntax whenever possible. 54 | * Keep lines within 80 characters. 55 | * Always end lines with semicolons. 56 | 57 | ## License 58 | By contributing to the Parse JavaScript SDK, you agree that your contributions will be licensed under its license. 59 | 60 | [google-group]: https://groups.google.com/forum/#!forum/parse-developers 61 | [stack-overflow]: http://stackoverflow.com/tags/parse.com 62 | [bug-reports]: https://www.parse.com/help#report 63 | [rest-api]: https://www.parse.com/docs/rest/guide 64 | [parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/ 65 | [stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg 66 | [tests-dir]: /src/__tests__ 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Parse JavaScript SDK software 4 | 5 | Copyright (c) 2015-present, Parse, LLC. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Parse nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the Parse JavaScript SDK software distributed by Parse, LLC. 4 | 5 | Parse, LLC. ("Parse") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Parse’s rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Parse or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Parse or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Parse or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Parse that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parse-Redux 2 | This is a drop-in replacement for Parse branched from Parse v1.6.14 that uses a Redux architecture, bringing better application state management to Parse users. 3 | 4 | ## Objects 5 | The vanilla Parse library maintains object state in a mutable JS object. Parse-Redux makes that object immutable and exposes it through the Redux state. 6 | 7 | It is not advised the data directly from the Redux state - it contains a number of keys that are used together to 'estimate' what a given object's properties are between the last fetched Server data and the local object cache. 8 | 9 | The Redux cache can be directly used to check for changes in object state - just not for calculating object attributes. 10 | 11 | ## Queries 12 | Parse-Redux allows for Query caching and cache management. Queries are first created as normal: 13 | 14 | ```javascript 15 | var query = new Parse.Query('Potato') 16 | ``` 17 | 18 | Parse-Redux uses JSON.stringify(query) as the default QUERY_ID, but this can be customized with the grouping parameter (string). 19 | 20 | The following functions can be run on any Query with the standard Parse Query restraints. 21 | 22 | ### query.find.refresh([grouping]) 23 | Runs the query and caches it, replacing any exsting cache. Returns a Parse.Promise. 24 | 25 | ### query.find.init([grouping]) 26 | Runs the query if there is no cache, then caches the result. Returns a Parse.Promise, which is instantly resolved with the cached data, if there is a cached result (and creates no network request). 27 | 28 | ### query.find.append([grouping]) 29 | Appends more results to an existing query cache. Only supports sorting ascending/descending queries by 'createdAt' parameter. Creates a new cache if there is no existing cache. Returns a Parse.Promise. 30 | 31 | ### query.find.prepend([grouping]) 32 | Prepends more results to an existing query cache. Only supports sorting ascending/descending queries by 'createdAt' parameter. Creates a new cache if there is no existing cache. Returns a Parse.Promise. 33 | 34 | *Note: prepend will hit the end of the available results, unless the query has a ``greaterThan`` or ``lessThan`` restraint between the first and last available `createdAt` dates, or if the query is descending and there are items that have been created since the query was first cached.* 35 | 36 | ### query.find.get([grouping]) 37 | Retrieves a cached query result, if it exists. Returns the object itself - not a promise. Returns undefined if there is no cache. Does not create network requests. 38 | 39 | ### query.find.getState([grouping]) 40 | Retrieves the cached query result with more context. Returns: 41 | 42 | ```javascript 43 | { 44 | cache: [] 45 | pending: boolean, 46 | appendEnd: boolean, 47 | prependEnd: boolean 48 | } 49 | ``` 50 | cache: The same result returned by ``query.find.get()`` 51 | pending: The query state 52 | appendEnd: Estimates if the query has hit the end of the available results in the forward direction. 53 | prependEnd: Estimates if the query has hit the end of the available results in the backward direction. 54 | 55 | prependEnd is undefined if prepend() has not been run. 56 | 57 | ## Cloud Code 58 | The name, data, and options parameters are the vanilla Parse parameters on the run function. Parse-Redux uses JSON.stringify(data) as the default FUNCTION_ID, but this can be customized with the grouping parameter (string). 59 | 60 | ### Parse.Cloud.run.refresh(name, [data], [grouping], [limit], [options]) 61 | Runs the function and caches it, replacing any exsting cache. Returns a Parse.Promise. 62 | 63 | ### Parse.Cloud.run.init(name, [data], [grouping], [limit], [options]) 64 | Runs the function if there is no cache, then caches the result. Returns a Parse.Promise, which is instantly resolved with the cached data, if there is a cached result (and creates no network request). 65 | 66 | ### Parse.Cloud.run.append(name, [data], [grouping], [limit], [options]) 67 | Appends more results to an existing function cache. Creates a new cache if there is no existing cache. Returns a Parse.Promise. 68 | 69 | ### Parse.Cloud.run.prepend(name, [data], [grouping], [limit], [options]) 70 | Prepends more results to an existing function cache. Creates a new cache if there is no existing cache. Returns a Parse.Promise. 71 | 72 | ### Parse.Cloud.run.get(name, [data], [grouping]) 73 | Retrieves a cached function result, if it exists. Returns the object itself - not a promise. Returns undefined if there is no cache. Does not create network requests. 74 | 75 | ### Parse.Cloud.run.getState(name, [data], [grouping]) 76 | Retrieves the cached function result with more context. Returns an identical object to the query getState function. 77 | 78 | ## Custom stores 79 | This is an optional step that allows Parse-Redux integration into existing Redux stores, giving all the normal Redux functionality. 80 | 81 | ```javascript 82 | import { createStore, combineReducers } from 'redux' 83 | import Parse from 'parse-redux' 84 | 85 | var reducer = combineReducers({ 86 | Parse: Parse.getReducer() // This reducer MUST be named Parse and be at the top level 87 | }) 88 | 89 | var store = createStore(reducer) 90 | 91 | Parse.setStore(store) 92 | Parse.initialize(APPLICATION_ID, JAVASCRIPT_KEY) 93 | ``` 94 | 95 | ## Accessing caches from the store 96 | The caches can be accessed as follows: 97 | 98 | ```javascript 99 | var state = store.getState() 100 | var objectState = state.Parse.Object[CLASS_NAME][OBJECT_ID] 101 | var queryState = state.Parse.Query[CLASS_NAME][QUERY_ID] 102 | var cloudState = state.Parse.Cloud[FUNCTION_NAME][FUNCTION_ID] 103 | ``` 104 | Direct access to these items should be fairly limited as the Parse-Redux library provides getters for easy access to the cache. 105 | 106 | ## Breaking changes from Parse 107 | Parse-Redux is a complete drop-in solution that changes none of the preexisting Parse functionality, with one exception. In the vanilla Parse API, when a user logs out, the objects stay in Parse's cache - even those with ACL limitations. Parse-Redux clears all items from the cache when the user logs out. 108 | 109 | ## What Parse-Redux doesn't do (right now) 110 | Parse-Redux is a young library. Some Parse-Redux to-do's are: 111 | 112 | * Pending state management for Objects: Queries and Cloud functions both have pending state available, but Objects don't have access to this for save() and fetch() functions. 113 | * Error management in Redux state: Errors currently work exactly as the vanilla Parse errors - through promises. -------------------------------------------------------------------------------- /build_releases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SDK_VERSION=$(cat package.json | sed -n -e '/version/ s/.*: *"\([^"]*\).*/\1/p') 4 | echo "Building JavaScript SDK v$SDK_VERSION...\n" 5 | 6 | echo "Cleaning up old builds...\n" 7 | rm -rf dist lib 8 | 9 | echo "Browser Release:" 10 | PARSE_BUILD=browser gulp compile 11 | echo "Node.js Release:" 12 | PARSE_BUILD=node gulp compile 13 | echo "React Native Release:" 14 | PARSE_BUILD=react-native gulp compile 15 | echo "Bundling and minifying for CDN distribution:" 16 | gulp browserify 17 | gulp minify 18 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var babel = require('gulp-babel'); 2 | var browserify = require('browserify'); 3 | var derequire = require('gulp-derequire'); 4 | var gulp = require('gulp'); 5 | var insert = require('gulp-insert'); 6 | var path = require('path'); 7 | var rename = require('gulp-rename'); 8 | var replace = require('gulp-replace'); 9 | var source = require('vinyl-source-stream'); 10 | var uglify = require('gulp-uglify'); 11 | 12 | var BUILD = process.env.PARSE_BUILD || 'browser'; 13 | var VERSION = require('./package.json').version; 14 | 15 | var DEV_HEADER = ( 16 | '/**\n' + 17 | ' * Parse JavaScript SDK v' + VERSION + '\n' + 18 | ' *\n' + 19 | ' * The source tree of this library can be found at\n' + 20 | ' * https://github.com/ParsePlatform/Parse-SDK-JS\n' + 21 | ' */\n' 22 | ); 23 | 24 | var FULL_HEADER = ( 25 | '/**\n' + 26 | ' * Parse JavaScript SDK v' + VERSION + '\n' + 27 | ' *\n' + 28 | ' * Copyright (c) 2015-present, Parse, LLC.\n' + 29 | ' * All rights reserved.\n' + 30 | ' *\n' + 31 | ' * The source tree of this library can be found at\n' + 32 | ' * https://github.com/ParsePlatform/Parse-SDK-JS\n' + 33 | ' * This source code is licensed under the BSD-style license found in the\n' + 34 | ' * LICENSE file in the root directory of this source tree. An additional grant\n' + 35 | ' * of patent rights can be found in the PATENTS file in the same directory.\n' + 36 | ' */\n' 37 | ); 38 | 39 | gulp.task('compile', function() { 40 | var packageJSON = { 41 | version: VERSION 42 | }; 43 | return gulp.src('src/*.js') 44 | .pipe(babel({ 45 | experimental: true, 46 | optional: [ 47 | 'runtime', 48 | 'utility.inlineEnvironmentVariables' 49 | ], 50 | plugins: [ 51 | 'inline-package-json', 52 | require('./vendor/babel-plugin-dead-code-elimination') 53 | ], 54 | })) 55 | .pipe(gulp.dest(path.join('lib', BUILD))); 56 | }); 57 | 58 | gulp.task('browserify', function() { 59 | var stream = browserify({ 60 | builtins: { _process: true }, 61 | entries: 'lib/browser/Parse.js', 62 | standalone: 'Parse' 63 | }) 64 | .exclude('xmlhttprequest') 65 | .ignore('_process') 66 | .bundle(); 67 | 68 | return stream.pipe(source('parse-latest.js')) 69 | .pipe(derequire()) 70 | .pipe(insert.prepend(DEV_HEADER)) 71 | .pipe(gulp.dest('./dist')); 72 | }); 73 | 74 | gulp.task('minify', function() { 75 | return gulp.src('dist/parse-latest.js') 76 | .pipe(uglify()) 77 | .pipe(insert.prepend(FULL_HEADER)) 78 | .pipe(rename({ extname: '.min.js' })) 79 | .pipe(gulp.dest('./dist')) 80 | }); 81 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/browser/Parse.js'); 2 | -------------------------------------------------------------------------------- /node.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/node/Parse.js'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-redux", 3 | "version": "0.1.5", 4 | "description": "The Parse JavaScript SDK", 5 | "homepage": "https://github.com/chrbala/Parse-Redux", 6 | "keywords": [ 7 | "cloud", 8 | "mobile", 9 | "api", 10 | "redux", 11 | "flux" 12 | ], 13 | "license": "BSD-3-Clause", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/chrbala/Parse-Redux" 17 | }, 18 | "bugs": "https://github.com/chrbala/Parse-Redux/issues", 19 | "files": [ 20 | "index.js", 21 | "node.js", 22 | "react-native.js", 23 | "dist/", 24 | "lib/", 25 | "LICENSE", 26 | "PATENTS", 27 | "README.md" 28 | ], 29 | "browser": { 30 | "react-native": false 31 | }, 32 | "dependencies": { 33 | "babel-runtime": "^5.8.20", 34 | "redux": "3.0.4", 35 | "xmlhttprequest": "^1.7.0" 36 | }, 37 | "devDependencies": { 38 | "babel-jest": "~5.3.0", 39 | "babel-plugin-flow-comments": "^1.0.9", 40 | "babel-plugin-inline-package-json": "~1.0.1", 41 | "browserify": "^11.0.1", 42 | "codecov.io": "^0.1.6", 43 | "gulp": "^3.9.0", 44 | "gulp-babel": "^5.2.0", 45 | "gulp-derequire": "^2.1.0", 46 | "gulp-insert": "^0.5.0", 47 | "gulp-rename": "^1.2.2", 48 | "gulp-replace": "^0.5.4", 49 | "gulp-uglify": "^1.4.0", 50 | "jasmine-reporters": "~1.0.0", 51 | "jest-cli": "~0.5.0", 52 | "vinyl-source-stream": "^1.1.0" 53 | }, 54 | "scripts": { 55 | "build": "./build_releases.sh", 56 | "release": "./build_releases.sh && npm publish", 57 | "test": "PARSE_BUILD=node jest", 58 | "browser": "PARSE_BUILD=browser gulp compile" 59 | }, 60 | "jest": { 61 | "collectCoverage": true, 62 | "testPathDirs": [ 63 | "src/" 64 | ], 65 | "testPathIgnorePatterns": [ 66 | "/node_modules/", 67 | "/test_helpers/" 68 | ], 69 | "scriptPreprocessor": "node_modules/babel-jest", 70 | "setupTestFrameworkScriptFile": "setup-jest.js" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /react-native.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/react-native/Parse.js'); 2 | -------------------------------------------------------------------------------- /setup-jest.js: -------------------------------------------------------------------------------- 1 | require('jasmine-reporters'); 2 | var reporter = new jasmine.JUnitXmlReporter('test_output/'); 3 | jasmine.getEnv().addReporter(reporter); 4 | -------------------------------------------------------------------------------- /src/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*\/node_modules\/ 3 | 4 | [include] 5 | 6 | [libs] 7 | interfaces/ 8 | 9 | [options] 10 | unsafe.enable_getters_and_setters=true 11 | -------------------------------------------------------------------------------- /src/Analytics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | 14 | import type ParsePromise from './ParsePromise'; 15 | 16 | /** 17 | * Parse.Analytics provides an interface to Parse's logging and analytics 18 | * backend. 19 | * 20 | * @class Parse.Analytics 21 | * @static 22 | */ 23 | 24 | /** 25 | * Tracks the occurrence of a custom event with additional dimensions. 26 | * Parse will store a data point at the time of invocation with the given 27 | * event name. 28 | * 29 | * Dimensions will allow segmentation of the occurrences of this custom 30 | * event. Keys and values should be {@code String}s, and will throw 31 | * otherwise. 32 | * 33 | * To track a user signup along with additional metadata, consider the 34 | * following: 35 | *
36 |   * var dimensions = {
37 |   *  gender: 'm',
38 |   *  source: 'web',
39 |   *  dayType: 'weekend'
40 |   * };
41 |   * Parse.Analytics.track('signup', dimensions);
42 |   * 
43 | * 44 | * There is a default limit of 8 dimensions per event tracked. 45 | * 46 | * @method track 47 | * @param {String} name The name of the custom event to report to Parse as 48 | * having happened. 49 | * @param {Object} dimensions The dictionary of information by which to 50 | * segment this event. 51 | * @param {Object} options A Backbone-style callback object. 52 | * @return {Parse.Promise} A promise that is resolved when the round-trip 53 | * to the server completes. 54 | */ 55 | export function track( 56 | name: string, 57 | dimensions: { [key: string]: string }, 58 | options?: mixed 59 | ): ParsePromise { 60 | name = name || ''; 61 | name = name.replace(/^\s*/, ''); 62 | name = name.replace(/\s*$/, ''); 63 | if (name.length === 0) { 64 | throw new TypeError('A name for the custom event must be provided'); 65 | } 66 | 67 | for (var key in dimensions) { 68 | if (typeof key !== 'string' || typeof dimensions[key] !== 'string') { 69 | throw new TypeError( 70 | 'track() dimensions expects keys and values of type "string".' 71 | ); 72 | } 73 | } 74 | 75 | options = options || {}; 76 | return ( 77 | CoreManager.getAnalyticsController() 78 | .track(name, dimensions) 79 | ._thenRunCallbacks(options) 80 | ); 81 | } 82 | 83 | var DefaultController = { 84 | track(name, dimensions) { 85 | var path = 'events/' + name; 86 | var RESTController = CoreManager.getRESTController(); 87 | return RESTController.request('POST', path, { dimensions: dimensions }); 88 | } 89 | }; 90 | 91 | CoreManager.setAnalyticsController(DefaultController); 92 | -------------------------------------------------------------------------------- /src/Cloud.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import decode from './decode'; 14 | import encode from './encode'; 15 | import ParseError from './ParseError'; 16 | import ParsePromise from './ParsePromise'; 17 | 18 | import * as Store from './ReduxStore'; 19 | import { FunctionActions as Actions } from './ReduxActionCreators'; 20 | 21 | import CacheHelper, { getItemState } from './ReduxCacheHelper'; 22 | var cacheHelper = new CacheHelper({Actions, namespace: "Cloud"}); 23 | 24 | /** 25 | * Contains functions for calling and declaring 26 | * cloud functions. 27 | *

28 | * Some functions are only available from Cloud Code. 29 | *

30 | * 31 | * @class Parse.Cloud 32 | * @static 33 | */ 34 | 35 | /** 36 | * Makes a call to a cloud function. 37 | * @method run 38 | * @param {String} name The function name. 39 | * @param {Object} data The parameters to send to the cloud function. 40 | * @param {Object} options A Backbone-style options object 41 | * options.success, if set, should be a function to handle a successful 42 | * call to a cloud function. options.error should be a function that 43 | * handles an error running the cloud function. Both functions are 44 | * optional. Both functions take a single argument. 45 | * @return {Parse.Promise} A promise that will be resolved with the result 46 | * of the function. 47 | */ 48 | export var run = function( 49 | name: string, 50 | data: mixed, 51 | options: { [key: string]: mixed } 52 | ) { 53 | options = options || {}; 54 | 55 | if (typeof name !== 'string' || name.length === 0) { 56 | throw new TypeError('Cloud function name must be a string.'); 57 | } 58 | 59 | var requestOptions = {}; 60 | if (options.useMasterKey) { 61 | requestOptions.useMasterKey = options.useMasterKey; 62 | } 63 | if (options.sessionToken) { 64 | requestOptions.sessionToken = options.sessionToken; 65 | } 66 | 67 | return ( 68 | CoreManager.getCloudController().run(name, data, requestOptions)._thenRunCallbacks(options) 69 | ); 70 | } 71 | 72 | run.refresh = function(name, data, grouping, limit, options) { 73 | var cb = run.bind(null, name, data, options); 74 | return cacheHelper.refresh(cb, {name, data, grouping, limit}); 75 | } 76 | 77 | run.init = function(name, data, grouping, limit, options) { 78 | var cb = run.bind(null, name, data, options); 79 | return cacheHelper.init(cb, {name, data, grouping, limit}); 80 | } 81 | 82 | run.get = function(name, data, grouping) { 83 | return cacheHelper.get({name, data, grouping}); 84 | } 85 | 86 | run.append = function( 87 | name: string, 88 | data = {}: mixed, 89 | grouping: string, 90 | limit: number, 91 | options: { [key: string]: mixed } 92 | ) { 93 | var cb = run.bind(null, name, data, options); 94 | return cacheHelper.append(cb, {name, data, grouping, limit}); 95 | } 96 | 97 | run.prepend = function( 98 | name: string, 99 | data = {}: mixed, 100 | grouping: string, 101 | limit: number, 102 | options: { [key: string]: mixed } 103 | ) { 104 | var cb = run.bind(null, name, data, options); 105 | return cacheHelper.prepend(cb, {name, data, grouping, limit}); 106 | } 107 | 108 | run.getState = function(name, data, grouping) { 109 | var state = getItemState(Store.getState().Parse.Cloud, {name, data, grouping}); 110 | if (Object.keys(state).length) 111 | return state; 112 | } 113 | 114 | var DefaultController = { 115 | run(name, data, options) { 116 | var RESTController = CoreManager.getRESTController(); 117 | 118 | var payload = encode(data, true); 119 | 120 | var requestOptions = {}; 121 | if (options.hasOwnProperty('useMasterKey')) { 122 | requestOptions.useMasterKey = options.useMasterKey; 123 | } 124 | if (options.hasOwnProperty('sessionToken')) { 125 | requestOptions.sessionToken = options.sessionToken; 126 | } 127 | 128 | var request = RESTController.request( 129 | 'POST', 130 | 'functions/' + name, 131 | payload, 132 | requestOptions 133 | ); 134 | 135 | return request.then(function(res) { 136 | var decoded = decode(res); 137 | if (decoded && decoded.hasOwnProperty('result')) { 138 | return ParsePromise.as(decoded.result); 139 | } 140 | return ParsePromise.error( 141 | new ParseError( 142 | ParseError.INVALID_JSON, 143 | 'The server returned an invalid response.' 144 | ) 145 | ); 146 | })._thenRunCallbacks(options); 147 | } 148 | }; 149 | 150 | CoreManager.setCloudController(DefaultController); 151 | -------------------------------------------------------------------------------- /src/FacebookUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow-weak 10 | */ 11 | 12 | import parseDate from './parseDate'; 13 | import ParseUser from './ParseUser'; 14 | 15 | var PUBLIC_KEY = "*"; 16 | 17 | var initialized = false; 18 | var requestedPermissions; 19 | var initOptions; 20 | var provider = { 21 | authenticate(options) { 22 | if (typeof FB === 'undefined') { 23 | options.error(this, 'Facebook SDK not found.'); 24 | } 25 | FB.login((response) => { 26 | if (response.authResponse) { 27 | if (options.success) { 28 | options.success(this, { 29 | id: response.authResponse.userID, 30 | access_token: response.authResponse.accessToken, 31 | expiration_date: new Date(response.authResponse.expiresIn * 1000 + 32 | (new Date()).getTime()).toJSON() 33 | }); 34 | } 35 | } else { 36 | if (options.error) { 37 | options.error(this, response); 38 | } 39 | } 40 | }, { 41 | scope: requestedPermissions 42 | }); 43 | }, 44 | 45 | restoreAuthentication(authData) { 46 | if (authData) { 47 | var expiration = parseDate(authData.expiration_date); 48 | var expiresIn = expiration ? 49 | (expiration.getTime() - new Date().getTime()) / 1000 : 50 | 0; 51 | 52 | var authResponse = { 53 | userID: authData.id, 54 | accessToken: authData.access_token, 55 | expiresIn: expiresIn 56 | }; 57 | var newOptions = {}; 58 | if (initOptions) { 59 | for (var key in initOptions) { 60 | newOptions[key] = initOptions[key]; 61 | } 62 | } 63 | newOptions.authResponse = authResponse; 64 | 65 | // Suppress checks for login status from the browser. 66 | newOptions.status = false; 67 | 68 | // If the user doesn't match the one known by the FB SDK, log out. 69 | // Most of the time, the users will match -- it's only in cases where 70 | // the FB SDK knows of a different user than the one being restored 71 | // from a Parse User that logged in with username/password. 72 | var existingResponse = FB.getAuthResponse(); 73 | if (existingResponse && 74 | existingResponse.userID !== authResponse.userID) { 75 | FB.logout(); 76 | } 77 | 78 | FB.init(newOptions); 79 | } 80 | return true; 81 | }, 82 | 83 | getAuthType() { 84 | return 'facebook'; 85 | }, 86 | 87 | deauthenticate() { 88 | this.restoreAuthentication(null); 89 | } 90 | }; 91 | 92 | /** 93 | * Provides a set of utilities for using Parse with Facebook. 94 | * @class Parse.FacebookUtils 95 | * @static 96 | */ 97 | var FacebookUtils = { 98 | /** 99 | * Initializes Parse Facebook integration. Call this function after you 100 | * have loaded the Facebook Javascript SDK with the same parameters 101 | * as you would pass to 102 | * 104 | * FB.init(). Parse.FacebookUtils will invoke FB.init() for you 105 | * with these arguments. 106 | * 107 | * @method init 108 | * @param {Object} options Facebook options argument as described here: 109 | * 111 | * FB.init(). The status flag will be coerced to 'false' because it 112 | * interferes with Parse Facebook integration. Call FB.getLoginStatus() 113 | * explicitly if this behavior is required by your application. 114 | */ 115 | init(options) { 116 | if (typeof FB === 'undefined') { 117 | throw new Error( 118 | 'The Facebook JavaScript SDK must be loaded before calling init.' 119 | ); 120 | } 121 | initOptions = {}; 122 | if (options) { 123 | for (var key in options) { 124 | initOptions[key] = options[key]; 125 | } 126 | } 127 | if (initOptions.status && typeof console !== 'undefined') { 128 | var warn = console.warn || console.log || function() {}; 129 | warn.call(console, 'The "status" flag passed into' + 130 | ' FB.init, when set to true, can interfere with Parse Facebook' + 131 | ' integration, so it has been suppressed. Please call' + 132 | ' FB.getLoginStatus() explicitly if you require this behavior.'); 133 | } 134 | initOptions.status = false; 135 | FB.init(initOptions); 136 | ParseUser._registerAuthenticationProvider(provider); 137 | initialized = true; 138 | }, 139 | 140 | /** 141 | * Gets whether the user has their account linked to Facebook. 142 | * 143 | * @method isLinked 144 | * @param {Parse.User} user User to check for a facebook link. 145 | * The user must be logged in on this device. 146 | * @return {Boolean} true if the user has their account 147 | * linked to Facebook. 148 | */ 149 | isLinked(user) { 150 | return user._isLinked('facebook'); 151 | }, 152 | 153 | /** 154 | * Logs in a user using Facebook. This method delegates to the Facebook 155 | * SDK to authenticate the user, and then automatically logs in (or 156 | * creates, in the case where it is a new user) a Parse.User. 157 | * 158 | * @method logIn 159 | * @param {String, Object} permissions The permissions required for Facebook 160 | * log in. This is a comma-separated string of permissions. 161 | * Alternatively, supply a Facebook authData object as described in our 162 | * REST API docs if you want to handle getting facebook auth tokens 163 | * yourself. 164 | * @param {Object} options Standard options object with success and error 165 | * callbacks. 166 | */ 167 | logIn(permissions, options) { 168 | if (!permissions || typeof permissions === 'string') { 169 | if (!initialized) { 170 | throw new Error( 171 | 'You must initialize FacebookUtils before calling logIn.' 172 | ); 173 | } 174 | requestedPermissions = permissions; 175 | return ParseUser._logInWith('facebook', options); 176 | } else { 177 | var newOptions = {}; 178 | if (options) { 179 | for (var key in options) { 180 | newOptions[key] = options[key]; 181 | } 182 | } 183 | newOptions.authData = permissions; 184 | return ParseUser._logInWith('facebook', newOptions); 185 | } 186 | }, 187 | 188 | /** 189 | * Links Facebook to an existing PFUser. This method delegates to the 190 | * Facebook SDK to authenticate the user, and then automatically links 191 | * the account to the Parse.User. 192 | * 193 | * @method link 194 | * @param {Parse.User} user User to link to Facebook. This must be the 195 | * current user. 196 | * @param {String, Object} permissions The permissions required for Facebook 197 | * log in. This is a comma-separated string of permissions. 198 | * Alternatively, supply a Facebook authData object as described in our 199 | * REST API docs if you want to handle getting facebook auth tokens 200 | * yourself. 201 | * @param {Object} options Standard options object with success and error 202 | * callbacks. 203 | */ 204 | link(user, permissions, options) { 205 | if (!permissions || typeof permissions === 'string') { 206 | if (!initialized) { 207 | throw new Error( 208 | 'You must initialize FacebookUtils before calling link.' 209 | ); 210 | } 211 | requestedPermissions = permissions; 212 | return user._linkWith('facebook', options); 213 | } else { 214 | var newOptions = {}; 215 | if (options) { 216 | for (var key in options) { 217 | newOptions[key] = options[key]; 218 | } 219 | } 220 | newOptions.authData = permissions; 221 | return user._linkWith('facebook', newOptions); 222 | } 223 | }, 224 | 225 | /** 226 | * Unlinks the Parse.User from a Facebook account. 227 | * 228 | * @method unlink 229 | * @param {Parse.User} user User to unlink from Facebook. This must be the 230 | * current user. 231 | * @param {Object} options Standard options object with success and error 232 | * callbacks. 233 | */ 234 | unlink: function(user, options) { 235 | if (!initialized) { 236 | throw new Error( 237 | 'You must initialize FacebookUtils before calling unlink.' 238 | ); 239 | } 240 | return user._unlinkFrom('facebook', options); 241 | } 242 | }; 243 | 244 | export default FacebookUtils; 245 | -------------------------------------------------------------------------------- /src/InstallationController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import ParsePromise from './ParsePromise'; 14 | import Storage from './Storage'; 15 | 16 | var iidCache = null; 17 | 18 | function hexOctet() { 19 | return Math.floor( 20 | (1 + Math.random()) * 0x10000 21 | ).toString(16).substring(1); 22 | } 23 | 24 | function generateId() { 25 | return ( 26 | hexOctet() + hexOctet() + '-' + 27 | hexOctet() + '-' + 28 | hexOctet() + '-' + 29 | hexOctet() + '-' + 30 | hexOctet() + hexOctet() + hexOctet() 31 | ); 32 | } 33 | 34 | var InstallationController = { 35 | currentInstallationId(): ParsePromise { 36 | if (typeof iidCache === 'string') { 37 | return ParsePromise.as(iidCache); 38 | } 39 | var path = Storage.generatePath('installationId'); 40 | return Storage.getItemAsync(path).then((iid) => { 41 | if (!iid) { 42 | iid = generateId(); 43 | return Storage.setItemAsync(path, iid).then(() => { 44 | iidCache = iid; 45 | return iid; 46 | }); 47 | } 48 | iidCache = iid; 49 | return iid; 50 | }); 51 | }, 52 | 53 | _clearCache() { 54 | iidCache = null; 55 | }, 56 | 57 | _setInstallationIdCache(iid: string) { 58 | iidCache = iid; 59 | } 60 | }; 61 | 62 | module.exports = InstallationController; 63 | -------------------------------------------------------------------------------- /src/ObjectState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParsePromise from './ParsePromise'; 13 | import TaskQueue from './TaskQueue'; 14 | import { RelationOp } from './ParseOp'; 15 | 16 | import * as Store from './ReduxStore'; 17 | import { ObjectActions as Actions } from './ReduxActionCreators'; 18 | 19 | import type { Op } from './ParseOp'; 20 | 21 | export type AttributeMap = { [attr: string]: any }; 22 | export type OpsMap = { [attr: string]: Op }; 23 | export type ObjectCache = { [attr: string]: string }; 24 | 25 | type State = { 26 | serverData: AttributeMap; 27 | pendingOps: Array; 28 | objectCache: ObjectCache; 29 | // tasks: TaskQueue; 30 | existed: boolean 31 | }; 32 | 33 | export function getState(className: string, id: string): ?State { 34 | var objectState = Store.getState().Parse.Object; 35 | var classData = objectState[className]; 36 | if (classData) { 37 | return classData[id] || null; 38 | } 39 | return null; 40 | } 41 | 42 | export function initializeState(className: string, id: string, initial?: State): State { 43 | var state = getState(className, id); 44 | if (state) { 45 | return state; 46 | } 47 | 48 | Store.dispatch(Actions.initializeState({className, id, initial})); 49 | return getState(...arguments); 50 | } 51 | 52 | export function removeState(className: string, id: string): ?State { 53 | var state = getState(className, id); 54 | if (state === null) { 55 | return null; 56 | } 57 | 58 | Store.dispatch(Actions.removeState({className, id})); 59 | return state; 60 | } 61 | 62 | export function getServerData(className: string, id: string): AttributeMap { 63 | var state = getState(className, id); 64 | if (state) { 65 | return state.serverData; 66 | } 67 | return {}; 68 | } 69 | 70 | export function setServerData(className: string, id: string, attributes: AttributeMap) { 71 | initializeState(className, id).serverData; 72 | Store.dispatch(Actions.setServerData({className, id, attributes})); 73 | return getState(...arguments); 74 | } 75 | 76 | export function getPendingOps(className: string, id: string): Array { 77 | var state = getState(className, id); 78 | if (state) { 79 | return state.pendingOps; 80 | } 81 | return [{}]; 82 | } 83 | 84 | export function setPendingOp(className: string, id: string, attr: string, op: ?Op) { 85 | initializeState(className, id); 86 | Store.dispatch(Actions.setPendingOp({className, id, attr, op})); 87 | } 88 | 89 | export function clearPendingOps(className: string, id: string) { 90 | initializeState(className, id); 91 | Store.dispatch(Actions.clearPendingOps({className, id})); 92 | } 93 | 94 | export function pushPendingState(className: string, id: string) { 95 | initializeState(className, id); 96 | Store.dispatch(Actions.pushPendingState({className, id})); 97 | } 98 | 99 | export function popPendingState(className: string, id: string): OpsMap { 100 | var first = initializeState(className, id).pendingOps[0]; 101 | Store.dispatch(Actions.popPendingState({className, id})); 102 | return first; 103 | } 104 | 105 | export function mergeFirstPendingState(className: string, id: string) { 106 | Store.dispatch(Actions.mergeFirstPendingState({className, id})); 107 | } 108 | 109 | export function getObjectCache(className: string, id: string): ObjectCache { 110 | var state = getState(className, id); 111 | if (state) { 112 | return state.objectCache; 113 | } 114 | return {}; 115 | } 116 | 117 | export function estimateAttribute(className: string, id: string, attr: string): mixed { 118 | var serverData = getServerData(className, id); 119 | var value = serverData[attr]; 120 | var pending = getPendingOps(className, id); 121 | for (var i = 0; i < pending.length; i++) { 122 | if (pending[i][attr]) { 123 | if (pending[i][attr] instanceof RelationOp) { 124 | value = pending[i][attr].applyTo( 125 | value, 126 | { className: className, id: id }, 127 | attr 128 | ); 129 | } else { 130 | value = pending[i][attr].applyTo(value); 131 | } 132 | } 133 | } 134 | return value; 135 | } 136 | 137 | export function estimateAttributes(className: string, id: string): AttributeMap { 138 | var data = {}; 139 | var attr; 140 | var serverData = getServerData(className, id); 141 | for (attr in serverData) { 142 | data[attr] = serverData[attr]; 143 | } 144 | var pending = getPendingOps(className, id); 145 | for (var i = 0; i < pending.length; i++) { 146 | for (attr in pending[i]) { 147 | if (pending[i][attr] instanceof RelationOp) { 148 | data[attr] = pending[i][attr].applyTo( 149 | data[attr], 150 | { className: className, id: id }, 151 | attr 152 | ); 153 | } else { 154 | data[attr] = pending[i][attr].applyTo(data[attr]); 155 | } 156 | } 157 | } 158 | return data; 159 | } 160 | 161 | export function commitServerChanges(className: string, id: string, changes: AttributeMap) { 162 | initializeState(className, id); 163 | Store.dispatch(Actions.commitServerChanges({className, id, changes})); 164 | } 165 | 166 | var QueueMap = {}; 167 | export function enqueueTask(className: string, id: string, task: () => ParsePromise) { 168 | initializeState(className, id); 169 | if (!QueueMap[className]) 170 | QueueMap[className] = {}; 171 | if (!QueueMap[className][id]) 172 | QueueMap[className][id] = new TaskQueue(); 173 | 174 | return QueueMap[className][id].enqueue(task); 175 | } 176 | 177 | export function _clearAllState() { 178 | Store.dispatch(Actions._clearAllState()); 179 | } 180 | 181 | export function _setExisted(className: string, id: string, existed: boolean) { 182 | Store.dispatch(Actions._setExisted({className, id, existed})); 183 | } 184 | -------------------------------------------------------------------------------- /src/Parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import decode from './decode'; 11 | import encode from './encode'; 12 | import CoreManager from './CoreManager'; 13 | import InstallationController from './InstallationController'; 14 | import * as ParseOp from './ParseOp'; 15 | import RESTController from './RESTController'; 16 | 17 | import * as Store from './ReduxStore'; 18 | import { default as parseReducer, generateReducers} from './ReduxReducers' 19 | import { generateActions } from './ReduxActionCreators' 20 | 21 | /** 22 | * Contains all Parse API classes and functions. 23 | * @class Parse 24 | * @static 25 | */ 26 | var Parse = { 27 | /** 28 | * Call this method first to set up your authentication tokens for Parse. 29 | * You can get your keys from the Data Browser on parse.com. 30 | * @method initialize 31 | * @param {String} applicationId Your Parse Application ID. 32 | * @param {String} javaScriptKey Your Parse JavaScript Key. 33 | * @param {String} masterKey (optional) Your Parse Master Key. (Node.js only!) 34 | * @static 35 | */ 36 | initialize(applicationId: string, javaScriptKey: string) { 37 | if (process.env.PARSE_BUILD === 'browser' && CoreManager.get('IS_NODE')) { 38 | console.log( 39 | 'It looks like you\'re using the browser version of the SDK in a ' + 40 | 'node.js environment. You should require(\'parse/node\') instead.' 41 | ); 42 | } 43 | Parse._initialize(applicationId, javaScriptKey); 44 | }, 45 | 46 | _initialize(applicationId: string, javaScriptKey: string, masterKey: string) { 47 | CoreManager.set('APPLICATION_ID', applicationId); 48 | CoreManager.set('JAVASCRIPT_KEY', javaScriptKey); 49 | CoreManager.set('MASTER_KEY', masterKey); 50 | CoreManager.set('USE_MASTER_KEY', false); 51 | } 52 | }; 53 | 54 | /** These legacy setters may eventually be deprecated **/ 55 | Object.defineProperty(Parse, 'applicationId', { 56 | get() { 57 | return CoreManager.get('APPLICATION_ID'); 58 | }, 59 | set(value) { 60 | CoreManager.set('APPLICATION_ID', value); 61 | } 62 | }); 63 | Object.defineProperty(Parse, 'javaScriptKey', { 64 | get() { 65 | return CoreManager.get('JAVASCRIPT_KEY'); 66 | }, 67 | set(value) { 68 | CoreManager.set('JAVASCRIPT_KEY', value); 69 | } 70 | }); 71 | Object.defineProperty(Parse, 'masterKey', { 72 | get() { 73 | return CoreManager.get('MASTER_KEY'); 74 | }, 75 | set(value) { 76 | CoreManager.set('MASTER_KEY', value); 77 | } 78 | }); 79 | Object.defineProperty(Parse, 'serverURL', { 80 | get() { 81 | return CoreManager.get('SERVER_URL'); 82 | }, 83 | set(value) { 84 | CoreManager.set('SERVER_URL', value); 85 | } 86 | }); 87 | /** End setters **/ 88 | 89 | Parse.ACL = require('./ParseACL'); 90 | Parse.Analytics = require('./Analytics'); 91 | Parse.Cloud = require('./Cloud'); 92 | Parse.CoreManager = require('./CoreManager'); 93 | Parse.Config = require('./ParseConfig'); 94 | Parse.Error = require('./ParseError'); 95 | Parse.FacebookUtils = require('./FacebookUtils'); 96 | Parse.File = require('./ParseFile'); 97 | Parse.GeoPoint = require('./ParseGeoPoint'); 98 | Parse.Installation = require('./ParseInstallation'); 99 | Parse.Object = require('./ParseObject'); 100 | Parse.Op = { 101 | Set: ParseOp.SetOp, 102 | Unset: ParseOp.UnsetOp, 103 | Increment: ParseOp.IncrementOp, 104 | Add: ParseOp.AddOp, 105 | Remove: ParseOp.RemoveOp, 106 | AddUnique: ParseOp.AddUniqueOp, 107 | Relation: ParseOp.RelationOp 108 | }; 109 | Parse.Promise = require('./ParsePromise'); 110 | Parse.Push = require('./Push'); 111 | Parse.Query = require('./ParseQuery'); 112 | Parse.Relation = require('./ParseRelation'); 113 | Parse.Role = require('./ParseRole'); 114 | Parse.Session = require('./ParseSession'); 115 | Parse.Storage = require('./Storage'); 116 | Parse.User = require('./ParseUser'); 117 | 118 | Parse._request = function(...args) { 119 | return CoreManager.getRESTController().request.apply(null, args); 120 | }; 121 | Parse._ajax = function(...args) { 122 | return CoreManager.getRESTController().ajax.apply(null, args); 123 | }; 124 | // We attempt to match the signatures of the legacy versions of these methods 125 | Parse._decode = function(_, value) { 126 | return decode(value); 127 | } 128 | Parse._encode = function(value, _, disallowObjects) { 129 | return encode(value, disallowObjects); 130 | } 131 | Parse._getInstallationId = function() { 132 | return CoreManager.getInstallationController().currentInstallationId(); 133 | } 134 | 135 | // redux 136 | Parse.setStore = function() { 137 | Store.set(...arguments); 138 | } 139 | Parse.getReducer = function() { 140 | return parseReducer; 141 | } 142 | Parse.ReduxHelpers = { 143 | generateActions, 144 | generateReducers 145 | } 146 | 147 | CoreManager.setInstallationController(InstallationController); 148 | CoreManager.setRESTController(RESTController); 149 | 150 | if (process.env.PARSE_BUILD === 'node') { 151 | Parse.initialize = Parse._initialize; 152 | Parse.Cloud = Parse.Cloud || {}; 153 | Parse.Cloud.useMasterKey = function() { 154 | CoreManager.set('USE_MASTER_KEY', true); 155 | } 156 | } 157 | 158 | // For legacy requires, of the form `var Parse = require('parse').Parse` 159 | Parse.Parse = Parse; 160 | 161 | module.exports = Parse; 162 | -------------------------------------------------------------------------------- /src/ParseConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import decode from './decode'; 14 | import encode from './encode'; 15 | import escape from './escape'; 16 | import ParseError from './ParseError'; 17 | import ParsePromise from './ParsePromise'; 18 | import Storage from './Storage'; 19 | 20 | /** 21 | * Parse.Config is a local representation of configuration data that 22 | * can be set from the Parse dashboard. 23 | * 24 | * @class Parse.Config 25 | * @constructor 26 | */ 27 | 28 | export default class ParseConfig { 29 | attributes: { [key: string]: any }; 30 | _escapedAttributes: { [key: string]: any }; 31 | 32 | constructor() { 33 | this.attributes = {}; 34 | this._escapedAttributes = {}; 35 | } 36 | 37 | /** 38 | * Gets the value of an attribute. 39 | * @method get 40 | * @param {String} attr The name of an attribute. 41 | */ 42 | get(attr: string): any { 43 | return this.attributes[attr]; 44 | } 45 | 46 | /** 47 | * Gets the HTML-escaped value of an attribute. 48 | * @method escape 49 | * @param {String} attr The name of an attribute. 50 | */ 51 | escape(attr: string) { 52 | var html = this._escapedAttributes[attr]; 53 | if (html) { 54 | return html; 55 | } 56 | var val = this.attributes[attr]; 57 | var escaped = ''; 58 | if (val != null) { 59 | escaped = escape(val.toString()); 60 | } 61 | this._escapedAttributes[attr] = escaped; 62 | return escaped; 63 | } 64 | 65 | /** 66 | * Retrieves the most recently-fetched configuration object, either from 67 | * memory or from local storage if necessary. 68 | * 69 | * @method current 70 | * @static 71 | * @return {Config} The most recently-fetched Parse.Config if it 72 | * exists, else an empty Parse.Config. 73 | */ 74 | static current() { 75 | var controller = CoreManager.getConfigController(); 76 | return controller.current(); 77 | } 78 | 79 | /** 80 | * Gets a new configuration object from the server. 81 | * @method get 82 | * @static 83 | * @param {Object} options A Backbone-style options object. 84 | * Valid options are:
    85 | *
  • success: Function to call when the get completes successfully. 86 | *
  • error: Function to call when the get fails. 87 | *
88 | * @return {Parse.Promise} A promise that is resolved with a newly-created 89 | * configuration object when the get completes. 90 | */ 91 | static get(options) { 92 | options = options || {}; 93 | 94 | var controller = CoreManager.getConfigController(); 95 | return controller.get()._thenRunCallbacks(options); 96 | } 97 | } 98 | 99 | var currentConfig = null; 100 | 101 | var CURRENT_CONFIG_KEY = 'currentConfig'; 102 | 103 | function decodePayload(data) { 104 | try { 105 | var json = JSON.parse(data); 106 | if (json && typeof json === 'object') { 107 | return decode(json); 108 | } 109 | } catch(e) { 110 | return null; 111 | } 112 | } 113 | 114 | var DefaultController = { 115 | current() { 116 | if (currentConfig) { 117 | return currentConfig; 118 | } 119 | 120 | var config = new ParseConfig(); 121 | var storagePath = Storage.generatePath(CURRENT_CONFIG_KEY); 122 | var configData; 123 | if (!Storage.async()) { 124 | configData = Storage.getItem(storagePath); 125 | 126 | if (configData) { 127 | var attributes = decodePayload(configData); 128 | if (attributes) { 129 | config.attributes = attributes; 130 | currentConfig = config; 131 | } 132 | } 133 | return config; 134 | } 135 | // Return a promise for async storage controllers 136 | return Storage.getItemAsync(storagePath).then((configData) => { 137 | if (configData) { 138 | var attributes = decodePayload(configData); 139 | if (attributes) { 140 | config.attributes = attributes; 141 | currentConfig = config; 142 | } 143 | } 144 | return config; 145 | }); 146 | }, 147 | 148 | get() { 149 | var RESTController = CoreManager.getRESTController(); 150 | 151 | return RESTController.request( 152 | 'GET', 'config', {}, {} 153 | ).then((response) => { 154 | if (!response || !response.params) { 155 | var error = new ParseError( 156 | ParseError.INVALID_JSON, 157 | 'Config JSON response invalid.' 158 | ); 159 | return ParsePromise.error(error); 160 | } 161 | 162 | var config = new ParseConfig(); 163 | config.attributes = {}; 164 | for (var attr in response.params) { 165 | config.attributes[attr] = decode(response.params[attr]); 166 | } 167 | currentConfig = config; 168 | return Storage.setItemAsync( 169 | Storage.generatePath(CURRENT_CONFIG_KEY), 170 | JSON.stringify(response.params) 171 | ).then(() => { 172 | return config; 173 | }); 174 | }); 175 | } 176 | }; 177 | 178 | CoreManager.setConfigController(DefaultController); 179 | -------------------------------------------------------------------------------- /src/ParseFile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import ParsePromise from './ParsePromise'; 14 | 15 | type FileData = Array | { base64: string } | File; 16 | export type FileSource = { 17 | format: 'file'; 18 | file: File; 19 | type: string 20 | } | { 21 | format: 'base64'; 22 | base64: string; 23 | type: string 24 | }; 25 | 26 | var dataUriRegexp = 27 | /^data:([a-zA-Z]*\/[a-zA-Z+.-]*);(charset=[a-zA-Z0-9\-\/\s]*,)?base64,(\S+)/; 28 | 29 | function b64Digit(number: number): string { 30 | if (number < 26) { 31 | return String.fromCharCode(65 + number); 32 | } 33 | if (number < 52) { 34 | return String.fromCharCode(97 + (number - 26)); 35 | } 36 | if (number < 62) { 37 | return String.fromCharCode(48 + (number - 52)); 38 | } 39 | if (number === 62) { 40 | return '+'; 41 | } 42 | if (number === 63) { 43 | return '/'; 44 | } 45 | throw new TypeError('Tried to encode large digit ' + number + ' in base64.'); 46 | } 47 | 48 | /** 49 | * A Parse.File is a local representation of a file that is saved to the Parse 50 | * cloud. 51 | * @class Parse.File 52 | * @constructor 53 | * @param name {String} The file's name. This will be prefixed by a unique 54 | * value once the file has finished saving. The file name must begin with 55 | * an alphanumeric character, and consist of alphanumeric characters, 56 | * periods, spaces, underscores, or dashes. 57 | * @param data {Array} The data for the file, as either: 58 | * 1. an Array of byte value Numbers, or 59 | * 2. an Object like { base64: "..." } with a base64-encoded String. 60 | * 3. a File object selected with a file upload control. (3) only works 61 | * in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+. 62 | * For example:
 63 |  * var fileUploadControl = $("#profilePhotoFileUpload")[0];
 64 |  * if (fileUploadControl.files.length > 0) {
 65 |  *   var file = fileUploadControl.files[0];
 66 |  *   var name = "photo.jpg";
 67 |  *   var parseFile = new Parse.File(name, file);
 68 |  *   parseFile.save().then(function() {
 69 |  *     // The file has been saved to Parse.
 70 |  *   }, function(error) {
 71 |  *     // The file either could not be read, or could not be saved to Parse.
 72 |  *   });
 73 |  * }
74 | * @param type {String} Optional Content-Type header to use for the file. If 75 | * this is omitted, the content type will be inferred from the name's 76 | * extension. 77 | */ 78 | export default class ParseFile { 79 | _name: string; 80 | _url: ?string; 81 | _source: FileSource; 82 | _previousSave: ?ParsePromise; 83 | 84 | constructor(name: string, data?: FileData, type?: string) { 85 | var specifiedType = type || ''; 86 | 87 | this._name = name; 88 | 89 | if (Array.isArray(data)) { 90 | this._source = { 91 | format: 'base64', 92 | base64: ParseFile.encodeBase64(data), 93 | type: specifiedType 94 | }; 95 | } else if (typeof File !== 'undefined' && data instanceof File) { 96 | this._source = { 97 | format: 'file', 98 | file: data, 99 | type: specifiedType 100 | }; 101 | } else if (data && data.hasOwnProperty('base64')) { 102 | var matches = dataUriRegexp.exec(data.base64); 103 | if (matches && matches.length > 0) { 104 | // if data URI with type and charset, there will be 4 matches. 105 | this._source = { 106 | format: 'base64', 107 | base64: matches.length === 4 ? matches[3] : matches[2], 108 | type: matches[1] 109 | }; 110 | } else { 111 | this._source = { 112 | format: 'base64', 113 | base64: data.base64, 114 | type: specifiedType 115 | }; 116 | } 117 | } else if (typeof data !== 'undefined') { 118 | throw new TypeError('Cannot create a Parse.File with that data.'); 119 | } 120 | } 121 | 122 | /** 123 | * Gets the name of the file. Before save is called, this is the filename 124 | * given by the user. After save is called, that name gets prefixed with a 125 | * unique identifier. 126 | * @method name 127 | * @return {String} 128 | */ 129 | name(): string { 130 | return this._name; 131 | } 132 | 133 | /** 134 | * Gets the url of the file. It is only available after you save the file or 135 | * after you get the file from a Parse.Object. 136 | * @method url 137 | * @return {String} 138 | */ 139 | url(): ?string { 140 | return this._url; 141 | } 142 | 143 | /** 144 | * Saves the file to the Parse cloud. 145 | * @method save 146 | * @param {Object} options A Backbone-style options object. 147 | * @return {Parse.Promise} Promise that is resolved when the save finishes. 148 | */ 149 | save(options?: { success?: any, error?: any }) { 150 | options = options || {}; 151 | var controller = CoreManager.getFileController(); 152 | if (!this._previousSave) { 153 | if (this._source.format === 'file') { 154 | this._previousSave = controller.saveFile(this._name, this._source).then((res) => { 155 | this._name = res.name; 156 | this._url = res.url; 157 | return this; 158 | }); 159 | } else { 160 | this._previousSave = controller.saveBase64(this._name, this._source).then((res) => { 161 | this._name = res.name; 162 | this._url = res.url; 163 | return this; 164 | }); 165 | } 166 | } 167 | if (this._previousSave) { 168 | return this._previousSave._thenRunCallbacks(options); 169 | } 170 | } 171 | 172 | toJSON(): { name: ?string, url: ?string } { 173 | return { 174 | __type: 'File', 175 | name: this._name, 176 | url: this._url 177 | }; 178 | } 179 | 180 | equals(other: mixed): boolean { 181 | if (this === other) { 182 | return true; 183 | } 184 | // Unsaved Files are never equal, since they will be saved to different URLs 185 | return ( 186 | (other instanceof ParseFile) && 187 | this.name() === other.name() && 188 | this.url() === other.url() && 189 | typeof this.url() !== 'undefined' 190 | ); 191 | } 192 | 193 | static fromJSON(obj): ParseFile { 194 | if (obj.__type !== 'File') { 195 | throw new TypeError('JSON object does not represent a ParseFile'); 196 | } 197 | var file = new ParseFile(obj.name); 198 | file._url = obj.url; 199 | return file; 200 | } 201 | 202 | static encodeBase64(bytes: Array): string { 203 | var chunks = []; 204 | chunks.length = Math.ceil(bytes.length / 3); 205 | for (var i = 0; i < chunks.length; i++) { 206 | var b1 = bytes[i * 3]; 207 | var b2 = bytes[i * 3 + 1] || 0; 208 | var b3 = bytes[i * 3 + 2] || 0; 209 | 210 | var has2 = (i * 3 + 1) < bytes.length; 211 | var has3 = (i * 3 + 2) < bytes.length; 212 | 213 | chunks[i] = [ 214 | b64Digit((b1 >> 2) & 0x3F), 215 | b64Digit(((b1 << 4) & 0x30) | ((b2 >> 4) & 0x0F)), 216 | has2 ? b64Digit(((b2 << 2) & 0x3C) | ((b3 >> 6) & 0x03)) : '=', 217 | has3 ? b64Digit(b3 & 0x3F) : '=' 218 | ].join(''); 219 | } 220 | 221 | return chunks.join(''); 222 | } 223 | } 224 | 225 | var DefaultController = { 226 | saveFile: function(name: string, source: FileSource) { 227 | if (source.format !== 'file') { 228 | throw new Error('saveFile can only be used with File-type sources.'); 229 | } 230 | // To directly upload a File, we use a REST-style AJAX request 231 | var headers = { 232 | 'X-Parse-Application-ID': CoreManager.get('APPLICATION_ID'), 233 | 'X-Parse-JavaScript-Key': CoreManager.get('JAVASCRIPT_KEY') 234 | }; 235 | var url = CoreManager.get('SERVER_URL'); 236 | if (url[url.length - 1] !== '/') { 237 | url += '/'; 238 | } 239 | url += 'files/' + name; 240 | return CoreManager.getRESTController().ajax('POST', url, source.file, headers); 241 | }, 242 | 243 | saveBase64: function(name: string, source: FileSource) { 244 | if (source.format !== 'base64') { 245 | throw new Error('saveBase64 can only be used with Base64-type sources.'); 246 | } 247 | var data: { base64: any; _ContentType?: any } = { 248 | base64: source.base64 249 | }; 250 | if (source.type) { 251 | data._ContentType = source.type; 252 | } 253 | var path = 'files/' + name; 254 | return CoreManager.getRESTController().request('POST', path, data); 255 | } 256 | }; 257 | 258 | CoreManager.setFileController(DefaultController); 259 | -------------------------------------------------------------------------------- /src/ParseGeoPoint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParsePromise from './ParsePromise'; 13 | 14 | /** 15 | * Creates a new GeoPoint with any of the following forms:
16 | *
 17 |  *   new GeoPoint(otherGeoPoint)
 18 |  *   new GeoPoint(30, 30)
 19 |  *   new GeoPoint([30, 30])
 20 |  *   new GeoPoint({latitude: 30, longitude: 30})
 21 |  *   new GeoPoint()  // defaults to (0, 0)
 22 |  *   
23 | * @class Parse.GeoPoint 24 | * @constructor 25 | * 26 | *

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

29 | * 30 | *

Only one key in a class may contain a GeoPoint.

31 | * 32 | *

Example:

 33 |  *   var point = new Parse.GeoPoint(30.0, -20.0);
 34 |  *   var object = new Parse.Object("PlaceObject");
 35 |  *   object.set("location", point);
 36 |  *   object.save();

37 | */ 38 | export default class ParseGeoPoint { 39 | _latitude: number; 40 | _longitude: number; 41 | 42 | constructor( 43 | arg1: Array | 44 | { latitude: number; longitude: number } | 45 | number, arg2?: number 46 | ) { 47 | if (Array.isArray(arg1)) { 48 | ParseGeoPoint._validate(arg1[0], arg1[1]); 49 | this._latitude = arg1[0]; 50 | this._longitude = arg1[1]; 51 | } else if (typeof arg1 === 'object') { 52 | ParseGeoPoint._validate(arg1.latitude, arg1.longitude); 53 | this._latitude = arg1.latitude; 54 | this._longitude = arg1.longitude; 55 | } else if (typeof arg1 === 'number' && typeof arg2 === 'number') { 56 | ParseGeoPoint._validate(arg1, arg2); 57 | this._latitude = arg1; 58 | this._longitude = arg2; 59 | } else { 60 | this._latitude = 0; 61 | this._longitude = 0; 62 | } 63 | } 64 | 65 | /** 66 | * North-south portion of the coordinate, in range [-90, 90]. 67 | * Throws an exception if set out of range in a modern browser. 68 | * @property latitude 69 | * @type Number 70 | */ 71 | get latitude(): number { 72 | return this._latitude; 73 | } 74 | 75 | set latitude(val: number) { 76 | ParseGeoPoint._validate(val, this.longitude); 77 | this._latitude = val; 78 | } 79 | 80 | /** 81 | * East-west portion of the coordinate, in range [-180, 180]. 82 | * Throws if set out of range in a modern browser. 83 | * @property longitude 84 | * @type Number 85 | */ 86 | get longitude(): number { 87 | return this._longitude; 88 | } 89 | 90 | set longitude(val: number) { 91 | ParseGeoPoint._validate(this.latitude, val); 92 | this._longitude = val; 93 | } 94 | 95 | /** 96 | * Returns a JSON representation of the GeoPoint, suitable for Parse. 97 | * @method toJSON 98 | * @return {Object} 99 | */ 100 | toJSON(): { __type: string; latitude: number; longitude: number } { 101 | ParseGeoPoint._validate(this._latitude, this._longitude); 102 | return { 103 | __type: 'GeoPoint', 104 | latitude: this._latitude, 105 | longitude: this._longitude 106 | }; 107 | } 108 | 109 | equals(other: mixed): boolean { 110 | return ( 111 | (other instanceof ParseGeoPoint) && 112 | this.latitude === other.latitude && 113 | this.longitude === other.longitude 114 | ); 115 | } 116 | 117 | /** 118 | * Returns the distance from this GeoPoint to another in radians. 119 | * @method radiansTo 120 | * @param {Parse.GeoPoint} point the other Parse.GeoPoint. 121 | * @return {Number} 122 | */ 123 | radiansTo(point: ParseGeoPoint): number { 124 | var d2r = Math.PI / 180.0; 125 | var lat1rad = this.latitude * d2r; 126 | var long1rad = this.longitude * d2r; 127 | var lat2rad = point.latitude * d2r; 128 | var long2rad = point.longitude * d2r; 129 | var deltaLat = lat1rad - lat2rad; 130 | var deltaLong = long1rad - long2rad; 131 | var sinDeltaLatDiv2 = Math.sin(deltaLat / 2); 132 | var sinDeltaLongDiv2 = Math.sin(deltaLong / 2); 133 | // Square of half the straight line chord distance between both points. 134 | var a = ((sinDeltaLatDiv2 * sinDeltaLatDiv2) + 135 | (Math.cos(lat1rad) * Math.cos(lat2rad) * 136 | sinDeltaLongDiv2 * sinDeltaLongDiv2)); 137 | a = Math.min(1.0, a); 138 | return 2 * Math.asin(Math.sqrt(a)); 139 | } 140 | 141 | /** 142 | * Returns the distance from this GeoPoint to another in kilometers. 143 | * @method kilometersTo 144 | * @param {Parse.GeoPoint} point the other Parse.GeoPoint. 145 | * @return {Number} 146 | */ 147 | kilometersTo(point: ParseGeoPoint): number { 148 | return this.radiansTo(point) * 6371.0; 149 | } 150 | 151 | /** 152 | * Returns the distance from this GeoPoint to another in miles. 153 | * @method milesTo 154 | * @param {Parse.GeoPoint} point the other Parse.GeoPoint. 155 | * @return {Number} 156 | */ 157 | milesTo(point: ParseGeoPoint): number { 158 | return this.radiansTo(point) * 3958.8; 159 | } 160 | 161 | /** 162 | * Throws an exception if the given lat-long is out of bounds. 163 | */ 164 | static _validate(latitude: number, longitude: number) { 165 | if (latitude !== latitude || longitude !== longitude) { 166 | throw new TypeError( 167 | 'GeoPoint latitude and longitude must be valid numbers' 168 | ); 169 | } 170 | if (latitude < -90.0) { 171 | throw new TypeError( 172 | 'GeoPoint latitude out of bounds: ' + latitude + ' < -90.0.' 173 | ); 174 | } 175 | if (latitude > 90.0) { 176 | throw new TypeError( 177 | 'GeoPoint latitude out of bounds: ' + latitude + ' > 90.0.' 178 | ); 179 | } 180 | if (longitude < -180.0) { 181 | throw new TypeError( 182 | 'GeoPoint longitude out of bounds: ' + longitude + ' < -180.0.' 183 | ); 184 | } 185 | if (longitude > 180.0) { 186 | throw new TypeError( 187 | 'GeoPoint longitude out of bounds: ' + longitude + ' > 180.0.' 188 | ); 189 | } 190 | } 191 | 192 | /** 193 | * Creates a GeoPoint with the user's current location, if available. 194 | * Calls options.success with a new GeoPoint instance or calls options.error. 195 | * @method current 196 | * @param {Object} options An object with success and error callbacks. 197 | * @static 198 | */ 199 | static current(options) { 200 | var promise = new ParsePromise(); 201 | navigator.geolocation.getCurrentPosition(function(location) { 202 | promise.resolve( 203 | new ParseGeoPoint(location.coords.latitude, location.coords.longitude) 204 | ); 205 | }, function(error) { 206 | promise.reject(error); 207 | }); 208 | 209 | return promise._thenRunCallbacks(options); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/ParseInstallation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseObject from './ParseObject'; 13 | 14 | import type { AttributeMap } from './ObjectState'; 15 | 16 | export default class Installation extends ParseObject { 17 | constructor(attributes: ?AttributeMap) { 18 | super('_Installation'); 19 | if (attributes && typeof attributes === 'object'){ 20 | if (!this.set(attributes || {})) { 21 | throw new Error('Can\'t create an invalid Session'); 22 | } 23 | } 24 | } 25 | } 26 | 27 | ParseObject.registerSubclass('_Installation', Installation); 28 | -------------------------------------------------------------------------------- /src/ParseRelation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import { RelationOp } from './ParseOp'; 13 | import ParseObject from './ParseObject'; 14 | import ParseQuery from './ParseQuery'; 15 | 16 | /** 17 | * Creates a new Relation for the given parent object and key. This 18 | * constructor should rarely be used directly, but rather created by 19 | * Parse.Object.relation. 20 | * @class Parse.Relation 21 | * @constructor 22 | * @param {Parse.Object} parent The parent of this relation. 23 | * @param {String} key The key for this relation on the parent. 24 | * 25 | *

26 | * A class that is used to access all of the children of a many-to-many 27 | * relationship. Each instance of Parse.Relation is associated with a 28 | * particular parent object and key. 29 | *

30 | */ 31 | export default class ParseRelation { 32 | parent: ParseObject; 33 | key: ?string; 34 | targetClassName: ?string; 35 | 36 | constructor(parent: ParseObject, key: ?string) { 37 | this.parent = parent; 38 | this.key = key; 39 | this.targetClassName = null; 40 | } 41 | 42 | /** 43 | * Makes sure that this relation has the right parent and key. 44 | */ 45 | _ensureParentAndKey(parent: ParseObject, key: string) { 46 | this.key = this.key || key; 47 | if (this.key !== key) { 48 | throw new Error( 49 | 'Internal Error. Relation retrieved from two different keys.' 50 | ); 51 | } 52 | if (this.parent) { 53 | if (this.parent.className !== parent.className) { 54 | throw new Error( 55 | 'Internal Error. Relation retrieved from two different Objects.' 56 | ); 57 | } 58 | if (this.parent.id) { 59 | if (this.parent.id !== parent.id) { 60 | throw new Error( 61 | 'Internal Error. Relation retrieved from two different Objects.' 62 | ); 63 | } 64 | } else if (parent.id) { 65 | this.parent = parent; 66 | } 67 | } else { 68 | this.parent = parent; 69 | } 70 | } 71 | 72 | /** 73 | * Adds a Parse.Object or an array of Parse.Objects to the relation. 74 | * @method add 75 | * @param {} objects The item or items to add. 76 | */ 77 | add(objects: ParseObject | Array): ParseObject { 78 | if (!Array.isArray(objects)) { 79 | objects = [objects]; 80 | } 81 | 82 | var change = new RelationOp(objects, []); 83 | this.parent.set(this.key, change); 84 | this.targetClassName = change._targetClassName; 85 | return this.parent; 86 | } 87 | 88 | /** 89 | * Removes a Parse.Object or an array of Parse.Objects from this relation. 90 | * @method remove 91 | * @param {} objects The item or items to remove. 92 | */ 93 | remove(objects: ParseObject | Array) { 94 | if (!Array.isArray(objects)) { 95 | objects = [objects]; 96 | } 97 | 98 | var change = new RelationOp([], objects); 99 | this.parent.set(this.key, change); 100 | this.targetClassName = change._targetClassName; 101 | } 102 | 103 | /** 104 | * Returns a JSON version of the object suitable for saving to disk. 105 | * @method toJSON 106 | * @return {Object} 107 | */ 108 | toJSON(): { __type: 'Relation', className: ?string } { 109 | return { 110 | __type: 'Relation', 111 | className: this.targetClassName 112 | }; 113 | } 114 | 115 | /** 116 | * Returns a Parse.Query that is limited to objects in this 117 | * relation. 118 | * @method query 119 | * @return {Parse.Query} 120 | */ 121 | query(): ParseQuery { 122 | var query; 123 | if (!this.targetClassName) { 124 | query = new ParseQuery(this.parent.className); 125 | query._extraOptions.redirectClassNameForKey = this.key; 126 | } else { 127 | query = new ParseQuery(this.targetClassName) 128 | } 129 | query._addCondition('$relatedTo', 'object', { 130 | __type: 'Pointer', 131 | className: this.parent.className, 132 | objectId: this.parent.id 133 | }); 134 | query._addCondition('$relatedTo', 'key', this.key); 135 | 136 | return query; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/ParseRole.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseACL from './ParseACL'; 13 | import ParseError from './ParseError'; 14 | import ParseObject from './ParseObject'; 15 | 16 | import type { AttributeMap } from './ObjectState'; 17 | import type ParseRelation from './ParseRelation'; 18 | 19 | /** 20 | * Represents a Role on the Parse server. Roles represent groupings of 21 | * Users for the purposes of granting permissions (e.g. specifying an ACL 22 | * for an Object). Roles are specified by their sets of child users and 23 | * child roles, all of which are granted any permissions that the parent 24 | * role has. 25 | * 26 | *

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

28 | * @class Parse.Role 29 | * @constructor 30 | * @param {String} name The name of the Role to create. 31 | * @param {Parse.ACL} acl The ACL for this role. Roles must have an ACL. 32 | * A Parse.Role is a local representation of a role persisted to the Parse 33 | * cloud. 34 | */ 35 | export default class ParseRole extends ParseObject { 36 | constructor(name: string, acl: ParseACL) { 37 | super('_Role'); 38 | if (typeof name === 'string' && (acl instanceof ParseACL)) { 39 | this.setName(name); 40 | this.setACL(acl); 41 | } 42 | } 43 | 44 | /** 45 | * Gets the name of the role. You can alternatively call role.get("name") 46 | * 47 | * @method getName 48 | * @return {String} the name of the role. 49 | */ 50 | getName(): string { 51 | return this.get('name'); 52 | } 53 | 54 | /** 55 | * Sets the name for a role. This value must be set before the role has 56 | * been saved to the server, and cannot be set once the role has been 57 | * saved. 58 | * 59 | *

60 | * A role's name can only contain alphanumeric characters, _, -, and 61 | * spaces. 62 | *

63 | * 64 | *

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

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

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

82 | * 83 | * @method getUsers 84 | * @return {Parse.Relation} the relation for the users belonging to this 85 | * role. 86 | */ 87 | getUsers(): ParseRelation { 88 | return this.relation('users'); 89 | } 90 | 91 | /** 92 | * Gets the Parse.Relation for the Parse.Roles that are direct 93 | * children of this role. These roles' users are granted any privileges that 94 | * this role has been granted (e.g. read or write access through ACLs). You 95 | * can add or remove child roles from this role through this relation. 96 | * 97 | *

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

98 | * 99 | * @method getRoles 100 | * @return {Parse.Relation} the relation for the roles belonging to this 101 | * role. 102 | */ 103 | getRoles(): ParseRelation { 104 | return this.relation('roles'); 105 | } 106 | 107 | validate(attrs: AttributeMap, options?: mixed) { 108 | var isInvalid = super.validate(attrs, options); 109 | if (isInvalid) { 110 | return isInvalid; 111 | } 112 | 113 | if ('name' in attrs && attrs.name !== this.getName()) { 114 | var newName = attrs.name; 115 | if (this.id && this.id !== attrs.objectId) { 116 | // Check to see if the objectId being set matches this.id 117 | // This happens during a fetch -- the id is set before calling fetch 118 | // Let the name be set in this case 119 | return new ParseError( 120 | ParseError.OTHER_CAUSE, 121 | 'A role\'s name can only be set before it has been saved.' 122 | ); 123 | } 124 | if (typeof newName !== 'string') { 125 | return new ParseError( 126 | ParseError.OTHER_CAUSE, 127 | 'A role\'s name must be a String.' 128 | ); 129 | } 130 | if (!(/^[0-9a-zA-Z\-_ ]+$/).test(newName)) { 131 | return new ParseError( 132 | ParseError.OTHER_CAUSE, 133 | 'A role\'s name can be only contain alphanumeric characters, _, ' + 134 | '-, and spaces.' 135 | ); 136 | } 137 | } 138 | return false; 139 | } 140 | } 141 | 142 | ParseObject.registerSubclass('_Role', ParseRole); 143 | -------------------------------------------------------------------------------- /src/ParseSession.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import isRevocableSession from './isRevocableSession'; 14 | import ParseObject from './ParseObject'; 15 | import ParsePromise from './ParsePromise'; 16 | import ParseUser from './ParseUser'; 17 | 18 | import type { AttributeMap } from './ObjectState'; 19 | import type { RequestOptions, FullOptions } from './RESTController'; 20 | 21 | /** 22 | * @class Parse.Session 23 | * @constructor 24 | * 25 | *

A Parse.Session object is a local representation of a revocable session. 26 | * This class is a subclass of a Parse.Object, and retains the same 27 | * functionality of a Parse.Object.

28 | */ 29 | export default class ParseSession extends ParseObject { 30 | constructor(attributes: ?AttributeMap) { 31 | super('_Session'); 32 | if (attributes && typeof attributes === 'object'){ 33 | if (!this.set(attributes || {})) { 34 | throw new Error('Can\'t create an invalid Session'); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Returns the session token string. 41 | * @method getSessionToken 42 | * @return {String} 43 | */ 44 | getSessionToken(): string { 45 | return this.get('sessionToken'); 46 | } 47 | 48 | static readOnlyAttributes() { 49 | return [ 50 | 'createdWith', 51 | 'expiresAt', 52 | 'installationId', 53 | 'restricted', 54 | 'sessionToken', 55 | 'user' 56 | ]; 57 | } 58 | 59 | /** 60 | * Retrieves the Session object for the currently logged in session. 61 | * @method current 62 | * @static 63 | * @return {Parse.Promise} A promise that is resolved with the Parse.Session 64 | * object after it has been fetched. If there is no current user, the 65 | * promise will be rejected. 66 | */ 67 | static current(options: FullOptions) { 68 | options = options || {}; 69 | var controller = CoreManager.getSessionController(); 70 | 71 | var sessionOptions = {}; 72 | if (options.hasOwnProperty('useMasterKey')) { 73 | sessionOptions.useMasterKey = options.useMasterKey; 74 | } 75 | return ParseUser.currentAsync().then((user) => { 76 | if (!user) { 77 | return ParsePromise.error('There is no current user.'); 78 | } 79 | var token = user.getSessionToken(); 80 | sessionOptions.sessionToken = user.getSessionToken(); 81 | return controller.getSession(sessionOptions); 82 | }); 83 | } 84 | 85 | /** 86 | * Determines whether the current session token is revocable. 87 | * This method is useful for migrating Express.js or Node.js web apps to 88 | * use revocable sessions. If you are migrating an app that uses the Parse 89 | * SDK in the browser only, please use Parse.User.enableRevocableSession() 90 | * instead, so that sessions can be automatically upgraded. 91 | * @method isCurrentSessionRevocable 92 | * @static 93 | * @return {Boolean} 94 | */ 95 | static isCurrentSessionRevocable(): boolean { 96 | var currentUser = ParseUser.current(); 97 | if (currentUser) { 98 | return isRevocableSession(currentUser.getSessionToken() || ''); 99 | } 100 | return false; 101 | } 102 | } 103 | 104 | ParseObject.registerSubclass('_Session', ParseSession); 105 | 106 | var DefaultController = { 107 | getSession(options: RequestOptions): ParsePromise { 108 | var RESTController = CoreManager.getRESTController(); 109 | var session = new ParseSession(); 110 | 111 | return RESTController.request( 112 | 'GET', 'sessions/me', {}, options 113 | ).then((sessionData) => { 114 | session._finishFetch(sessionData); 115 | session._setExisted(true); 116 | return session; 117 | }); 118 | } 119 | }; 120 | 121 | CoreManager.setSessionController(DefaultController); 122 | -------------------------------------------------------------------------------- /src/Push.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import ParseQuery from './ParseQuery'; 14 | 15 | import type ParsePromise from './ParsePromise'; 16 | import type { WhereClause } from './ParseQuery'; 17 | import type { RequestOptions } from './RESTController'; 18 | 19 | export type PushData = { 20 | where?: WhereClause | ParseQuery; 21 | push_time?: Date | string; 22 | expiration_time?: Date | string; 23 | expiration_interval?: number; 24 | }; 25 | 26 | /** 27 | * Contains functions to deal with Push in Parse. 28 | * @class Parse.Push 29 | * @static 30 | */ 31 | 32 | /** 33 | * Sends a push notification. 34 | * @method send 35 | * @param {Object} data - The data of the push notification. Valid fields 36 | * are: 37 | *
    38 | *
  1. channels - An Array of channels to push to.
  2. 39 | *
  3. push_time - A Date object for when to send the push.
  4. 40 | *
  5. expiration_time - A Date object for when to expire 41 | * the push.
  6. 42 | *
  7. expiration_interval - The seconds from now to expire the push.
  8. 43 | *
  9. where - A Parse.Query over Parse.Installation that is used to match 44 | * a set of installations to push to.
  10. 45 | *
  11. data - The data to send as part of the push
  12. 46 | *
      47 | * @param {Object} options An object that has an optional success function, 48 | * that takes no arguments and will be called on a successful push, and 49 | * an error function that takes a Parse.Error and will be called if the push 50 | * failed. 51 | * @return {Parse.Promise} A promise that is fulfilled when the push request 52 | * completes. 53 | */ 54 | export function send( 55 | data: PushData, 56 | options?: { useMasterKey?: boolean, success?: any, error?: any } 57 | ): ParsePromise { 58 | options = options || {}; 59 | 60 | if (data.where && data.where instanceof ParseQuery) { 61 | data.where = data.where.toJSON().where; 62 | } 63 | 64 | if (data.push_time && typeof data.push_time === 'object') { 65 | data.push_time = data.push_time.toJSON(); 66 | } 67 | 68 | if (data.expiration_time && typeof data.expiration_time === 'object') { 69 | data.expiration_time = data.expiration_time.toJSON(); 70 | } 71 | 72 | if (data.expiration_time && data.expiration_interval) { 73 | throw new Error( 74 | 'expiration_time and expiration_interval cannot both be set.' 75 | ); 76 | } 77 | 78 | return ( 79 | CoreManager.getPushController().send(data, { 80 | useMasterKey: options.useMasterKey 81 | })._thenRunCallbacks(options) 82 | ); 83 | } 84 | 85 | var DefaultController = { 86 | send(data: PushData, options: RequestOptions) { 87 | var RESTController = CoreManager.getRESTController(); 88 | 89 | var request = RESTController.request( 90 | 'POST', 91 | 'push', 92 | data, 93 | { useMasterKey: !!options.useMasterKey } 94 | ); 95 | 96 | return request._thenRunCallbacks(options); 97 | } 98 | } 99 | 100 | CoreManager.setPushController(DefaultController); 101 | -------------------------------------------------------------------------------- /src/RESTController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import ParseError from './ParseError'; 14 | import ParsePromise from './ParsePromise'; 15 | import Storage from './Storage'; 16 | 17 | export type RequestOptions = { 18 | useMasterKey?: boolean; 19 | sessionToken?: string; 20 | }; 21 | 22 | export type FullOptions = { 23 | success?: any; 24 | error?: any; 25 | useMasterKey?: boolean; 26 | sessionToken?: string; 27 | } 28 | 29 | var XHR = null; 30 | if (typeof XMLHttpRequest !== 'undefined') { 31 | XHR = XMLHttpRequest; 32 | } 33 | if (process.env.PARSE_BUILD === 'node') { 34 | XHR = require('xmlhttprequest').XMLHttpRequest; 35 | } 36 | 37 | var useXDomainRequest = false; 38 | if (typeof XDomainRequest !== 'undefined' && 39 | !('withCredentials' in new XMLHttpRequest())) { 40 | useXDomainRequest = true; 41 | } 42 | 43 | function ajaxIE9(method: string, url: string, data: any) { 44 | var promise = new ParsePromise(); 45 | var xdr = new XDomainRequest(); 46 | xdr.onload = function() { 47 | var response; 48 | try { 49 | response = JSON.parse(xdr.responseText); 50 | } catch (e) { 51 | promise.reject(e); 52 | } 53 | if (response) { 54 | promise.resolve(response); 55 | } 56 | }; 57 | xdr.onerror = xdr.ontimeout = function() { 58 | // Let's fake a real error message. 59 | var fakeResponse = { 60 | responseText: JSON.stringify({ 61 | code: ParseError.X_DOMAIN_REQUEST, 62 | error: 'IE\'s XDomainRequest does not supply error info.' 63 | }) 64 | }; 65 | promise.reject(fakeResponse); 66 | }; 67 | xdr.onprogress = function() { }; 68 | xdr.open(method, url); 69 | xdr.send(data); 70 | return promise; 71 | } 72 | 73 | var RESTController = { 74 | ajax(method: string, url: string, data: any, headers?: any) { 75 | if (useXDomainRequest) { 76 | return ajaxIE9(method, url, data, headers); 77 | } 78 | 79 | var promise = new ParsePromise(); 80 | var attempts = 0; 81 | 82 | var dispatch = function() { 83 | if (XHR == null) { 84 | throw new Error( 85 | 'Cannot make a request: No definition of XMLHttpRequest was found.' 86 | ); 87 | } 88 | var handled = false; 89 | var xhr = new XHR(); 90 | 91 | xhr.onreadystatechange = function() { 92 | if (xhr.readyState !== 4 || handled) { 93 | return; 94 | } 95 | handled = true; 96 | 97 | if (xhr.status >= 200 && xhr.status < 300) { 98 | var response; 99 | try { 100 | response = JSON.parse(xhr.responseText); 101 | } catch (e) { 102 | promise.reject(e.toString()); 103 | } 104 | if (response) { 105 | promise.resolve(response, xhr.status, xhr); 106 | } 107 | } else if (xhr.status >= 500 || xhr.status === 0) { // retry on 5XX or node-xmlhttprequest error 108 | if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) { 109 | // Exponentially-growing random delay 110 | var delay = Math.round( 111 | Math.random() * 125 * Math.pow(2, attempts) 112 | ); 113 | setTimeout(dispatch, delay); 114 | } else if (xhr.status === 0) { 115 | promise.reject('Unable to connect to the Parse API'); 116 | } else { 117 | // After the retry limit is reached, fail 118 | promise.reject(xhr); 119 | } 120 | } else { 121 | promise.reject(xhr); 122 | } 123 | }; 124 | 125 | headers = headers || {}; 126 | headers['Content-Type'] = 'text/plain'; // Avoid pre-flight 127 | if (CoreManager.get('IS_NODE')) { 128 | headers['User-Agent'] = 'Parse/' + CoreManager.get('VERSION') + 129 | ' (NodeJS ' + process.versions.node + ')'; 130 | } 131 | 132 | xhr.open(method, url, true); 133 | for (var h in headers) { 134 | xhr.setRequestHeader(h, headers[h]); 135 | } 136 | xhr.send(data); 137 | } 138 | dispatch(); 139 | 140 | return promise; 141 | }, 142 | 143 | request(method: string, path: string, data: mixed, options?: RequestOptions) { 144 | options = options || {}; 145 | var url = CoreManager.get('SERVER_URL'); 146 | if (url[url.length - 1] !== '/') { 147 | url += '/'; 148 | } 149 | url += path; 150 | 151 | var payload = {}; 152 | if (data && typeof data === 'object') { 153 | for (var k in data) { 154 | payload[k] = data[k]; 155 | } 156 | } 157 | 158 | if (method !== 'POST') { 159 | payload._method = method; 160 | method = 'POST'; 161 | } 162 | 163 | payload._ApplicationId = CoreManager.get('APPLICATION_ID'); 164 | payload._JavaScriptKey = CoreManager.get('JAVASCRIPT_KEY'); 165 | payload._ClientVersion = CoreManager.get('VERSION'); 166 | 167 | var useMasterKey = options.useMasterKey; 168 | if (typeof useMasterKey === 'undefined') { 169 | useMasterKey = CoreManager.get('USE_MASTER_KEY'); 170 | } 171 | if (useMasterKey) { 172 | if (CoreManager.get('MASTER_KEY')) { 173 | delete payload._JavaScriptKey; 174 | payload._MasterKey = CoreManager.get('MASTER_KEY'); 175 | } else { 176 | throw new Error('Cannot use the Master Key, it has not been provided.'); 177 | } 178 | } 179 | 180 | if (CoreManager.get('FORCE_REVOCABLE_SESSION')) { 181 | payload._RevocableSession = '1'; 182 | } 183 | 184 | var installationController = CoreManager.getInstallationController(); 185 | 186 | return installationController.currentInstallationId().then((iid) => { 187 | payload._InstallationId = iid; 188 | var userController = CoreManager.getUserController(); 189 | if (options && typeof options.sessionToken === 'string') { 190 | return ParsePromise.as(options.sessionToken); 191 | } else if (userController) { 192 | return userController.currentUserAsync().then((user) => { 193 | if (user) { 194 | return ParsePromise.as(user.getSessionToken()); 195 | } 196 | return ParsePromise.as(null); 197 | }); 198 | } 199 | return ParsePromise.as(null); 200 | }).then((token) => { 201 | if (token) { 202 | payload._SessionToken = token; 203 | } 204 | 205 | var payloadString = JSON.stringify(payload); 206 | 207 | return RESTController.ajax(method, url, payloadString); 208 | }).then(null, function(response: { responseText: string }) { 209 | // Transform the error into an instance of ParseError by trying to parse 210 | // the error string as JSON 211 | var error; 212 | if (response && response.responseText) { 213 | try { 214 | var errorJSON = JSON.parse(response.responseText); 215 | error = new ParseError(errorJSON.code, errorJSON.error); 216 | } catch (e) { 217 | // If we fail to parse the error text, that's okay. 218 | error = new ParseError( 219 | ParseError.INVALID_JSON, 220 | 'Received an error with invalid JSON from Parse: ' + 221 | response.responseText 222 | ); 223 | } 224 | } else { 225 | error = new ParseError( 226 | ParseError.CONNECTION_FAILED, 227 | 'XMLHttpRequest failed: ' + JSON.stringify(response) 228 | ); 229 | } 230 | 231 | return ParsePromise.error(error); 232 | }); 233 | }, 234 | 235 | _setXHR(xhr: any) { 236 | XHR = xhr; 237 | } 238 | } 239 | 240 | module.exports = RESTController; 241 | -------------------------------------------------------------------------------- /src/ReduxActionCreators.js: -------------------------------------------------------------------------------- 1 | var lib = { 2 | toUnderscore(str){ 3 | return str.replace(/([A-Z])/g, function($1){return "_"+$1}).toUpperCase(); 4 | } 5 | } 6 | 7 | export function generateActions(actions, namespace) { 8 | var out = {}; 9 | namespace = namespace ? namespace + '/' : ''; 10 | 11 | actions.forEach(function(m) { 12 | if (typeof m == 'string') { 13 | out[m] = function(payload) { 14 | return { 15 | type: namespace + lib.toUnderscore(m), 16 | payload 17 | } 18 | } 19 | } else 20 | out[m.name] = function(payload) { 21 | var action = m.action(payload); 22 | action.type = namespace + action.type; 23 | 24 | return action; 25 | } 26 | }); 27 | 28 | return out; 29 | } 30 | 31 | export const ObjectActions = generateActions([ 32 | 'initializeState', 33 | 'removeState', 34 | 'setServerData', 35 | 'setPendingOp', 36 | 'clearPendingOps', 37 | 'pushPendingState', 38 | 'popPendingState', 39 | 'mergeFirstPendingState', 40 | 'commitServerChanges', 41 | '_clearAllState', 42 | '_setExisted', 43 | ], 'Parse/Object'); 44 | 45 | var _functionActions = [ 46 | 'setPending', 47 | 'unsetPending', 48 | 'saveResult', 49 | {name: 'appendResult', action: function(payload) { 50 | var type = 'OPERATE_ON_ARRAY'; 51 | payload.operation = 'push'; 52 | 53 | return {type, payload}; 54 | }}, 55 | {name: 'prependResult', action: function(payload) { 56 | var type = 'OPERATE_ON_ARRAY'; 57 | payload.operation = 'unshift'; 58 | 59 | return {type, payload}; 60 | }}, 61 | 'estimateEndOfResults', 62 | ]; 63 | 64 | export const FunctionActions = generateActions(_functionActions, 'Parse/Cloud'); 65 | export const QueryActions = generateActions(_functionActions, 'Parse/Query'); 66 | 67 | export const UserActions = generateActions([ 68 | 'set' 69 | ], 'Parse/User'); -------------------------------------------------------------------------------- /src/ReduxCacheHelper.js: -------------------------------------------------------------------------------- 1 | import * as Store from './ReduxStore'; 2 | 3 | export default function({Actions, namespace}) { 4 | var Executed = {}; 5 | 6 | function refresh(cb, options) { 7 | Store.dispatch(Actions.setPending(options)); 8 | 9 | var { limit } = options; 10 | 11 | var done = cb().then(function(result) { 12 | Store.dispatch(Actions.saveResult({...options, result})); 13 | if (limit) 14 | Store.dispatch(Actions.estimateEndOfResults({...options, operation: 'appendResult', length: result.length})); 15 | 16 | return Parse.Promise.as(result); 17 | }).fail(function(err) { 18 | Store.dispatch(Actions.unsetPending(options)); 19 | 20 | return Parse.Promise.error(err); 21 | }); 22 | 23 | Executed = setItemState(Executed, options, done); 24 | 25 | return done; 26 | } 27 | 28 | function init(cb, options) { 29 | var State = Store.getState().Parse[namespace]; 30 | var { cache, pending } = getItemState(State, options); 31 | 32 | if (pending) 33 | return getItemState(Executed, options); 34 | 35 | if (cache) 36 | return Parse.Promise.as(cache); 37 | 38 | return refresh(...arguments); 39 | } 40 | 41 | // returns cached result if it has already been saved 42 | // queries if no result exists 43 | function get(options) { 44 | var State = Store.getState().Parse[namespace]; 45 | var state = getItemState(State, options); 46 | 47 | return state.cache; 48 | } 49 | 50 | function _operateOnArray(cb, options, operation) { 51 | Store.dispatch(Actions.setPending(options)); 52 | 53 | var { limit } = options; 54 | 55 | return cb().then(function(result) { 56 | Store.dispatch(Actions[operation]({...options, result})); 57 | Store.dispatch(Actions.estimateEndOfResults({...options, operation, length: result.length})); 58 | 59 | return Parse.Promise.as(result); 60 | }).fail(function(err) { 61 | Store.dispatch(Actions.unsetPending(options)); 62 | 63 | return Parse.Promise.error(err); 64 | }); 65 | } 66 | 67 | function append() { 68 | return _operateOnArray(...arguments, 'appendResult'); 69 | } 70 | 71 | function prepend() { 72 | return _operateOnArray(...arguments, 'prependResult'); 73 | } 74 | 75 | return { 76 | refresh, 77 | init, 78 | get, 79 | append, 80 | prepend 81 | } 82 | } 83 | 84 | export function getItemState(object, {name, data, grouping}) { 85 | var next = object[name]; 86 | if (!next) 87 | return {}; 88 | 89 | if (grouping) 90 | next = next[grouping]; 91 | else 92 | next = next[JSON.stringify(data)]; 93 | 94 | return next || {}; 95 | } 96 | 97 | export function setItemState(object, {name, data, grouping}, value) { 98 | var object = {...object}; 99 | var next = object[name]; 100 | 101 | if (next) 102 | next = {...next}; 103 | else 104 | next = {}; 105 | 106 | object[name] = next; 107 | 108 | var key; 109 | if (grouping) 110 | key = grouping; 111 | else 112 | key = JSON.stringify(data); 113 | 114 | next[key] = value; 115 | 116 | return object; 117 | } -------------------------------------------------------------------------------- /src/ReduxStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import parseReducer from './ReduxReducers'; 3 | 4 | var Store = null; 5 | 6 | export function set(_Store) { 7 | if (Store) 8 | throw new Error("Store already set! Make sure to initialize the store before it is retrieved."); 9 | 10 | if (!_Store) 11 | { 12 | var reducer = combineReducers({Parse: parseReducer}); 13 | _Store = createStore(reducer); 14 | } 15 | 16 | Store = _Store; 17 | } 18 | 19 | export function getState() { 20 | if (!Store) 21 | set(); 22 | return Store.getState(...arguments); 23 | } 24 | 25 | export function dispatch() { 26 | if (!Store) 27 | set(); 28 | return Store.dispatch(...arguments); 29 | } -------------------------------------------------------------------------------- /src/Storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import CoreManager from './CoreManager'; 13 | import ParsePromise from './ParsePromise'; 14 | 15 | var Storage = { 16 | async(): boolean { 17 | var controller = CoreManager.getStorageController(); 18 | return !!controller.async; 19 | }, 20 | 21 | getItem(path: string): ?string { 22 | var controller = CoreManager.getStorageController(); 23 | if (controller.async === 1) { 24 | throw new Error( 25 | 'Synchronous storage is not supported by the current storage controller' 26 | ); 27 | } 28 | return controller.getItem(path); 29 | }, 30 | 31 | getItemAsync(path: string): ParsePromise { 32 | var controller = CoreManager.getStorageController(); 33 | if (controller.async === 1) { 34 | return controller.getItemAsync(path); 35 | } 36 | return ParsePromise.as(controller.getItem(path)); 37 | }, 38 | 39 | setItem(path: string, value: string) { 40 | var controller = CoreManager.getStorageController(); 41 | if (controller.async === 1) { 42 | throw new Error( 43 | 'Synchronous storage is not supported by the current storage controller' 44 | ); 45 | } 46 | return controller.setItem(path, value); 47 | }, 48 | 49 | setItemAsync(path: string, value: string): ParsePromise { 50 | var controller = CoreManager.getStorageController(); 51 | if (controller.async === 1) { 52 | return controller.setItemAsync(path, value); 53 | } 54 | return ParsePromise.as(controller.setItem(path, value)); 55 | }, 56 | 57 | removeItem(path: string) { 58 | var controller = CoreManager.getStorageController(); 59 | if (controller.async === 1) { 60 | throw new Error( 61 | 'Synchronous storage is not supported by the current storage controller' 62 | ); 63 | } 64 | return controller.removeItem(path); 65 | }, 66 | 67 | removeItemAsync(path: string): ParsePromise { 68 | var controller = CoreManager.getStorageController(); 69 | if (controller.async === 1) { 70 | return controller.removeItemAsync(path); 71 | } 72 | return ParsePromise.as(controller.removeItem(path)); 73 | }, 74 | 75 | generatePath(path: string): string { 76 | if (!CoreManager.get('APPLICATION_ID')) { 77 | throw new Error('You need to call Parse.initialize before using Parse.'); 78 | } 79 | if (typeof path !== 'string') { 80 | throw new Error('Tried to get a Storage path that was not a String.'); 81 | } 82 | if (path[0] === '/') { 83 | path = path.substr(1); 84 | } 85 | return 'Parse/' + CoreManager.get('APPLICATION_ID') + '/' + path; 86 | }, 87 | 88 | _clear() { 89 | var controller = CoreManager.getStorageController(); 90 | if (controller.hasOwnProperty('clear')) { 91 | controller.clear(); 92 | } 93 | } 94 | }; 95 | 96 | module.exports = Storage; 97 | 98 | if (process.env.PARSE_BUILD === 'react-native') { 99 | CoreManager.setStorageController(require('./StorageController.react-native')); 100 | } else if (process.env.PARSE_BUILD === 'browser') { 101 | CoreManager.setStorageController(require('./StorageController.browser')); 102 | } else { 103 | CoreManager.setStorageController(require('./StorageController.default')); 104 | } 105 | -------------------------------------------------------------------------------- /src/StorageController.browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParsePromise from './ParsePromise'; 13 | 14 | var StorageController = { 15 | async: 0, 16 | 17 | getItem(path: string): ?string { 18 | return localStorage.getItem(path); 19 | }, 20 | 21 | setItem(path: string, value: string) { 22 | try { 23 | localStorage.setItem(path, value); 24 | } catch (e) { 25 | // Quota exceeded, possibly due to Safari Private Browsing mode 26 | } 27 | }, 28 | 29 | removeItem(path: string) { 30 | localStorage.removeItem(path); 31 | }, 32 | 33 | clear() { 34 | localStorage.clear(); 35 | } 36 | }; 37 | 38 | module.exports = StorageController; 39 | -------------------------------------------------------------------------------- /src/StorageController.default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | // When there is no native storage interface, we default to an in-memory map 13 | var memMap = {}; 14 | var StorageController = { 15 | async: 0, 16 | 17 | getItem(path: string): ?string { 18 | if (memMap.hasOwnProperty(path)) { 19 | return memMap[path]; 20 | } 21 | return null; 22 | }, 23 | 24 | setItem(path: string, value: string) { 25 | memMap[path] = String(value); 26 | }, 27 | 28 | removeItem(path: string) { 29 | delete memMap[path]; 30 | }, 31 | 32 | clear() { 33 | for (var key in memMap) { 34 | if (memMap.hasOwnProperty(key)) { 35 | delete memMap[key]; 36 | } 37 | } 38 | } 39 | }; 40 | 41 | module.exports = StorageController; 42 | -------------------------------------------------------------------------------- /src/StorageController.react-native.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParsePromise from './ParsePromise'; 13 | import { AsyncStorage } from 'react-native'; 14 | 15 | var StorageController = { 16 | async: 1, 17 | 18 | getItemAsync(path: string): ParsePromise { 19 | var p = new ParsePromise(); 20 | AsyncStorage.getItem(path, function(err, value) { 21 | if (err) { 22 | p.reject(err); 23 | } else { 24 | p.resolve(value); 25 | } 26 | }); 27 | return p; 28 | }, 29 | 30 | setItemAsync(path: string, value: string): ParsePromise { 31 | var p = new ParsePromise(); 32 | AsyncStorage.setItem(path, value, function(err) { 33 | if (err) { 34 | p.reject(err); 35 | } else { 36 | p.resolve(value); 37 | } 38 | }); 39 | return p; 40 | }, 41 | 42 | removeItemAsync(path: string): ParsePromise { 43 | var p = new ParsePromise(); 44 | AsyncStorage.removeItem(path, function(err) { 45 | if (err) { 46 | p.reject(err); 47 | } else { 48 | p.resolve(); 49 | } 50 | }); 51 | return p; 52 | }, 53 | 54 | clear() { 55 | AsyncStorage.clear(); 56 | } 57 | }; 58 | 59 | module.exports = StorageController; 60 | -------------------------------------------------------------------------------- /src/TaskQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParsePromise from './ParsePromise'; 13 | 14 | type Task = { 15 | task: () => ParsePromise; 16 | _completion: ParsePromise 17 | }; 18 | 19 | class TaskQueue { 20 | queue: Array; 21 | 22 | constructor() { 23 | this.queue = []; 24 | } 25 | 26 | enqueue(task: () => ParsePromise): ParsePromise { 27 | var taskComplete = new ParsePromise(); 28 | this.queue.push({ 29 | task: task, 30 | _completion: taskComplete 31 | }); 32 | if (this.queue.length === 1) { 33 | task().then(() => { 34 | this._dequeue(); 35 | taskComplete.resolve(); 36 | }, (error) => { 37 | this._dequeue(); 38 | taskComplete.reject(error); 39 | }); 40 | } 41 | return taskComplete; 42 | } 43 | 44 | _dequeue() { 45 | this.queue.shift(); 46 | if (this.queue.length) { 47 | var next = this.queue[0]; 48 | next.task().then(() => { 49 | this._dequeue(); 50 | next._completion.resolve(); 51 | }, (error) => { 52 | this._dequeue(); 53 | next._completion.reject(error); 54 | }); 55 | } 56 | } 57 | } 58 | 59 | module.exports = TaskQueue; 60 | -------------------------------------------------------------------------------- /src/__mocks__/react-native.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | var mockStorage = {}; 11 | var mockStorageInterface = { 12 | getItem(path, cb) { 13 | cb(undefined, mockStorage[path] || null); 14 | }, 15 | 16 | setItem(path, value, cb) { 17 | mockStorage[path] = value; 18 | cb(); 19 | }, 20 | 21 | removeItem(path, cb) { 22 | delete mockStorage[path]; 23 | cb(); 24 | }, 25 | 26 | clear() { 27 | mockStorage = {}; 28 | } 29 | } 30 | 31 | module.exports = { 32 | AsyncStorage: mockStorageInterface 33 | }; 34 | -------------------------------------------------------------------------------- /src/__tests__/Analytics-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../Analytics'); 11 | jest.dontMock('../CoreManager'); 12 | jest.dontMock('../ParsePromise'); 13 | 14 | var Analytics = require('../Analytics'); 15 | var CoreManager = require('../CoreManager'); 16 | var ParsePromise = require('../ParsePromise'); 17 | 18 | var defaultController = CoreManager.getAnalyticsController(); 19 | 20 | describe('Analytics', () => { 21 | beforeEach(() => { 22 | var track = jest.genMockFunction(); 23 | track.mockReturnValue(ParsePromise.as()); 24 | CoreManager.setAnalyticsController({ track: track }); 25 | }); 26 | 27 | it('throws when no event name is provided', () => { 28 | expect(Analytics.track) 29 | .toThrow('A name for the custom event must be provided'); 30 | 31 | expect(Analytics.track.bind(null, '')) 32 | .toThrow('A name for the custom event must be provided'); 33 | }); 34 | 35 | it('trims whitespace from event names', () => { 36 | Analytics.track(' before', {}); 37 | expect(CoreManager.getAnalyticsController().track.mock.calls[0]) 38 | .toEqual(['before', {}]); 39 | 40 | Analytics.track('after ', {}); 41 | expect(CoreManager.getAnalyticsController().track.mock.calls[1]) 42 | .toEqual(['after', {}]); 43 | 44 | Analytics.track(' both ', {}); 45 | expect(CoreManager.getAnalyticsController().track.mock.calls[2]) 46 | .toEqual(['both', {}]); 47 | }); 48 | 49 | it('passes along event names and dimensions', () => { 50 | Analytics.track('myEvent', { value: 'a' }); 51 | expect(CoreManager.getAnalyticsController().track.mock.calls[0]) 52 | .toEqual(['myEvent', { value: 'a' }]); 53 | }); 54 | 55 | it('throws when invalid dimensions are provided', () => { 56 | expect(Analytics.track.bind(null, 'event', { number: 12 })) 57 | .toThrow('track() dimensions expects keys and values of type "string".'); 58 | 59 | expect(Analytics.track.bind(null, 'event', { 'null': null })) 60 | .toThrow('track() dimensions expects keys and values of type "string".'); 61 | }); 62 | }); 63 | 64 | describe('AnalyticsController', () => { 65 | beforeEach(() => { 66 | CoreManager.setAnalyticsController(defaultController); 67 | var request = jest.genMockFunction(); 68 | request.mockReturnValue(ParsePromise.as({ 69 | success: true, 70 | result: {} 71 | })); 72 | var ajax = jest.genMockFunction(); 73 | CoreManager.setRESTController({ request: request, ajax: ajax }); 74 | }); 75 | 76 | it('passes dimensions along to the appropriate endpoint', () => { 77 | Analytics.track('click', { x: '12', y: '40' }); 78 | 79 | expect(CoreManager.getRESTController().request.mock.calls[0]) 80 | .toEqual([ 81 | 'POST', 82 | 'events/click', 83 | { dimensions: { x: '12', y: '40'} } 84 | ]); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/__tests__/Cloud-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../Cloud'); 11 | jest.dontMock('../CoreManager'); 12 | jest.dontMock('../decode'); 13 | jest.dontMock('../encode'); 14 | jest.dontMock('../ParsePromise'); 15 | 16 | jest.dontMock('../ReduxCacheHelper'); 17 | 18 | var Cloud = require('../Cloud'); 19 | var CoreManager = require('../CoreManager'); 20 | var ParsePromise = require('../ParsePromise'); 21 | 22 | var defaultController = CoreManager.getCloudController(); 23 | 24 | describe('Cloud', () => { 25 | beforeEach(() => { 26 | var run = jest.genMockFunction(); 27 | run.mockReturnValue(ParsePromise.as({ 28 | result: {} 29 | })); 30 | CoreManager.setCloudController({ run: run }); 31 | }); 32 | 33 | it('throws with an invalid function name', () => { 34 | expect(Cloud.run) 35 | .toThrow('Cloud function name must be a string.'); 36 | 37 | expect(Cloud.run.bind(null, '')) 38 | .toThrow('Cloud function name must be a string.'); 39 | 40 | expect(Cloud.run.bind(null, {})) 41 | .toThrow('Cloud function name must be a string.'); 42 | }); 43 | 44 | it('passes function name and data along', () => { 45 | Cloud.run('myfunction', {}); 46 | 47 | expect(CoreManager.getCloudController().run.mock.calls[0]) 48 | .toEqual(['myfunction', {}, {}]); 49 | }); 50 | 51 | it('passes options', () => { 52 | Cloud.run('myfunction', {}, { useMasterKey: false }); 53 | 54 | expect(CoreManager.getCloudController().run.mock.calls[0]) 55 | .toEqual(['myfunction', {}, {}]); 56 | 57 | Cloud.run('myfunction', {}, { useMasterKey: true }); 58 | 59 | expect(CoreManager.getCloudController().run.mock.calls[1]) 60 | .toEqual(['myfunction', {}, { useMasterKey: true }]); 61 | 62 | Cloud.run('myfunction', {}, { sessionToken: 'asdf1234' }); 63 | 64 | expect(CoreManager.getCloudController().run.mock.calls[2]) 65 | .toEqual(['myfunction', {}, { sessionToken: 'asdf1234' }]); 66 | 67 | Cloud.run('myfunction', {}, { useMasterKey: true, sessionToken: 'asdf1234' }); 68 | 69 | expect(CoreManager.getCloudController().run.mock.calls[3]) 70 | .toEqual(['myfunction', {}, { useMasterKey: true, sessionToken: 'asdf1234' }]); 71 | }) 72 | }); 73 | 74 | describe('CloudController', () => { 75 | beforeEach(() => { 76 | CoreManager.setCloudController(defaultController); 77 | var request = jest.genMockFunction(); 78 | request.mockReturnValue(ParsePromise.as({ 79 | success: true, 80 | result: {} 81 | })); 82 | var ajax = jest.genMockFunction(); 83 | CoreManager.setRESTController({ request: request, ajax: ajax }); 84 | }); 85 | 86 | it('passes encoded requests', () => { 87 | Cloud.run('myfunction', { value: 12, when: new Date(Date.UTC(2015,0,1)) }); 88 | 89 | expect(CoreManager.getRESTController().request.mock.calls[0]) 90 | .toEqual(['POST', 'functions/myfunction', { 91 | value: 12, when: { __type: 'Date', iso: '2015-01-01T00:00:00.000Z'} 92 | }, { }]); 93 | }); 94 | 95 | it('passes options', () => { 96 | Cloud.run('myfunction', { value: 12 }, { useMasterKey: true }); 97 | 98 | expect(CoreManager.getRESTController().request.mock.calls[0]) 99 | .toEqual(['POST', 'functions/myfunction', { 100 | value: 12 101 | }, { useMasterKey: true }]); 102 | 103 | Cloud.run('myfunction', { value: 12 }, { sessionToken: 'asdf1234' }); 104 | 105 | expect(CoreManager.getRESTController().request.mock.calls[1]) 106 | .toEqual(['POST', 'functions/myfunction', { 107 | value: 12 108 | }, { sessionToken: 'asdf1234' }]); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/__tests__/InstallationController-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../CoreManager'); 11 | jest.dontMock('../InstallationController'); 12 | jest.dontMock('../ParsePromise'); 13 | jest.dontMock('../Storage'); 14 | jest.dontMock('../StorageController.default'); 15 | jest.dontMock('./test_helpers/asyncHelper'); 16 | 17 | var CoreManager = require('../CoreManager'); 18 | var InstallationController = require('../InstallationController'); 19 | var Storage = require('../Storage'); 20 | var asyncHelper = require('./test_helpers/asyncHelper'); 21 | 22 | describe('InstallationController', () => { 23 | beforeEach(() => { 24 | CoreManager.set('APPLICATION_ID', 'A'); 25 | CoreManager.set('JAVASCRIPT_KEY', 'B'); 26 | Storage._clear(); 27 | InstallationController._clearCache(); 28 | }); 29 | 30 | it('generates a new installation id when there is none', asyncHelper((done) => { 31 | InstallationController.currentInstallationId().then((iid) => { 32 | expect(typeof iid).toBe('string'); 33 | expect(iid.length).toBeGreaterThan(0); 34 | done(); 35 | }); 36 | })); 37 | 38 | it('caches the installation id', asyncHelper((done) => { 39 | var iid = null; 40 | InstallationController.currentInstallationId().then((i) => { 41 | iid = i; 42 | Storage._clear(); 43 | return InstallationController.currentInstallationId(); 44 | }).then((i) => { 45 | expect(i).toBe(iid); 46 | done(); 47 | }); 48 | })); 49 | 50 | it('permanently stores the installation id', asyncHelper((done) => { 51 | var iid = null; 52 | InstallationController.currentInstallationId().then((i) => { 53 | iid = i; 54 | InstallationController._clearCache(); 55 | return InstallationController.currentInstallationId(); 56 | }).then((i) => { 57 | expect(i).toBe(iid); 58 | done(); 59 | }); 60 | })); 61 | }); 62 | -------------------------------------------------------------------------------- /src/__tests__/Parse-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../CoreManager'); 11 | jest.dontMock('../Parse'); 12 | 13 | jest.dontMock('../ReduxCacheHelper'); 14 | 15 | var CoreManager = require('../CoreManager'); 16 | var Parse = require('../Parse'); 17 | 18 | describe('Parse module', () => { 19 | it('can be initialized with keys', () => { 20 | Parse.initialize('A', 'B'); 21 | expect(CoreManager.get('APPLICATION_ID')).toBe('A'); 22 | expect(CoreManager.get('JAVASCRIPT_KEY')).toBe('B'); 23 | 24 | Parse._initialize('A', 'B', 'C'); 25 | expect(CoreManager.get('APPLICATION_ID')).toBe('A'); 26 | expect(CoreManager.get('JAVASCRIPT_KEY')).toBe('B'); 27 | expect(CoreManager.get('MASTER_KEY')).toBe('C'); 28 | }); 29 | 30 | it('enables master key use in the node build', () => { 31 | expect(typeof Parse.Cloud.useMasterKey).toBe('function'); 32 | Parse.Cloud.useMasterKey(); 33 | expect(CoreManager.get('USE_MASTER_KEY')).toBe(true); 34 | }); 35 | 36 | it('exposes certain keys as properties', () => { 37 | Parse.applicationId = '123'; 38 | expect(CoreManager.get('APPLICATION_ID')).toBe('123'); 39 | expect(Parse.applicationId).toBe('123'); 40 | 41 | Parse.javaScriptKey = '456'; 42 | expect(CoreManager.get('JAVASCRIPT_KEY')).toBe('456'); 43 | expect(Parse.javaScriptKey).toBe('456'); 44 | 45 | Parse.masterKey = '789'; 46 | expect(CoreManager.get('MASTER_KEY')).toBe('789'); 47 | expect(Parse.masterKey).toBe('789'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/__tests__/ParseConfig-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../CoreManager'); 11 | jest.dontMock('../decode'); 12 | jest.dontMock('../escape'); 13 | jest.dontMock('../ParseConfig'); 14 | jest.dontMock('../ParseError'); 15 | jest.dontMock('../ParseFile'); 16 | jest.dontMock('../ParseGeoPoint'); 17 | jest.dontMock('../ParsePromise'); 18 | jest.dontMock('../RESTController'); 19 | jest.dontMock('../Storage'); 20 | jest.dontMock('../StorageController.default'); 21 | jest.dontMock('./test_helpers/asyncHelper'); 22 | 23 | jest.dontMock('../ReduxCacheHelper'); 24 | 25 | var CoreManager = require('../CoreManager'); 26 | var ParseConfig = require('../ParseConfig'); 27 | var ParseGeoPoint = require('../ParseGeoPoint'); 28 | var ParsePromise = require('../ParsePromise'); 29 | var Storage = require('../Storage'); 30 | 31 | var asyncHelper = require('./test_helpers/asyncHelper'); 32 | 33 | CoreManager.set('APPLICATION_ID', 'A'); 34 | CoreManager.set('JAVASCRIPT_KEY', 'B'); 35 | 36 | describe('ParseConfig', () => { 37 | it('exposes attributes via get()', () => { 38 | var c = new ParseConfig(); 39 | c.attributes = { 40 | str: 'hello', 41 | num: 44 42 | }; 43 | expect(c.get('str')).toBe('hello'); 44 | expect(c.get('num')).toBe(44); 45 | expect(c.get('nonexistent')).toBe(undefined); 46 | }); 47 | 48 | it('exposes escaped attributes', () => { 49 | var c = new ParseConfig(); 50 | c.attributes = { 51 | brackets: '<>', 52 | phone: 'AT&T' 53 | }; 54 | expect(c.escape('brackets')).toBe('<>'); 55 | expect(c.escape('phone')).toBe('AT&T'); 56 | }); 57 | 58 | it('can retrieve the current config from disk or cache', () => { 59 | var path = Storage.generatePath('currentConfig'); 60 | Storage.setItem(path, JSON.stringify({ 61 | count: 12, 62 | point: { 63 | __type: 'GeoPoint', 64 | latitude: 20.02, 65 | longitude: 30.03 66 | } 67 | })); 68 | expect(ParseConfig.current().attributes).toEqual({ 69 | count: 12, 70 | point: new ParseGeoPoint(20.02, 30.03) 71 | }); 72 | }); 73 | 74 | it('can get a config object from the network', asyncHelper((done) => { 75 | CoreManager.setRESTController({ 76 | request(method, path, body, options) { 77 | return ParsePromise.as({ 78 | params: { 79 | str: 'hello', 80 | num: 45, 81 | file: { 82 | __type: 'File', 83 | name: 'parse.txt', 84 | url: 'https://files.parsetfss.com/a/parse.txt' 85 | } 86 | } 87 | }); 88 | }, 89 | ajax() {} 90 | }); 91 | ParseConfig.get().then((config) => { 92 | expect(config.get('str')).toBe('hello'); 93 | expect(config.get('num')).toBe(45); 94 | expect(config.get('file').name()).toBe('parse.txt'); 95 | var path = Storage.generatePath('currentConfig'); 96 | expect(JSON.parse(Storage.getItem(path))).toEqual({ 97 | str: 'hello', 98 | num: 45, 99 | file: { 100 | __type: 'File', 101 | name: 'parse.txt', 102 | url: 'https://files.parsetfss.com/a/parse.txt' 103 | } 104 | }); 105 | 106 | done(); 107 | }); 108 | })); 109 | 110 | it('rejects the promise when an invalid payload comes back', asyncHelper((done) => { 111 | CoreManager.setRESTController({ 112 | request(method, path, body, options) { 113 | return ParsePromise.as(null); 114 | }, 115 | ajax() {} 116 | }); 117 | ParseConfig.get().then(null, (error) => { 118 | expect(error.code).toBe(107); 119 | expect(error.message).toBe('Config JSON response invalid.'); 120 | 121 | done(); 122 | }); 123 | })); 124 | 125 | it('rejects the promise when the http request fails', asyncHelper((done) => { 126 | CoreManager.setRESTController({ 127 | request(method, path, body, options) { 128 | return ParsePromise.error('failure'); 129 | }, 130 | ajax() {} 131 | }); 132 | ParseConfig.get().then(null, (error) => { 133 | expect(error).toBe('failure'); 134 | done(); 135 | }); 136 | })); 137 | }); 138 | -------------------------------------------------------------------------------- /src/__tests__/ParseFile-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var ParseFile = require('../ParseFile'); 13 | var ParsePromise = require('../ParsePromise'); 14 | var CoreManager = require('../CoreManager'); 15 | 16 | function generateSaveMock(prefix) { 17 | return function(name, payload) { 18 | return ParsePromise.as({ 19 | name: name, 20 | url: prefix + name 21 | }); 22 | }; 23 | } 24 | 25 | var defaultController = CoreManager.getFileController(); 26 | 27 | describe('ParseFile', () => { 28 | beforeEach(() => { 29 | CoreManager.setFileController({ 30 | saveFile: generateSaveMock('http://files.parsetfss.com/a/'), 31 | saveBase64: generateSaveMock('http://files.parsetfss.com/a/') 32 | }); 33 | }); 34 | 35 | it('can create files with base64 encoding', () => { 36 | var file = new ParseFile('parse.txt', { base64: 'ParseA==' }); 37 | expect(file._source.base64).toBe('ParseA=='); 38 | expect(file._source.type).toBe(''); 39 | }); 40 | 41 | it('can extact data type from base64', () => { 42 | var file = new ParseFile('parse.txt', { 43 | base64: 'data:image/png;base64,ParseA==' 44 | }); 45 | expect(file._source.base64).toBe('ParseA=='); 46 | expect(file._source.type).toBe('image/png'); 47 | }); 48 | 49 | it('can create files with byte arrays', () => { 50 | var file = new ParseFile('parse.txt', [61, 170, 236, 120]); 51 | expect(file._source.base64).toBe('ParseA=='); 52 | expect(file._source.type).toBe(''); 53 | }); 54 | 55 | it('can create files with all types of characters', () => { 56 | var file = new ParseFile('parse.txt', [11, 239, 191, 215, 80, 52]); 57 | expect(file._source.base64).toBe('C++/11A0'); 58 | expect(file._source.type).toBe(''); 59 | }); 60 | 61 | it('can create an empty file', () => { 62 | var file = new ParseFile('parse.txt'); 63 | expect(file.name()).toBe('parse.txt'); 64 | expect(file.url()).toBe(undefined); 65 | }); 66 | 67 | it('throws when creating a file with invalid data', () => { 68 | expect(function() { 69 | new ParseFile('parse.txt', 12); 70 | }).toThrow('Cannot create a Parse.File with that data.'); 71 | 72 | expect(function() { 73 | new ParseFile('parse.txt', null); 74 | }).toThrow('Cannot create a Parse.File with that data.'); 75 | 76 | expect(function() { 77 | new ParseFile('parse.txt', 'string'); 78 | }).toThrow('Cannot create a Parse.File with that data.'); 79 | }); 80 | 81 | it('updates fields when saved', () => { 82 | var file = new ParseFile('parse.txt', { base64: 'ParseA==' }); 83 | expect(file.name()).toBe('parse.txt'); 84 | expect(file.url()).toBe(undefined); 85 | file.save().then(function(result) { 86 | expect(result).toBe(file); 87 | expect(result.name()).toBe('parse.txt'); 88 | expect(result.url()).toBe('http://files.parsetfss.com/a/parse.txt'); 89 | }); 90 | }); 91 | 92 | it('generates a JSON representation', () => { 93 | var file = new ParseFile('parse.txt', { base64: 'ParseA==' }); 94 | file.save().then(function(result) { 95 | expect(result.toJSON()).toEqual({ 96 | __type: 'File', 97 | name: 'parse.txt', 98 | url: 'http://files.parsetfss.com/a/parse.txt' 99 | }); 100 | }); 101 | }); 102 | 103 | it('can construct a file from a JSON object', () => { 104 | var f = ParseFile.fromJSON({ 105 | __type: 'File', 106 | name: 'parse.txt', 107 | url: 'http://files.parsetfss.com/a/parse.txt' 108 | }); 109 | expect(f).toBeTruthy(); 110 | expect(f.name()).toBe('parse.txt'); 111 | expect(f.url()).toBe('http://files.parsetfss.com/a/parse.txt'); 112 | 113 | expect(ParseFile.fromJSON.bind(null, {})) 114 | .toThrow('JSON object does not represent a ParseFile'); 115 | }); 116 | 117 | it('can test equality against another ParseFile', () => { 118 | var a = new ParseFile('parse.txt', [61, 170, 236, 120]); 119 | var b = new ParseFile('parse.txt', [61, 170, 236, 120]); 120 | 121 | expect(a.equals(a)).toBe(true); 122 | // unsaved files are never equal 123 | expect(a.equals(b)).toBe(false); 124 | expect(b.equals(a)).toBe(false); 125 | 126 | a = ParseFile.fromJSON({ 127 | __type: 'File', 128 | name: 'parse.txt', 129 | url: 'http://files.parsetfss.com/a/parse.txt' 130 | }); 131 | b = ParseFile.fromJSON({ 132 | __type: 'File', 133 | name: 'parse.txt', 134 | url: 'http://files.parsetfss.com/a/parse.txt' 135 | }); 136 | 137 | expect(a.equals(b)).toBe(true); 138 | expect(b.equals(a)).toBe(true); 139 | 140 | b = ParseFile.fromJSON({ 141 | __type: 'File', 142 | name: 'parse.txt', 143 | url: 'http://files.parsetfss.com/b/parse.txt' 144 | }); 145 | 146 | expect(a.equals(b)).toBe(false); 147 | expect(b.equals(a)).toBe(false); 148 | }); 149 | }); 150 | 151 | describe('FileController', () => { 152 | beforeEach(() => { 153 | CoreManager.setFileController(defaultController); 154 | var request = function(method, path, data) { 155 | var name = path.substr(path.indexOf('/') + 1); 156 | return ParsePromise.as({ 157 | name: name, 158 | url: 'https://files.parsetfss.com/a/' + name 159 | }); 160 | }; 161 | var ajax = function(method, path, data, headers) { 162 | var name = path.substr(path.indexOf('/') + 1); 163 | return ParsePromise.as({ 164 | name: name, 165 | url: 'https://files.parsetfss.com/a/' + name 166 | }); 167 | }; 168 | CoreManager.setRESTController({ request: request, ajax: ajax }); 169 | }); 170 | 171 | it('saves files created with bytes', () => { 172 | var file = new ParseFile('parse.txt', [61, 170, 236, 120]); 173 | file.save().then(function(f) { 174 | expect(f).toBe(file); 175 | expect(f.name()).toBe('parse.txt'); 176 | expect(f.url()).toBe('https://files.parsetfss.com/a/parse.txt'); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/__tests__/ParseGeoPoint-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var ParseGeoPoint = require('../ParseGeoPoint'); 13 | 14 | describe('GeoPoint', () => { 15 | it('can be constructed from various inputs', () => { 16 | var point = new ParseGeoPoint(); 17 | expect(point.latitude).toBe(0); 18 | expect(point.longitude).toBe(0); 19 | 20 | point = new ParseGeoPoint(42, 36); 21 | expect(point.latitude).toBe(42); 22 | expect(point.longitude).toBe(36); 23 | 24 | point = new ParseGeoPoint([12, 24]); 25 | expect(point.latitude).toBe(12); 26 | expect(point.longitude).toBe(24); 27 | 28 | point = new ParseGeoPoint({ latitude: 8, longitude: 88 }); 29 | expect(point.latitude).toBe(8); 30 | expect(point.longitude).toBe(88); 31 | }); 32 | 33 | it('throws when created with NaN values', () => { 34 | expect(function() { 35 | new ParseGeoPoint(NaN, NaN); 36 | }).toThrow('GeoPoint latitude and longitude must be valid numbers'); 37 | }); 38 | 39 | it('can set latitude and longitude', () => { 40 | var point = new ParseGeoPoint(); 41 | point.latitude = 5.5; 42 | expect(point.latitude).toBe(5.5); 43 | 44 | point.latitude = 10; 45 | expect(point.latitude).toBe(10); 46 | 47 | point.longitude = 12.1; 48 | expect(point.longitude).toBe(12.1); 49 | 50 | point.longitude = 14.9; 51 | expect(point.longitude).toBe(14.9); 52 | }); 53 | 54 | it('throws for points out of bounds', () => { 55 | var point; 56 | expect(() => { 57 | point = new ParseGeoPoint(90.01, 0.0); 58 | }).toThrow(); 59 | 60 | expect(() => { 61 | point = new ParseGeoPoint(-90.01, 0.0); 62 | }).toThrow(); 63 | 64 | expect(() => { 65 | point = new ParseGeoPoint(0.0, 180.01); 66 | }).toThrow(); 67 | 68 | expect(() => { 69 | point = new ParseGeoPoint(0.0, -180.01); 70 | }).toThrow(); 71 | }); 72 | 73 | it('can calculate distance in radians', () => { 74 | var d2r = Math.PI / 180.0; 75 | var pointA = new ParseGeoPoint(); 76 | var pointB = new ParseGeoPoint(); 77 | 78 | // Zero 79 | expect(pointA.radiansTo(pointB)).toBe(0); 80 | expect(pointB.radiansTo(pointA)).toBe(0); 81 | 82 | // Wrap Long 83 | pointA.longitude = 179.0; 84 | pointB.longitude = -179.0; 85 | expect(pointA.radiansTo(pointB)).toBeCloseTo(2 * d2r, 5); 86 | expect(pointB.radiansTo(pointA)).toBeCloseTo(2 * d2r, 5); 87 | 88 | // North South Lat 89 | pointA.latitude = 89.0; 90 | pointA.longitude = 0.0; 91 | pointB.latitude = -89.0; 92 | pointB.longitude = 0.0; 93 | expect(pointA.radiansTo(pointB)).toBeCloseTo(178 * d2r, 5); 94 | expect(pointB.radiansTo(pointA)).toBeCloseTo(178 * d2r, 5); 95 | 96 | // Long wrap Lat 97 | pointA.latitude = 89.0; 98 | pointA.longitude = 0.0; 99 | pointB.latitude = -89.0; 100 | pointB.longitude = 179.9999; 101 | expect(pointA.radiansTo(pointB)).toBeCloseTo(180 * d2r, 5); 102 | expect(pointB.radiansTo(pointA)).toBeCloseTo(180 * d2r, 5); 103 | 104 | pointA.latitude = 79.0; 105 | pointA.longitude = 90.0; 106 | pointB.latitude = -79.0; 107 | pointB.longitude = -90; 108 | expect(pointA.radiansTo(pointB)).toBeCloseTo(180 * d2r, 5); 109 | expect(pointB.radiansTo(pointA)).toBeCloseTo(180 * d2r, 5); 110 | 111 | // Wrap near pole - somewhat ill conditioned case due to pole proximity 112 | pointA.latitude = 85.0; 113 | pointA.longitude = 90.0; 114 | pointB.latitude = 85.0; 115 | pointB.longitude = -90; 116 | expect(pointA.radiansTo(pointB)).toBeCloseTo(10 * d2r, 5); 117 | expect(pointB.radiansTo(pointA)).toBeCloseTo(10 * d2r, 5); 118 | 119 | // Reference cities 120 | // Sydney Australia 121 | pointA.latitude = -34.0; 122 | pointA.longitude = 151.0; 123 | // Buenos Aires 124 | pointB.latitude = -34.5; 125 | pointB.longitude = -58.35; 126 | expect(pointA.radiansTo(pointB)).toBeCloseTo(1.85, 2); 127 | expect(pointB.radiansTo(pointA)).toBeCloseTo(1.85, 2); 128 | }); 129 | 130 | it('can calculate distances in mi and km', () => { 131 | // [SAC] 38.52 -121.50 Sacramento,CA 132 | var sacramento = new ParseGeoPoint(38.52, -121.50); 133 | 134 | // [HNL] 21.35 -157.93 Honolulu Int,HI 135 | var honolulu = new ParseGeoPoint(21.35, -157.93); 136 | 137 | // [51Q] 37.75 -122.68 San Francisco,CA 138 | var sanfran = new ParseGeoPoint(37.75, -122.68); 139 | 140 | // Vorkuta 67.509619,64.085999 141 | var vorkuta = new ParseGeoPoint(67.509619, 64.085999); 142 | 143 | // London 144 | var london = new ParseGeoPoint(51.501904,-0.115356); 145 | 146 | // Northampton 147 | var northampton = new ParseGeoPoint(52.241256,-0.895386); 148 | 149 | // Powell St BART station 150 | var powell = new ParseGeoPoint(37.785071,-122.407007); 151 | 152 | // Apple store 153 | var astore = new ParseGeoPoint(37.785809,-122.406363); 154 | 155 | // Self 156 | expect(honolulu.kilometersTo(honolulu)).toBeCloseTo(0.0, 3); 157 | expect(honolulu.milesTo(honolulu)).toBeCloseTo(0.0, 3); 158 | 159 | // Symmetric 160 | expect(sacramento.kilometersTo(honolulu)).toBeCloseTo(3964.8, -2); 161 | expect(honolulu.kilometersTo(sacramento)).toBeCloseTo(3964.8, -2); 162 | expect(sacramento.milesTo(honolulu)).toBeCloseTo(2463.6, -1); 163 | expect(honolulu.milesTo(sacramento)).toBeCloseTo(2463.6, -1); 164 | 165 | // Semi-local 166 | expect(london.kilometersTo(northampton)).toBeCloseTo(98.4, 0); 167 | expect(london.milesTo(northampton)).toBeCloseTo(61.2, 0); 168 | 169 | expect(sacramento.kilometersTo(sanfran)).toBeCloseTo(134.5, 0); 170 | expect(sacramento.milesTo(sanfran)).toBeCloseTo(84.8, -1); 171 | 172 | // Very local 173 | expect(powell.kilometersTo(astore)).toBeCloseTo(0.1, 2); 174 | 175 | // Far (for error tolerance's sake) 176 | expect(sacramento.kilometersTo(vorkuta)).toBeCloseTo(8303.8, -3); 177 | expect(sacramento.milesTo(vorkuta)).toBeCloseTo(5159.7, -3); 178 | }); 179 | 180 | it('can test equality against another GeoPoint', () => { 181 | var a = new ParseGeoPoint(40, 40); 182 | expect(a.equals(a)).toBe(true); 183 | 184 | var b = new ParseGeoPoint(40, 40); 185 | expect(a.equals(b)).toBe(true); 186 | expect(b.equals(a)).toBe(true); 187 | 188 | b = new ParseGeoPoint(50, 40); 189 | expect(a.equals(b)).toBe(false); 190 | expect(b.equals(a)).toBe(false); 191 | 192 | a = new ParseGeoPoint(40.001, 40.001); 193 | b = new ParseGeoPoint(40.001, 40.001); 194 | expect(a.equals(b)).toBe(true); 195 | expect(b.equals(a)).toBe(true); 196 | 197 | b = new ParseGeoPoint(40, 40); 198 | expect(a.equals(b)).toBe(false); 199 | expect(b.equals(a)).toBe(false); 200 | 201 | a = new ParseGeoPoint(40, 50); 202 | expect(a.equals(b)).toBe(false); 203 | expect(b.equals(a)).toBe(false); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /src/__tests__/ParseRelation-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../encode'); 11 | jest.dontMock('../ParseRelation'); 12 | jest.dontMock('../ParseOp'); 13 | jest.dontMock('../unique'); 14 | 15 | var mockStore = {}; 16 | var mockObject = function(className) { 17 | this.className = className; 18 | this.ops = {}; 19 | }; 20 | mockObject.registerSubclass = function() {}; 21 | mockObject.prototype = { 22 | _getId() { 23 | return this.id; 24 | }, 25 | set(key, value) { 26 | if (!mockStore[this.id]) { 27 | mockStore[this.id] = {}; 28 | } 29 | if (!mockStore[this.id][key]) { 30 | mockStore[this.id][key] = []; 31 | } 32 | mockStore[this.id][key].push(value); 33 | }, 34 | relation(key) { 35 | if (mockStore[this.id][key]) { 36 | return this.get(key); 37 | } 38 | return new ParseRelation(this, key); 39 | }, 40 | get(key) { 41 | return this.op(key).applyTo( 42 | null, 43 | { className: this.className, id: this.id }, 44 | key 45 | ); 46 | }, 47 | op(key) { 48 | var finalOp = undefined; 49 | for (var i = 0; i < mockStore[this.id][key].length; i++) { 50 | finalOp = mockStore[this.id][key][i].mergeWith(finalOp); 51 | } 52 | return finalOp; 53 | } 54 | }; 55 | jest.setMock('../ParseObject', mockObject); 56 | 57 | var mockQuery = function(className) { 58 | this.className = className; 59 | this.where = {}; 60 | this._extraOptions = {}; 61 | }; 62 | mockQuery.prototype = { 63 | _addCondition(key, comparison, value) { 64 | this.where[key] = this.where[key] || {}; 65 | this.where[key][comparison] = value; 66 | } 67 | }; 68 | jest.setMock('../ParseQuery', mockQuery); 69 | 70 | var ParseObject = require('../ParseObject'); 71 | var ParseRelation = require('../ParseRelation'); 72 | 73 | describe('ParseRelation', () => { 74 | it('can be constructed with a reference parent and key', () => { 75 | var parent = new ParseObject('Item'); 76 | parent.id = 'I1'; 77 | var r = new ParseRelation(parent, 'shipments'); 78 | expect(r.parent).toBe(parent); 79 | expect(r.key).toBe('shipments'); 80 | expect(r.targetClassName).toBe(null); 81 | }); 82 | 83 | it('can add objects to a relation', () => { 84 | var parent = new ParseObject('Item'); 85 | parent.id = 'I1'; 86 | var r = new ParseRelation(parent, 'shipments'); 87 | var o = new ParseObject('Delivery'); 88 | o.id = 'D1'; 89 | var p = r.add(o); 90 | expect(p).toBeTruthy(); 91 | expect(r.toJSON()).toEqual({ 92 | __type: 'Relation', 93 | className: 'Delivery' 94 | }); 95 | expect(parent.op('shipments').toJSON()).toEqual({ 96 | __op: 'AddRelation', 97 | objects: [ 98 | { __type: 'Pointer', objectId: 'D1', className: 'Delivery' } 99 | ] 100 | }); 101 | 102 | var o2 = new ParseObject('Delivery'); 103 | o2.id = 'D2'; 104 | var o3 = new ParseObject('Delivery'); 105 | o3.id = 'D3'; 106 | r.add([o2, o3]); 107 | expect(r.toJSON()).toEqual({ 108 | __type: 'Relation', 109 | className: 'Delivery' 110 | }); 111 | expect(parent.op('shipments').toJSON()).toEqual({ 112 | __op: 'AddRelation', 113 | objects: [ 114 | { __type: 'Pointer', objectId: 'D1', className: 'Delivery' }, 115 | { __type: 'Pointer', objectId: 'D2', className: 'Delivery' }, 116 | { __type: 'Pointer', objectId: 'D3', className: 'Delivery' }, 117 | ] 118 | }); 119 | }); 120 | 121 | it('can remove objects from a relation', () => { 122 | var parent = new ParseObject('Item'); 123 | parent.id = 'I2'; 124 | var r = new ParseRelation(parent, 'shipments'); 125 | var o = new ParseObject('Delivery'); 126 | o.id = 'D1'; 127 | r.remove(o); 128 | expect(r.toJSON()).toEqual({ 129 | __type: 'Relation', 130 | className: 'Delivery' 131 | }); 132 | expect(parent.op('shipments').toJSON()).toEqual({ 133 | __op: 'RemoveRelation', 134 | objects: [ 135 | { __type: 'Pointer', objectId: 'D1', className: 'Delivery' } 136 | ] 137 | }); 138 | 139 | var o2 = new ParseObject('Delivery'); 140 | o2.id = 'D2'; 141 | var o3 = new ParseObject('Delivery'); 142 | o3.id = 'D3'; 143 | r.remove([o2, o3]); 144 | expect(r.toJSON()).toEqual({ 145 | __type: 'Relation', 146 | className: 'Delivery' 147 | }); 148 | expect(parent.op('shipments').toJSON()).toEqual({ 149 | __op: 'RemoveRelation', 150 | objects: [ 151 | { __type: 'Pointer', objectId: 'D1', className: 'Delivery' }, 152 | { __type: 'Pointer', objectId: 'D2', className: 'Delivery' }, 153 | { __type: 'Pointer', objectId: 'D3', className: 'Delivery' }, 154 | ] 155 | }); 156 | }); 157 | 158 | it('can generate a query for relation objects', () => { 159 | var parent = new ParseObject('Item'); 160 | parent.id = 'I1'; 161 | var r = new ParseRelation(parent, 'shipments'); 162 | var q = r.query(); 163 | expect(q.className).toBe('Item'); 164 | expect(q._extraOptions).toEqual({ 165 | redirectClassNameForKey: 'shipments' 166 | }); 167 | expect(q.where).toEqual({ 168 | $relatedTo: { 169 | object: { 170 | __type: 'Pointer', 171 | objectId: 'I1', 172 | className: 'Item' 173 | }, 174 | key: 'shipments' 175 | } 176 | }); 177 | 178 | r = new ParseRelation(parent, 'shipments'); 179 | var o = new ParseObject('Delivery'); 180 | o.id = 'D1'; 181 | r.add(o); 182 | q = r.query(); 183 | expect(q.className).toBe('Delivery'); 184 | expect(q.where).toEqual({ 185 | $relatedTo: { 186 | object: { 187 | __type: 'Pointer', 188 | className: 'Item', 189 | objectId: 'I1' 190 | }, 191 | key: 'shipments' 192 | } 193 | }); 194 | }); 195 | 196 | it('can ensure it relates to the correct parent and key', () => { 197 | var parent = new ParseObject('Item'); 198 | parent.id = 'I3'; 199 | var r = new ParseRelation(parent, 'shipments'); 200 | expect(r._ensureParentAndKey.bind(r, new ParseObject('Item'), 'shipments')) 201 | .toThrow('Internal Error. Relation retrieved from two different Objects.'); 202 | expect(r._ensureParentAndKey.bind(r, parent, 'partners')) 203 | .toThrow('Internal Error. Relation retrieved from two different keys.'); 204 | expect(r._ensureParentAndKey.bind(r, parent, 'shipments')).not.toThrow(); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /src/__tests__/ParseRole-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../decode'); 11 | jest.dontMock('../ObjectState'); 12 | jest.dontMock('../ParseError'); 13 | jest.dontMock('../ParseObject'); 14 | jest.dontMock('../ParseOp'); 15 | jest.dontMock('../ParseRole'); 16 | 17 | jest.dontMock('redux'); 18 | jest.dontMock('../ReduxActionCreators'); 19 | jest.dontMock('../ReduxStore'); 20 | jest.dontMock('../ReduxReducers'); 21 | jest.dontMock('../ReduxCacheHelper'); 22 | 23 | var ParseACL = require('../ParseACL'); 24 | var ParseError = require('../ParseError'); 25 | var ParseObject = require('../ParseObject'); 26 | var ParseRole = require('../ParseRole'); 27 | 28 | describe('ParseRole', () => { 29 | it('can create Roles', () => { 30 | var role = new ParseRole(); 31 | expect(role.getName()).toBe(undefined); 32 | expect(role.getACL()).toBe(null); 33 | 34 | var acl = new ParseACL({ aUserId: { read: true, write: true } }); 35 | role = new ParseRole('admin', acl); 36 | expect(role.getName()).toBe('admin'); 37 | expect(role.getACL()).toBe(acl); 38 | }); 39 | 40 | it('can validate attributes', () => { 41 | var acl = new ParseACL({ aUserId: { read: true, write: true } }); 42 | var role = new ParseRole('admin', acl); 43 | role.id = '101'; 44 | expect(role.validate({ 45 | name: 'author' 46 | })).toEqual(new ParseError( 47 | ParseError.OTHER_CAUSE, 48 | 'A role\'s name can only be set before it has been saved.' 49 | )); 50 | 51 | role.id = undefined; 52 | expect(role.validate({ 53 | name: 12 54 | })).toEqual(new ParseError( 55 | ParseError.OTHER_CAUSE, 56 | 'A role\'s name must be a String.' 57 | )); 58 | 59 | expect(role.validate({ 60 | name: '$$$' 61 | })).toEqual(new ParseError( 62 | ParseError.OTHER_CAUSE, 63 | 'A role\'s name can be only contain alphanumeric characters, _, ' + 64 | '-, and spaces.' 65 | )); 66 | 67 | expect(role.validate({ 68 | name: 'admin' 69 | })).toBe(false); 70 | }); 71 | 72 | it('can be constructed from JSON', () => { 73 | var role = ParseObject.fromJSON({ 74 | className: '_Role', 75 | objectId: '102', 76 | name: 'admin' 77 | }); 78 | expect(role instanceof ParseObject).toBe(true); 79 | expect(role instanceof ParseRole).toBe(true); 80 | expect(role.getName()).toBe('admin'); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/__tests__/ParseSession-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../CoreManager'); 11 | jest.dontMock('../decode'); 12 | jest.dontMock('../encode'); 13 | jest.dontMock('../isRevocableSession'); 14 | jest.dontMock('../ObjectState') 15 | jest.dontMock('../parseDate'); 16 | jest.dontMock('../ParseError'); 17 | jest.dontMock('../ParseObject'); 18 | jest.dontMock('../ParseOp'); 19 | jest.dontMock('../ParsePromise'); 20 | jest.dontMock('../ParseSession'); 21 | jest.dontMock('../RESTController'); 22 | jest.dontMock('../Storage'); 23 | jest.dontMock('../StorageController.default'); 24 | jest.dontMock('../TaskQueue'); 25 | jest.dontMock('../unique'); 26 | 27 | jest.dontMock('./test_helpers/asyncHelper'); 28 | jest.dontMock('./test_helpers/mockXHR'); 29 | 30 | jest.dontMock('redux'); 31 | jest.dontMock('../ReduxActionCreators'); 32 | jest.dontMock('../ReduxStore'); 33 | jest.dontMock('../ReduxReducers'); 34 | jest.dontMock('../ReduxCacheHelper'); 35 | 36 | var mockUser = function(token) { 37 | this.token = token; 38 | }; 39 | mockUser.prototype.getSessionToken = function() { 40 | return this.token; 41 | }; 42 | mockUser.current = function() { 43 | return null; 44 | }; 45 | jest.setMock('../ParseUser', mockUser); 46 | 47 | var CoreManager = require('../CoreManager'); 48 | var ParseObject = require('../ParseObject'); 49 | var ParsePromise = require('../ParsePromise'); 50 | var ParseSession = require('../ParseSession'); 51 | var ParseUser = require('../ParseUser'); 52 | 53 | var asyncHelper = require('./test_helpers/asyncHelper'); 54 | 55 | CoreManager.set('APPLICATION_ID', 'A'); 56 | CoreManager.set('JAVASCRIPT_KEY', 'B'); 57 | 58 | describe('ParseSession', () => { 59 | it('can be initialized', () => { 60 | var session = new ParseSession(); 61 | session.set('someField', 'someValue'); 62 | expect(session.get('someField')).toBe('someValue'); 63 | 64 | session = new ParseSession({ 65 | someField: 'someValue' 66 | }); 67 | expect(session.get('someField')).toBe('someValue'); 68 | }); 69 | 70 | it('cannot write to readonly fields', () => { 71 | var session = new ParseSession(); 72 | expect(session.set.bind(session, 'createdWith', 'facebook')).toThrow( 73 | 'Cannot modify readonly attribute: createdWith' 74 | ); 75 | expect(session.set.bind(session, 'expiresAt', new Date())).toThrow( 76 | 'Cannot modify readonly attribute: expiresAt' 77 | ); 78 | expect(session.set.bind(session, 'installationId', 'iid')).toThrow( 79 | 'Cannot modify readonly attribute: installationId' 80 | ); 81 | expect(session.set.bind(session, 'restricted', true)).toThrow( 82 | 'Cannot modify readonly attribute: restricted' 83 | ); 84 | expect(session.set.bind(session, 'sessionToken', 'st')).toThrow( 85 | 'Cannot modify readonly attribute: sessionToken' 86 | ); 87 | expect(session.set.bind(session, 'user', null)).toThrow( 88 | 'Cannot modify readonly attribute: user' 89 | ); 90 | }); 91 | 92 | it('exposes the token through a getter', () => { 93 | var session = new ParseSession(); 94 | session._finishFetch({ 95 | id: 'session1', 96 | sessionToken: 'abc123' 97 | }); 98 | expect(session.getSessionToken()).toBe('abc123'); 99 | }); 100 | 101 | it('checks the current user for a revocable token', () => { 102 | expect(ParseSession.isCurrentSessionRevocable()).toBe(false); 103 | mockUser.current = function() { return new mockUser('r:abc123'); }; 104 | expect(ParseSession.isCurrentSessionRevocable()).toBe(true); 105 | mockUser.current = function() { return new mockUser('abc123'); }; 106 | expect(ParseSession.isCurrentSessionRevocable()).toBe(false); 107 | }); 108 | 109 | it('can fetch the full session for the current token', asyncHelper((done) => { 110 | CoreManager.setRESTController({ 111 | request(method, path, body, options) { 112 | expect(method).toBe('GET'); 113 | expect(path).toBe('sessions/me'); 114 | expect(options).toEqual({ 115 | sessionToken: 'abc123' 116 | }); 117 | return ParsePromise.as({ 118 | objectId: 'session1', 119 | sessionToken: 'abc123' 120 | }); 121 | }, 122 | ajax() {} 123 | }); 124 | 125 | mockUser.currentAsync = function() { 126 | return ParsePromise.as(new mockUser('abc123')); 127 | }; 128 | ParseSession.current().then((session) => { 129 | expect(session instanceof ParseSession).toBe(true); 130 | expect(session.id).toBe('session1'); 131 | expect(session.getSessionToken()).toBe('abc123'); 132 | done(); 133 | }); 134 | })); 135 | }); 136 | -------------------------------------------------------------------------------- /src/__tests__/Push-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../CoreManager'); 11 | jest.dontMock('../ParsePromise'); 12 | jest.dontMock('../Push'); 13 | jest.dontMock('./test_helpers/asyncHelper'); 14 | 15 | var mockQuery = function() { 16 | this.where = {}; 17 | }; 18 | mockQuery.prototype = { 19 | toJSON() { 20 | return { 21 | where: this.where 22 | }; 23 | } 24 | }; 25 | jest.setMock('../ParseQuery', mockQuery); 26 | 27 | var CoreManager = require('../CoreManager'); 28 | var ParsePromise = require('../ParsePromise'); 29 | var ParseQuery = require('../ParseQuery'); 30 | var Push = require('../Push'); 31 | 32 | var asyncHelper = require('./test_helpers/asyncHelper'); 33 | 34 | var defaultController = CoreManager.getPushController(); 35 | 36 | describe('Push', () => { 37 | beforeEach(() => { 38 | CoreManager.setPushController({ 39 | send(data, options) { 40 | // Pipe data through so we can test it 41 | return ParsePromise.as(data); 42 | } 43 | }); 44 | }); 45 | 46 | it('can be sent with a where clause', asyncHelper((done) => { 47 | var q = new ParseQuery(); 48 | q.where = { 49 | installationId: '123' 50 | }; 51 | 52 | Push.send({ 53 | where: q 54 | }).then((data) => { 55 | expect(data.where).toEqual({ 56 | installationId: '123' 57 | }); 58 | done(); 59 | }); 60 | })); 61 | 62 | it('can specify a push time with a Date', asyncHelper((done) => { 63 | Push.send({ 64 | push_time: new Date(Date.UTC(2015, 1, 1)) 65 | }).then((data) => { 66 | expect(data.push_time).toBe('2015-02-01T00:00:00.000Z'); 67 | done(); 68 | }); 69 | })); 70 | 71 | it('can specify a push time with a string', asyncHelper((done) => { 72 | Push.send({ 73 | // Local timezone push 74 | push_time: '2015-02-01T00:00:00.000' 75 | }).then((data) => { 76 | expect(data.push_time).toBe('2015-02-01T00:00:00.000'); 77 | done(); 78 | }); 79 | })); 80 | 81 | it('can specify an expiration time', asyncHelper((done) => { 82 | Push.send({ 83 | expiration_time: new Date(Date.UTC(2015, 1, 1)) 84 | }).then((data) => { 85 | expect(data.expiration_time).toBe('2015-02-01T00:00:00.000Z'); 86 | done(); 87 | }); 88 | })); 89 | 90 | it('cannot specify both an expiration time and an expiration interval', () => { 91 | expect(Push.send.bind(null, { 92 | expiration_time: new Date(), 93 | expiration_interval: 518400 94 | })).toThrow('expiration_time and expiration_interval cannot both be set.'); 95 | }); 96 | }); 97 | 98 | describe('PushController', () => { 99 | it('forwards data along', () => { 100 | CoreManager.setPushController(defaultController); 101 | var request = jest.genMockFunction().mockReturnValue({ 102 | _thenRunCallbacks() { 103 | return { 104 | _thenRunCallbacks() {} 105 | }; 106 | } 107 | }); 108 | CoreManager.setRESTController({ 109 | request: request, 110 | ajax: function() {} 111 | }); 112 | 113 | Push.send({ 114 | push_time: new Date(Date.UTC(2015, 1, 1)) 115 | }, { 116 | useMasterKey: true 117 | }); 118 | expect(CoreManager.getRESTController().request.mock.calls[0]).toEqual([ 119 | 'POST', 120 | 'push', 121 | { push_time: '2015-02-01T00:00:00.000Z' }, 122 | { useMasterKey: true} 123 | ]); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/__tests__/Storage-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var CoreManager = require('../CoreManager'); 13 | var ParsePromise = require('../ParsePromise'); 14 | 15 | var asyncHelper = require('./test_helpers/asyncHelper'); 16 | 17 | var mockStorage = {}; 18 | var mockStorageInterface = { 19 | getItem(path) { 20 | return mockStorage[path] || null; 21 | }, 22 | 23 | getItemAsync(path) { 24 | return ParsePromise.as(mockStorageInterface.getItem(path)); 25 | }, 26 | 27 | setItem(path, value) { 28 | mockStorage[path] = value; 29 | }, 30 | 31 | setItemAsync(path, value) { 32 | return ParsePromise.as(mockStorageInterface.setItem(path, value)); 33 | }, 34 | 35 | removeItem(path) { 36 | delete mockStorage[path]; 37 | }, 38 | 39 | removeItemAsync(path) { 40 | return ParsePromise.as(mockStorageInterface.removeItem(path)); 41 | }, 42 | 43 | clear() { 44 | mockStorage = {}; 45 | } 46 | } 47 | 48 | global.localStorage = mockStorageInterface; 49 | 50 | var BrowserStorageController = require('../StorageController.browser'); 51 | 52 | describe('Browser StorageController', () => { 53 | beforeEach(() => { 54 | BrowserStorageController.clear(); 55 | }); 56 | 57 | it('is synchronous', () => { 58 | expect(BrowserStorageController.async).toBe(0); 59 | expect(typeof BrowserStorageController.getItem).toBe('function'); 60 | expect(typeof BrowserStorageController.setItem).toBe('function'); 61 | expect(typeof BrowserStorageController.removeItem).toBe('function'); 62 | }); 63 | 64 | it('can store and retrieve values', () => { 65 | expect(BrowserStorageController.getItem('myKey')).toBe(null); 66 | BrowserStorageController.setItem('myKey', 'myValue'); 67 | expect(BrowserStorageController.getItem('myKey')).toBe('myValue'); 68 | }); 69 | 70 | it('can remove values', () => { 71 | BrowserStorageController.setItem('myKey', 'myValue'); 72 | expect(BrowserStorageController.getItem('myKey')).toBe('myValue'); 73 | BrowserStorageController.removeItem('myKey'); 74 | expect(BrowserStorageController.getItem('myKey')).toBe(null); 75 | }); 76 | }); 77 | 78 | var RNStorageController = require('../StorageController.react-native'); 79 | 80 | describe('React Native StorageController', () => { 81 | beforeEach(() => { 82 | RNStorageController.clear(); 83 | }); 84 | 85 | it('is asynchronous', () => { 86 | expect(RNStorageController.async).toBe(1); 87 | expect(typeof RNStorageController.getItemAsync).toBe('function'); 88 | expect(typeof RNStorageController.setItemAsync).toBe('function'); 89 | expect(typeof RNStorageController.removeItemAsync).toBe('function'); 90 | }); 91 | 92 | it('can store and retrieve values', asyncHelper((done) => { 93 | RNStorageController.getItemAsync('myKey').then((result) => { 94 | expect(result).toBe(null); 95 | return RNStorageController.setItemAsync('myKey', 'myValue'); 96 | }).then(() => { 97 | return RNStorageController.getItemAsync('myKey'); 98 | }).then((result) => { 99 | expect(result).toBe('myValue'); 100 | done(); 101 | }); 102 | })); 103 | 104 | it('can remove values', asyncHelper((done) => { 105 | RNStorageController.setItemAsync('myKey', 'myValue').then(() => { 106 | return RNStorageController.getItemAsync('myKey'); 107 | }).then((result) => { 108 | expect(result).toBe('myValue'); 109 | return RNStorageController.removeItemAsync('myKey'); 110 | }).then(() => { 111 | return RNStorageController.getItemAsync('myKey'); 112 | }).then((result) => { 113 | expect(result).toBe(null); 114 | done(); 115 | }); 116 | })); 117 | }); 118 | 119 | var DefaultStorageController = require('../StorageController.default'); 120 | 121 | describe('Default StorageController', () => { 122 | beforeEach(() => { 123 | DefaultStorageController.clear(); 124 | }); 125 | 126 | it('is synchronous', () => { 127 | expect(DefaultStorageController.async).toBe(0); 128 | expect(typeof DefaultStorageController.getItem).toBe('function'); 129 | expect(typeof DefaultStorageController.setItem).toBe('function'); 130 | expect(typeof DefaultStorageController.removeItem).toBe('function'); 131 | }); 132 | 133 | it('can store and retrieve values', () => { 134 | expect(DefaultStorageController.getItem('myKey')).toBe(null); 135 | DefaultStorageController.setItem('myKey', 'myValue'); 136 | expect(DefaultStorageController.getItem('myKey')).toBe('myValue'); 137 | }); 138 | 139 | it('can remove values', () => { 140 | DefaultStorageController.setItem('myKey', 'myValue'); 141 | expect(DefaultStorageController.getItem('myKey')).toBe('myValue'); 142 | DefaultStorageController.removeItem('myKey'); 143 | expect(DefaultStorageController.getItem('myKey')).toBe(null); 144 | }); 145 | }); 146 | 147 | var Storage = require('../Storage'); 148 | 149 | describe('Storage (Default StorageController)', () => { 150 | beforeEach(() => { 151 | CoreManager.setStorageController(require('../StorageController.default')); 152 | }); 153 | 154 | it('can store and retrieve values', () => { 155 | expect(Storage.getItem('myKey')).toBe(null); 156 | Storage.setItem('myKey', 'myValue'); 157 | expect(Storage.getItem('myKey')).toBe('myValue'); 158 | }); 159 | 160 | it('can remove values', () => { 161 | Storage.setItem('myKey', 'myValue'); 162 | expect(Storage.getItem('myKey')).toBe('myValue'); 163 | Storage.removeItem('myKey'); 164 | expect(Storage.getItem('myKey')).toBe(null); 165 | }); 166 | 167 | it('wraps synchronous methods in async wrappers', asyncHelper((done) => { 168 | Storage.getItemAsync('myKey').then((result) => { 169 | expect(result).toBe(null); 170 | return Storage.setItemAsync('myKey', 'myValue'); 171 | }).then(() => { 172 | return Storage.getItemAsync('myKey'); 173 | }).then((result) => { 174 | expect(result).toBe('myValue'); 175 | return Storage.removeItemAsync('myKey'); 176 | }).then(() => { 177 | return Storage.getItemAsync('myKey'); 178 | }).then((result) => { 179 | done(); 180 | }); 181 | })); 182 | 183 | it('can generate a unique storage path', () => { 184 | expect(Storage.generatePath.bind(null, 'hello')).toThrow( 185 | 'You need to call Parse.initialize before using Parse.' 186 | ); 187 | CoreManager.set('APPLICATION_ID', 'appid'); 188 | expect(Storage.generatePath.bind(null, 12)).toThrow( 189 | 'Tried to get a Storage path that was not a String.' 190 | ); 191 | expect(Storage.generatePath('hello')).toBe('Parse/appid/hello'); 192 | expect(Storage.generatePath('/hello')).toBe('Parse/appid/hello'); 193 | }); 194 | }); 195 | 196 | describe('Storage (Async StorageController)', () => { 197 | beforeEach(() => { 198 | CoreManager.setStorageController( 199 | require('../StorageController.react-native') 200 | ); 201 | }); 202 | 203 | it('throws when using a synchronous method', () => { 204 | expect(Storage.getItem).toThrow( 205 | 'Synchronous storage is not supported by the current storage controller' 206 | ); 207 | expect(Storage.setItem).toThrow( 208 | 'Synchronous storage is not supported by the current storage controller' 209 | ); 210 | expect(Storage.removeItem).toThrow( 211 | 'Synchronous storage is not supported by the current storage controller' 212 | ); 213 | }); 214 | 215 | it('wraps synchronous methods in async wrappers', asyncHelper((done) => { 216 | Storage.getItemAsync('myKey').then((result) => { 217 | expect(result).toBe(null); 218 | return Storage.setItemAsync('myKey', 'myValue'); 219 | }).then(() => { 220 | return Storage.getItemAsync('myKey'); 221 | }).then((result) => { 222 | expect(result).toBe('myValue'); 223 | return Storage.removeItemAsync('myKey'); 224 | }).then(() => { 225 | return Storage.getItemAsync('myKey'); 226 | }).then((result) => { 227 | done(); 228 | }); 229 | })); 230 | }); 231 | -------------------------------------------------------------------------------- /src/__tests__/TaskQueue-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var ParsePromise = require('../ParsePromise'); 13 | var TaskQueue = require('../TaskQueue'); 14 | 15 | describe('TaskQueue', () => { 16 | it('is initialized with an empty queue', () => { 17 | var q = new TaskQueue(); 18 | expect(q.queue).toEqual([]); 19 | }); 20 | 21 | it('runs a single task immediately', () => { 22 | var q = new TaskQueue(); 23 | var p = new ParsePromise(); 24 | var called = false; 25 | var completed = false; 26 | var t = q.enqueue(() => { 27 | called = true; 28 | return p.then(() => { 29 | completed = true; 30 | }); 31 | }); 32 | expect(called).toBe(true); 33 | expect(completed).toBe(false); 34 | p.resolve(); 35 | expect(completed).toBe(true); 36 | expect(t._resolved).toBe(true); 37 | }); 38 | 39 | it('rejects the enqueue promise when the task errors', () => { 40 | var q = new TaskQueue(); 41 | var p = new ParsePromise(); 42 | var called = false; 43 | var completed = false; 44 | var t = q.enqueue(() => { 45 | called = true; 46 | return p; 47 | }); 48 | expect(called).toBe(true); 49 | p.reject('error'); 50 | expect(t._rejected).toBe(true); 51 | }) 52 | 53 | it('can execute a chain of tasks', () => { 54 | var q = new TaskQueue(); 55 | var called = [false, false, false]; 56 | var completed = [false, false, false]; 57 | var promises = [new ParsePromise(), new ParsePromise(), new ParsePromise()]; 58 | q.enqueue(() => { 59 | called[0] = true; 60 | return promises[0].then(() => { 61 | completed[0] = true; 62 | }); 63 | }); 64 | q.enqueue(() => { 65 | called[1] = true; 66 | return promises[1].then(() => { 67 | completed[1] = true; 68 | }); 69 | }); 70 | q.enqueue(() => { 71 | called[2] = true; 72 | return promises[2].then(() => { 73 | completed[2] = true; 74 | }); 75 | }); 76 | expect(called).toEqual([true, false, false]); 77 | expect(completed).toEqual([false, false, false]); 78 | promises[0].resolve(); 79 | expect(called).toEqual([true, true, false]); 80 | expect(completed).toEqual([true, false, false]); 81 | expect(q.queue.length).toBe(2); 82 | promises[1].resolve(); 83 | expect(called).toEqual([true, true, true]); 84 | expect(completed).toEqual([true, true, false]); 85 | expect(q.queue.length).toBe(1); 86 | promises[2].resolve(); 87 | expect(completed).toEqual([true, true, true]); 88 | expect(q.queue.length).toBe(0); 89 | }); 90 | 91 | it('continues the chain when a task errors', () => { 92 | var q = new TaskQueue(); 93 | var called = [false, false, false]; 94 | var promises = [new ParsePromise(), new ParsePromise(), new ParsePromise()]; 95 | q.enqueue(() => { 96 | called[0] = true; 97 | return promises[0]; 98 | }); 99 | q.enqueue(() => { 100 | called[1] = true; 101 | return promises[1]; 102 | }); 103 | q.enqueue(() => { 104 | called[2] = true; 105 | return promises[2]; 106 | }); 107 | expect(called).toEqual([true, false, false]); 108 | promises[0].reject(); 109 | expect(called).toEqual([true, true, false]); 110 | expect(q.queue.length).toBe(2); 111 | promises[1].resolve(); 112 | expect(called).toEqual([true, true, true]); 113 | expect(q.queue.length).toBe(1); 114 | promises[2].resolve(); 115 | expect(q.queue.length).toBe(0); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/__tests__/arrayContainsObject-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../arrayContainsObject'); 11 | 12 | var localCount = 0; 13 | var mockObject = function(className, id) { 14 | this.className = className; 15 | this.id = id; 16 | if (!id) { 17 | this._localId = 'local' + localCount++; 18 | } 19 | } 20 | mockObject.prototype._getId = function() { 21 | return this.id || this._localId; 22 | } 23 | jest.setMock('../ParseObject', mockObject); 24 | 25 | var arrayContainsObject = require('../arrayContainsObject'); 26 | var ParseObject = require('../ParseObject'); 27 | 28 | describe('arrayContainsObject', () => { 29 | it('detects objects by their id', () => { 30 | var o = new ParseObject('Item'); 31 | expect(arrayContainsObject([], o)).toBe(false); 32 | expect(arrayContainsObject([1, 'string'], o)).toBe(false); 33 | expect(arrayContainsObject([o], o)).toBe(true); 34 | expect(arrayContainsObject([ 35 | new ParseObject('Item') 36 | ], new ParseObject('Item'))).toBe(false); 37 | expect(arrayContainsObject([ 38 | new ParseObject('Item', 'a'), 39 | new ParseObject('Item', 'b') 40 | ], new ParseObject('Item', 'a'))).toBe(true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__tests__/canBeSerialized-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../canBeSerialized'); 11 | 12 | jest.dontMock('../ReduxCacheHelper'); 13 | 14 | function mockObject(id, attributes) { 15 | this.id = id; 16 | this.attributes = attributes; 17 | } 18 | mockObject.registerSubclass = function() {}; 19 | jest.setMock('../ParseObject', mockObject); 20 | 21 | function mockFile(url) { 22 | this._url = url; 23 | } 24 | mockFile.prototype.url = function() { 25 | return this._url; 26 | }; 27 | jest.setMock('../ParseFile', mockFile); 28 | 29 | var canBeSerialized = require('../canBeSerialized'); 30 | var ParseFile = require('../ParseFile'); 31 | var ParseObject = require('../ParseObject'); 32 | 33 | describe('canBeSerialized', () => { 34 | it('returns true for anything that is not a ParseObject', () => { 35 | expect(canBeSerialized(12)).toBe(true); 36 | expect(canBeSerialized('string')).toBe(true); 37 | expect(canBeSerialized(false)).toBe(true); 38 | expect(canBeSerialized([])).toBe(true); 39 | expect(canBeSerialized({})).toBe(true); 40 | }); 41 | 42 | it('validates primitives', () => { 43 | var o = new ParseObject('oid', { 44 | a: 12, 45 | b: 'string', 46 | c: false 47 | }); 48 | expect(canBeSerialized(o)).toBe(true); 49 | }); 50 | 51 | it('returns false when a child is an unsaved object or file', () => { 52 | var o = new ParseObject('oid', { 53 | a: new ParseObject() 54 | }); 55 | expect(canBeSerialized(o)).toBe(false); 56 | 57 | o = new ParseObject('oid', { 58 | a: new ParseObject('oid2', {}) 59 | }); 60 | expect(canBeSerialized(o)).toBe(true); 61 | 62 | o = new ParseObject('oid', { 63 | a: new ParseFile() 64 | }); 65 | expect(canBeSerialized(o)).toBe(false); 66 | 67 | o = new ParseObject('oid', { 68 | a: new ParseFile('http://files.parsetfss.com/a/parse.txt') 69 | }); 70 | expect(canBeSerialized(o)).toBe(true); 71 | }); 72 | 73 | it('returns true when all children have an id', () => { 74 | var child = new ParseObject('child', {}); 75 | var parent = new ParseObject(undefined, { 76 | child: child 77 | }); 78 | child.attributes.parent = parent; 79 | expect(canBeSerialized(parent)).toBe(true); 80 | expect(canBeSerialized(child)).toBe(false); 81 | }); 82 | 83 | it('traverses nested arrays and objects', () => { 84 | var o = new ParseObject('oid', { 85 | a: { 86 | a: { 87 | a: { 88 | b: new ParseObject() 89 | } 90 | } 91 | } 92 | }); 93 | expect(canBeSerialized(o)).toBe(false); 94 | 95 | o = new ParseObject('oid', { 96 | a: { 97 | a: { 98 | a: { 99 | b: new ParseObject('oid2') 100 | } 101 | } 102 | } 103 | }); 104 | expect(canBeSerialized(o)).toBe(true); 105 | 106 | o = new ParseObject('oid', { 107 | a: [1, 2, 3, { 108 | b: new ParseObject() 109 | }] 110 | }); 111 | expect(canBeSerialized(o)).toBe(false); 112 | 113 | o = new ParseObject('oid', { 114 | a: [1, 2, 3, { 115 | b: new ParseObject('oid2') 116 | }] 117 | }); 118 | expect(canBeSerialized(o)).toBe(true); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/__tests__/decode-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../decode'); 11 | jest.dontMock('../ParseFile'); 12 | jest.dontMock('../ParseGeoPoint'); 13 | 14 | jest.dontMock('../ReduxCacheHelper'); 15 | 16 | var decode = require('../decode'); 17 | 18 | var ParseFile = require('../ParseFile'); 19 | var ParseGeoPoint = require('../ParseGeoPoint'); 20 | var ParseObject = require('../ParseObject'); 21 | 22 | describe('decode', () => { 23 | it('ignores primitives', () => { 24 | expect(decode(undefined)).toBe(undefined); 25 | expect(decode(null)).toBe(null); 26 | expect(decode(true)).toBe(true); 27 | expect(decode(12)).toBe(12); 28 | expect(decode('string')).toBe('string'); 29 | }); 30 | 31 | it('decodes dates', () => { 32 | expect(decode({ 33 | __type: 'Date', 34 | iso: '2015-02-01T00:00:00.000Z' 35 | })).toEqual(new Date(Date.UTC(2015, 1))); 36 | }); 37 | 38 | it('decodes GeoPoints', () => { 39 | var point = decode({ 40 | __type: 'GeoPoint', 41 | latitude: 40.5, 42 | longitude: 50.4 43 | }); 44 | expect(point instanceof ParseGeoPoint).toBe(true); 45 | expect(point.latitude).toBe(40.5); 46 | expect(point.longitude).toBe(50.4); 47 | }); 48 | 49 | it('decodes Files', () => { 50 | var file = decode({ 51 | __type: 'File', 52 | name: 'parse.txt', 53 | url: 'https://files.parsetfss.com/a/parse.txt' 54 | }); 55 | expect(file instanceof ParseFile).toBe(true); 56 | expect(file.name()).toBe('parse.txt'); 57 | expect(file.url()).toBe('https://files.parsetfss.com/a/parse.txt'); 58 | }); 59 | 60 | it('decodes Relations', () => { 61 | var obj = decode({ 62 | __type: 'Relation', 63 | className: 'Delivery' 64 | }); 65 | expect(obj.constructor.mock.calls[0]).toEqual([ 66 | null, 67 | null 68 | ]); 69 | expect(obj.targetClassName).toBe('Delivery'); 70 | }); 71 | 72 | it('decodes Pointers', () => { 73 | var data = { 74 | __type: 'Pointer', 75 | className: 'Item', 76 | objectId: '1001' 77 | }; 78 | decode(data); 79 | expect(ParseObject.fromJSON.mock.calls[0][0]).toEqual(data); 80 | }); 81 | 82 | it('decodes ParseObjects', () => { 83 | var data = { 84 | __type: 'Object', 85 | className: 'Item', 86 | objectId: '1001' 87 | }; 88 | decode(data); 89 | expect(ParseObject.fromJSON.mock.calls[1][0]).toEqual(data); 90 | }); 91 | 92 | it('iterates over arrays', () => { 93 | expect(decode([ 94 | { __type: 'Date', iso: '2015-02-01T00:00:00.000Z' }, 95 | 12, 96 | 'string' 97 | ])).toEqual([ 98 | new Date(Date.UTC(2015, 1)), 99 | 12, 100 | 'string' 101 | ]); 102 | }); 103 | 104 | it('iterates over objects', () => { 105 | expect(decode({ 106 | empty: null, 107 | when: { __type: 'Date', iso: '2015-04-01T00:00:00.000Z' }, 108 | count: 15 109 | })).toEqual({ 110 | empty: null, 111 | when: new Date(Date.UTC(2015, 3)), 112 | count: 15 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/__tests__/encode-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../encode'); 11 | jest.dontMock('../ParseACL'); 12 | jest.dontMock('../ParseFile'); 13 | jest.dontMock('../ParseGeoPoint'); 14 | 15 | jest.dontMock('../ReduxCacheHelper'); 16 | 17 | var mockObject = function(className) { 18 | this.className = className; 19 | }; 20 | mockObject.registerSubclass = function() {}; 21 | mockObject.prototype = { 22 | _getServerData() { 23 | return this._serverData; 24 | }, 25 | toPointer() { 26 | return 'POINTER'; 27 | }, 28 | dirty() {}, 29 | toJSON() { 30 | return this.attributes; 31 | }, 32 | _toFullJSON(seen) { 33 | var json = { 34 | __type: 'Object', 35 | className: this.className 36 | }; 37 | for (var attr in this.attributes) { 38 | json[attr] = encode(this.attributes[attr], false, false, seen.concat(this)); 39 | } 40 | return json; 41 | } 42 | }; 43 | jest.setMock('../ParseObject', mockObject); 44 | 45 | var encode = require('../encode'); 46 | var ParseACL = require('../ParseACL'); 47 | var ParseFile = require('../ParseFile'); 48 | var ParseGeoPoint = require('../ParseGeoPoint'); 49 | var ParseObject = require('../ParseObject'); 50 | var ParseRelation = require('../ParseRelation'); 51 | 52 | describe('encode', () => { 53 | it('ignores primitives', () => { 54 | expect(encode(undefined)).toBe(undefined); 55 | expect(encode(null)).toBe(null); 56 | expect(encode(true)).toBe(true); 57 | expect(encode(12)).toBe(12); 58 | expect(encode('string')).toBe('string'); 59 | }); 60 | 61 | it('encodes dates', () => { 62 | expect(encode(new Date(Date.UTC(2015, 1)))).toEqual({ 63 | __type: 'Date', 64 | iso: '2015-02-01T00:00:00.000Z' 65 | }); 66 | expect(encode.bind(null, new Date(Date.parse(null)))).toThrow( 67 | 'Tried to encode an invalid date.' 68 | ); 69 | }); 70 | 71 | it('encodes regular expressions', () => { 72 | expect(encode(new RegExp('^hello'))).toEqual('^hello'); 73 | expect(encode(/a[^b]+c/g)).toEqual('a[^b]+c'); 74 | }); 75 | 76 | it('encodes GeoPoints', () => { 77 | var point = new ParseGeoPoint(40.5, 50.4); 78 | expect(encode(point)).toEqual({ 79 | __type: 'GeoPoint', 80 | latitude: 40.5, 81 | longitude: 50.4 82 | }); 83 | }); 84 | 85 | it('encodes Files', () => { 86 | var file = new ParseFile('parse.txt'); 87 | expect(encode.bind(null, file)).toThrow('Tried to encode an unsaved file.'); 88 | file._url = 'https://files.parsetfss.com/a/parse.txt'; 89 | expect(encode(file)).toEqual({ 90 | __type: 'File', 91 | name: 'parse.txt', 92 | url: 'https://files.parsetfss.com/a/parse.txt' 93 | }); 94 | }); 95 | 96 | it('encodes Relations', () => { 97 | var rel = new ParseRelation(); 98 | var json = encode(rel); 99 | expect(rel.toJSON.mock.calls.length).toBe(1); 100 | }); 101 | 102 | it('encodes ACLs', () => { 103 | var acl = new ParseACL({ aUserId: { read: true, write: false } }); 104 | expect(encode(acl)).toEqual({ 105 | aUserId: { 106 | read: true, 107 | write: false 108 | } 109 | }); 110 | }); 111 | 112 | it('encodes ParseObjects', () => { 113 | var obj = new ParseObject('Item'); 114 | obj._serverData = {}; 115 | expect(encode(obj)).toEqual('POINTER'); 116 | 117 | obj._serverData = obj.attributes = { 118 | str: 'string', 119 | date: new Date(Date.UTC(2015, 1, 1)) 120 | }; 121 | expect(encode(obj)).toEqual({ 122 | __type: 'Object', 123 | className: 'Item', 124 | str: 'string', 125 | date: { 126 | __type: 'Date', 127 | iso: '2015-02-01T00:00:00.000Z' 128 | } 129 | }); 130 | 131 | obj.attributes.self = obj; 132 | expect(encode(obj)).toEqual({ 133 | __type: 'Object', 134 | className: 'Item', 135 | str: 'string', 136 | date: { 137 | __type: 'Date', 138 | iso: '2015-02-01T00:00:00.000Z' 139 | }, 140 | self: 'POINTER' 141 | }); 142 | }); 143 | 144 | it('does not encode ParseObjects when they are disallowed', () => { 145 | var obj = new ParseObject('Item'); 146 | expect(encode.bind(null, obj, true)).toThrow( 147 | 'Parse Objects not allowed here' 148 | ); 149 | }); 150 | 151 | it('iterates over arrays', () => { 152 | var arr = [12, new Date(Date.UTC(2015, 1)), 'str']; 153 | expect(encode(arr)).toEqual([ 154 | 12, 155 | { __type: 'Date', iso: '2015-02-01T00:00:00.000Z' }, 156 | 'str' 157 | ]); 158 | 159 | arr = [arr]; 160 | expect(encode(arr)).toEqual([[ 161 | 12, 162 | { __type: 'Date', iso: '2015-02-01T00:00:00.000Z' }, 163 | 'str' 164 | ]]); 165 | }); 166 | 167 | it('iterates over objects', () => { 168 | var obj = { 169 | num: 12, 170 | date: new Date(Date.UTC(2015, 1)), 171 | str: 'abc' 172 | }; 173 | expect(encode(obj)).toEqual({ 174 | num: 12, 175 | date: { __type: 'Date', iso: '2015-02-01T00:00:00.000Z' }, 176 | str: 'abc' 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/__tests__/equals-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var equals = require('../equals'); 13 | var ParseACL = require('../ParseACL'); 14 | var ParseFile = require('../ParseFile'); 15 | var ParseGeoPoint = require('../ParseGeoPoint'); 16 | var ParseObject = require('../ParseObject'); 17 | 18 | describe('equals', () => { 19 | it('tests equality of primitives', () => { 20 | expect(equals(1, 'string')).toBe(false); 21 | expect(equals(1, true)).toBe(false); 22 | expect(equals(1, undefined)).toBe(false); 23 | expect(equals(1, null)).toBe(false); 24 | expect(equals(1, {})).toBe(false); 25 | expect(equals(1, 4)).toBe(false); 26 | expect(equals(1, 1)).toBe(true); 27 | 28 | expect(equals(null, 'string')).toBe(false); 29 | expect(equals(true, 'string')).toBe(false); 30 | expect(equals(undefined, 'string')).toBe(false); 31 | expect(equals(true, 'string')).toBe(false); 32 | expect(equals({}, 'string')).toBe(false); 33 | expect(equals('abc', 'def')).toBe(false); 34 | expect(equals('abc', 'abc')).toBe(true); 35 | 36 | expect(equals(false, false)).toBe(true); 37 | expect(equals(true, true)).toBe(true); 38 | expect(equals(true, false)).toBe(false); 39 | 40 | expect(equals(null, null)).toBe(true); 41 | expect(equals(undefined, undefined)).toBe(true); 42 | expect(equals(null, undefined)).toBe(false); 43 | }); 44 | 45 | it('tests equality of objects and arrays', () => { 46 | var a = {}; 47 | expect(equals(a, a)).toBe(true); 48 | expect(equals({}, {})).toBe(true); 49 | expect(equals({ a: 1 }, { a: 1 })).toBe(true); 50 | expect(equals({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true); 51 | expect(equals({ a: 1, b: 2 }, { b: 2 })).toBe(false); 52 | expect(equals({ a: {} }, { a: {} })).toBe(true); 53 | 54 | expect(equals([], [])).toBe(true); 55 | expect(equals([1, 2, 3], [1, 2, 3])).toBe(true); 56 | expect(equals([1, 2, 3], [3, 2, 1])).toBe(false); 57 | expect(equals([1, 2, 3], [1, 2])).toBe(false); 58 | expect(equals([{ c: 3 }, 2, 1], [{ c: 3 }, 2, 1])).toBe(true); 59 | }); 60 | 61 | it('tests equality of ACLs', () => { 62 | // Defer to ParseACL tests for the majority of testing 63 | var a = new ParseACL(); 64 | var b = new ParseACL(); 65 | 66 | expect(equals(a, a)).toBe(true); 67 | expect(equals(a, b)).toBe(true); 68 | expect(equals(b, a)).toBe(true); 69 | 70 | a.setPublicReadAccess(true); 71 | expect(equals(a, a)).toBe(true); 72 | expect(equals(a, b)).toBe(false); 73 | expect(equals(b, a)).toBe(false); 74 | }); 75 | 76 | it('tests equality of GeoPoints', () => { 77 | // Defer to ParseGeoPoint tests for the majority of testing 78 | var a = new ParseGeoPoint(40, 40); 79 | expect(equals(a, a)).toBe(true); 80 | 81 | var b = new ParseGeoPoint(40, 40); 82 | expect(equals(a, b)).toBe(true); 83 | expect(equals(b, a)).toBe(true); 84 | 85 | b = new ParseGeoPoint(50, 40); 86 | expect(equals(a, b)).toBe(false); 87 | expect(equals(b, a)).toBe(false); 88 | }); 89 | 90 | it('tests equality of Files', () => { 91 | // Defer to ParseFile tests for the majority of testing 92 | var a = new ParseFile('parse.txt', [61, 170, 236, 120]); 93 | var b = new ParseFile('parse.txt', [61, 170, 236, 120]); 94 | 95 | expect(equals(a, a)).toBe(true); 96 | // unsaved files are never equal 97 | expect(equals(a, b)).toBe(false); 98 | a = ParseFile.fromJSON({ 99 | __type: 'File', 100 | name: 'parse.txt', 101 | url: 'http://files.parsetfss.com/a/parse.txt' 102 | }); 103 | b = ParseFile.fromJSON({ 104 | __type: 'File', 105 | name: 'parse.txt', 106 | url: 'http://files.parsetfss.com/a/parse.txt' 107 | }); 108 | 109 | expect(equals(a, b)).toBe(true); 110 | }); 111 | 112 | it('tests equality of ParseObjects', () => { 113 | // Defer to ParseObject tests for the majority of testing 114 | var a = new ParseObject('Item'); 115 | var b = new ParseObject('Item'); 116 | expect(equals(a, a)).toBe(true); 117 | expect(equals(a, b)).toBe(false); 118 | 119 | a.id = 'myobj'; 120 | b.id = 'myobj'; 121 | expect(equals(a, b)).toBe(true); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/__tests__/escape-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var escape = require('../escape.js'); 13 | 14 | describe('escape', () => { 15 | it('escapes special HTML characters', () => { 16 | expect(escape('&')).toBe('&'); 17 | expect(escape('<')).toBe('<'); 18 | expect(escape('>')).toBe('>'); 19 | expect(escape('\'')).toBe('''); 20 | expect(escape('"')).toBe('"'); 21 | expect(escape('/')).toBe('/'); 22 | 23 | // globally escapes 24 | expect(escape('

      left & right

      ')) 25 | .toBe('<p>left & right</p>'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/__tests__/parseDate-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.autoMockOff(); 11 | 12 | var parseDate = require('../parseDate'); 13 | 14 | describe('parseDate', () => { 15 | it('returns a Date for valid strings', () => { 16 | expect(Number(parseDate('2013-12-14T04:51:19.582Z'))).toBe( 17 | Number(new Date(Date.UTC(2013, 11, 14, 4, 51, 19, 582))) 18 | ); 19 | expect(Number(parseDate('2013-12-14T04:51:19Z'))).toBe( 20 | Number(new Date(Date.UTC(2013, 11, 14, 4, 51, 19))) 21 | ); 22 | }); 23 | 24 | it('returns null for invalid strings', () => { 25 | expect(parseDate('asdf')).toBe(null); 26 | }); 27 | }) 28 | -------------------------------------------------------------------------------- /src/__tests__/test_helpers/asyncHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | // We need this until Jest finishes upgrading to Jasmine 2.0 11 | export default function asyncHelper(fn) { 12 | var finished = false; 13 | var done = function() { 14 | finished = true; 15 | }; 16 | 17 | return function() { 18 | runs(function() { 19 | fn(done); 20 | }); 21 | 22 | waitsFor(function() { 23 | return finished; 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/test_helpers/mockXHR.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | /** 11 | * Mock an XMLHttpRequest by pre-defining the statuses and results that it 12 | * return. 13 | * `results` is an array of objects of the form: 14 | * { status: ..., response: ... } 15 | * where status is a HTTP status number and result is a JSON object to pass 16 | * alongside it. 17 | */ 18 | function mockXHR(results) { 19 | var XHR = function() { }; 20 | var attempts = 0; 21 | XHR.prototype = { 22 | open: function() { }, 23 | setRequestHeader: function() { }, 24 | send: function() { 25 | this.status = results[attempts].status; 26 | this.responseText = JSON.stringify(results[attempts].response || {}); 27 | this.readyState = 4; 28 | attempts++; 29 | this.onreadystatechange(); 30 | } 31 | }; 32 | return XHR; 33 | } 34 | 35 | module.exports = mockXHR; 36 | -------------------------------------------------------------------------------- /src/__tests__/unique-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../arrayContainsObject'); 11 | jest.dontMock('../unique'); 12 | 13 | var localCount = 0; 14 | var mockObject = function(className, id) { 15 | this.className = className; 16 | this.id = id; 17 | if (!id) { 18 | this._localId = 'local' + localCount++; 19 | } 20 | } 21 | mockObject.prototype._getId = function() { 22 | return this.id || this._localId; 23 | } 24 | jest.setMock('../ParseObject', mockObject); 25 | 26 | var unique = require('../unique'); 27 | var ParseObject = require('../ParseObject'); 28 | 29 | describe('unique', () => { 30 | it('produces an array with unique elements', () => { 31 | expect(unique([])).toEqual([]); 32 | expect(unique([1])).toEqual([1]); 33 | expect(unique([3, 4, 1])).toEqual([3, 4, 1]); 34 | expect(unique([3, 4, 3, 1])).toEqual([3, 4, 1]); 35 | expect(unique([2, 2, 2, 2, 2, 2, 2])).toEqual([2]); 36 | expect(unique(['a', 'b', 'c', 'a', 'd'])).toEqual(['a', 'b', 'c', 'd']); 37 | }); 38 | 39 | it('dedups objects by their id', () => { 40 | var o = new ParseObject('Item'); 41 | expect(unique([o, o, o])).toEqual([o]); 42 | expect(unique([ 43 | new ParseObject('Item'), 44 | new ParseObject('Item') 45 | ]).length).toBe(2); 46 | expect(unique([ 47 | new ParseObject('Item', 'a'), 48 | new ParseObject('Item', 'b'), 49 | new ParseObject('Item', 'a') 50 | ])).toEqual([ 51 | new ParseObject('Item', 'a'), 52 | new ParseObject('Item', 'b') 53 | ]); 54 | expect(unique([ 55 | new ParseObject('Item', 'a'), 56 | new ParseObject('Item', 'b'), 57 | new ParseObject('Item', 'b'), 58 | new ParseObject('Item', 'a') 59 | ])).toEqual([ 60 | new ParseObject('Item', 'a'), 61 | new ParseObject('Item', 'b') 62 | ]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/__tests__/unsavedChildren-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | jest.dontMock('../ParseFile'); 11 | jest.dontMock('../unsavedChildren'); 12 | 13 | jest.dontMock('../ReduxCacheHelper'); 14 | 15 | function mockObject({ className, localId, id, attributes, dirty }) { 16 | this.className = className; 17 | this.localId = localId; 18 | this.id = id; 19 | this.attributes = attributes; 20 | this._dirty = !!dirty; 21 | } 22 | mockObject.registerSubclass = function() {}; 23 | mockObject.prototype = { 24 | _getId() { 25 | return this.id || this.localId; 26 | }, 27 | dirty() { 28 | return this._dirty; 29 | } 30 | }; 31 | jest.setMock('../ParseObject', mockObject); 32 | 33 | var ParseFile = require('../ParseFile'); 34 | var ParseObject = require('../ParseObject'); 35 | var unsavedChildren = require('../unsavedChildren'); 36 | 37 | describe('unsavedChildren', () => { 38 | it('finds unsaved files', () => { 39 | var files = [ 40 | new ParseFile('parse1.txt', [61, 170, 236, 120]), 41 | new ParseFile('parse2.txt', [61, 170, 236, 120]), 42 | new ParseFile('parse3.txt', [61, 170, 236, 120]) 43 | ]; 44 | 45 | var f = new ParseObject({ 46 | className: 'Folder', 47 | id: '121', 48 | attributes: { 49 | a: files[0], 50 | b: files[1], 51 | c: files[2], 52 | }, 53 | }); 54 | expect(unsavedChildren(f)).toEqual([ 55 | files[0], files[1], files[2] 56 | ]); 57 | 58 | f.attributes = { 59 | files: files 60 | }; 61 | expect(unsavedChildren(f)).toEqual([ 62 | files[0], files[1], files[2] 63 | ]); 64 | 65 | f.attributes = { 66 | files: { 67 | a: files[0], 68 | b: files[1], 69 | c: files[2] 70 | } 71 | }; 72 | expect(unsavedChildren(f)).toEqual([ 73 | files[0], files[1], files[2] 74 | ]); 75 | }); 76 | 77 | it('only returns unique files', () => { 78 | var file = new ParseFile('parse1.txt', [61, 170, 236, 120]); 79 | var f = new ParseObject({ 80 | className: 'Folder', 81 | id: '121', 82 | attributes: { 83 | a: file, 84 | b: file, 85 | c: file, 86 | }, 87 | }); 88 | expect(unsavedChildren(f)).toEqual([ file ]); 89 | }); 90 | 91 | it('finds unsaved child objects', () => { 92 | var a = new ParseObject({ 93 | className: 'File', 94 | localId: 'local0', 95 | attributes: {}, 96 | dirty: true 97 | }); 98 | var b = new ParseObject({ 99 | className: 'File', 100 | localId: 'local1', 101 | attributes: {}, 102 | dirty: true 103 | }); 104 | var f = new ParseObject({ 105 | className: 'Folder', 106 | id: '121', 107 | attributes: { 108 | a: a, 109 | b: b 110 | }, 111 | }); 112 | 113 | expect(unsavedChildren(f)).toEqual([ a, b ]); 114 | 115 | f.attributes = { 116 | contents: [ a, b ] 117 | }; 118 | 119 | expect(unsavedChildren(f)).toEqual([ a, b ]); 120 | 121 | f.attributes = { 122 | contents: { 123 | a: a, 124 | b: b 125 | } 126 | }; 127 | 128 | expect(unsavedChildren(f)).toEqual([ a, b ]); 129 | }); 130 | 131 | it('throws on nested objects without ids', () => { 132 | var a = new ParseObject({ 133 | className: 'File', 134 | localId: 'local0', 135 | attributes: {}, 136 | dirty: true 137 | }); 138 | var b = new ParseObject({ 139 | className: 'File', 140 | localId: 'local1', 141 | attributes: { 142 | a: a 143 | }, 144 | dirty: true 145 | }); 146 | var f = new ParseObject({ 147 | className: 'Folder', 148 | id: '121', 149 | attributes: { 150 | b: b 151 | }, 152 | }); 153 | 154 | expect(unsavedChildren.bind(null, f)).toThrow( 155 | 'Cannot create a pointer to an unsaved Object.' 156 | ); 157 | }); 158 | 159 | it('can explicitly allow nested objects without ids', () => { 160 | var a = new ParseObject({ 161 | className: 'Folder', 162 | localId: 'local0', 163 | dirty: true, 164 | attributes: {} 165 | }); 166 | var b = new ParseObject({ 167 | className: 'Folder', 168 | localId: 'local1', 169 | dirty: true, 170 | attributes: {} 171 | }); 172 | var c = new ParseObject({ 173 | className: 'File', 174 | localId: 'local2', 175 | dirty: true, 176 | attributes: {} 177 | }); 178 | 179 | a.attributes.items = [b]; 180 | b.attributes.items = [c]; 181 | 182 | expect(unsavedChildren(a, true)).toEqual([ b, c ]); 183 | }); 184 | 185 | it('does not revisit objects', () => { 186 | var a = new ParseObject({ 187 | className: 'File', 188 | id: '130', 189 | attributes: { 190 | b: new ParseObject({ 191 | className: 'File', 192 | localId: '131', 193 | attributes: {}, 194 | dirty: true 195 | }) 196 | }, 197 | dirty: true 198 | }); 199 | a.attributes.b.attributes.a = a; 200 | 201 | expect(unsavedChildren(a)).toEqual([ a.attributes.b ]); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /src/arrayContainsObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseObject from './ParseObject'; 13 | 14 | export default function arrayContainsObject( 15 | array: Array, 16 | object: ParseObject 17 | ): boolean { 18 | if (array.indexOf(object) > -1) { 19 | return true; 20 | } 21 | for (var i = 0; i < array.length; i++) { 22 | if ((array[i] instanceof ParseObject) && 23 | array[i].className === object.className && 24 | array[i]._getId() === object._getId() 25 | ) { 26 | return true; 27 | } 28 | } 29 | return false; 30 | } 31 | -------------------------------------------------------------------------------- /src/canBeSerialized.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseFile from './ParseFile'; 13 | import ParseObject from './ParseObject'; 14 | import ParseRelation from './ParseRelation'; 15 | 16 | export default function canBeSerialized(obj: ParseObject): boolean { 17 | if (!(obj instanceof ParseObject)) { 18 | return true; 19 | } 20 | var attributes = obj.attributes; 21 | for (var attr in attributes) { 22 | var val = attributes[attr]; 23 | if (!canBeSerializedHelper(val)) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | 30 | function canBeSerializedHelper(value: any): boolean { 31 | if (typeof value !== 'object') { 32 | return true; 33 | } 34 | if (value instanceof ParseRelation) { 35 | return true; 36 | } 37 | if (value instanceof ParseObject) { 38 | return !!value.id; 39 | } 40 | if (value instanceof ParseFile) { 41 | if (value.url()) { 42 | return true; 43 | } 44 | return false; 45 | } 46 | if (Array.isArray(value)) { 47 | for (var i = 0; i < value.length; i++) { 48 | if (!canBeSerializedHelper(value[i])) { 49 | return false; 50 | } 51 | } 52 | return true; 53 | } 54 | for (var k in value) { 55 | if (!canBeSerializedHelper(value[k])) { 56 | return false; 57 | } 58 | } 59 | return true; 60 | } 61 | -------------------------------------------------------------------------------- /src/decode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseACL from './ParseACL'; 13 | import ParseFile from './ParseFile'; 14 | import ParseGeoPoint from './ParseGeoPoint'; 15 | import ParseObject from './ParseObject'; 16 | import { opFromJSON } from './ParseOp'; 17 | import ParseRelation from './ParseRelation'; 18 | 19 | export default function decode(value: any) { 20 | if (value === null || typeof value !== 'object') { 21 | return value; 22 | } 23 | if (Array.isArray(value)) { 24 | var dup = []; 25 | value.forEach((v, i) => { 26 | dup[i] = decode(v); 27 | }); 28 | return dup; 29 | } 30 | if (typeof value.__op === 'string') { 31 | return opFromJSON(value); 32 | } 33 | if (value.__type === 'Pointer' && value.className) { 34 | return ParseObject.fromJSON(value); 35 | } 36 | if (value.__type === 'Object' && value.className) { 37 | return ParseObject.fromJSON(value); 38 | } 39 | if (value.__type === 'Relation') { 40 | // The parent and key fields will be populated by the parent 41 | var relation = new ParseRelation(null, null); 42 | relation.targetClassName = value.className; 43 | return relation; 44 | } 45 | if (value.__type === 'Date') { 46 | return new Date(value.iso); 47 | } 48 | if (value.__type === 'File') { 49 | return ParseFile.fromJSON(value); 50 | } 51 | if (value.__type === 'GeoPoint') { 52 | return new ParseGeoPoint({ 53 | latitude: value.latitude, 54 | longitude: value.longitude 55 | }); 56 | } 57 | var copy = {}; 58 | for (var k in value) { 59 | copy[k] = decode(value[k]); 60 | } 61 | return copy; 62 | } 63 | -------------------------------------------------------------------------------- /src/encode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseACL from './ParseACL'; 13 | import ParseFile from './ParseFile'; 14 | import ParseGeoPoint from './ParseGeoPoint'; 15 | import ParseObject from './ParseObject'; 16 | import { Op } from './ParseOp'; 17 | import ParseRelation from './ParseRelation'; 18 | 19 | var toString = Object.prototype.toString; 20 | 21 | function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, seen: Array) { 22 | if (value instanceof ParseObject) { 23 | if (disallowObjects) { 24 | throw new Error('Parse Objects not allowed here'); 25 | } 26 | var seenEntry = value.id ? value.className + ':' + value.id : value; 27 | if (forcePointers || 28 | !seen || 29 | seen.indexOf(seenEntry) > -1 || 30 | value.dirty() || 31 | Object.keys(value._getServerData()).length < 1 32 | ) { 33 | return value.toPointer(); 34 | } 35 | seen = seen.concat(seenEntry); 36 | return value._toFullJSON(seen); 37 | } 38 | if (value instanceof Op || 39 | value instanceof ParseACL || 40 | value instanceof ParseGeoPoint || 41 | value instanceof ParseRelation) { 42 | return value.toJSON(); 43 | } 44 | if (value instanceof ParseFile) { 45 | if (!value.url()) { 46 | throw new Error('Tried to encode an unsaved file.'); 47 | } 48 | return value.toJSON(); 49 | } 50 | if (toString.call(value) === '[object Date]') { 51 | if (isNaN(value)) { 52 | throw new Error('Tried to encode an invalid date.'); 53 | } 54 | return { __type: 'Date', iso: value.toJSON() }; 55 | } 56 | if (toString.call(value) === '[object RegExp]' && 57 | typeof value.source === 'string') { 58 | return value.source; 59 | } 60 | 61 | if (Array.isArray(value)) { 62 | return value.map((v) => { 63 | return encode(v, disallowObjects, forcePointers, seen); 64 | }); 65 | } 66 | 67 | if (value && typeof value === 'object') { 68 | var output = {}; 69 | for (var k in value) { 70 | output[k] = encode(value[k], disallowObjects, forcePointers, seen); 71 | } 72 | return output; 73 | } 74 | 75 | return value; 76 | } 77 | 78 | export default function(value: mixed, disallowObjects?: boolean, forcePointers?: boolean, seen?: Array) { 79 | return encode(value, !!disallowObjects, !!forcePointers, seen || []); 80 | } 81 | -------------------------------------------------------------------------------- /src/equals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import ParseACL from './ParseACL'; 11 | import ParseFile from './ParseFile'; 12 | import ParseGeoPoint from './ParseGeoPoint'; 13 | import ParseObject from './ParseObject'; 14 | 15 | export default function equals(a, b) { 16 | if (typeof a !== typeof b) { 17 | return false; 18 | } 19 | 20 | if (!a || typeof a !== 'object') { 21 | // a is a primitive 22 | return (a === b); 23 | } 24 | 25 | if (Array.isArray(a) || Array.isArray(b)) { 26 | if (!Array.isArray(a) || !Array.isArray(b)) { 27 | return false; 28 | } 29 | if (a.length !== b.length) { 30 | return false; 31 | } 32 | for (var i = a.length; i--;) { 33 | if (!equals(a[i], b[i])) { 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | if ((a instanceof ParseACL) || 41 | (a instanceof ParseFile) || 42 | (a instanceof ParseGeoPoint) || 43 | (a instanceof ParseObject)) { 44 | return a.equals(b); 45 | } 46 | 47 | if (Object.keys(a).length !== Object.keys(b).length) { 48 | return false; 49 | } 50 | for (var k in a) { 51 | if (!equals(a[k], b[k])) { 52 | return false; 53 | } 54 | } 55 | return true; 56 | } 57 | -------------------------------------------------------------------------------- /src/escape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | var encoded = { 13 | '&': '&', 14 | '<': '<', 15 | '>': '>', 16 | '/': '/', 17 | '\'': ''', 18 | '"': '"' 19 | }; 20 | 21 | export default function escape(str: string): string { 22 | return str.replace(/[&<>\/'"]/g, function(char) { 23 | return encoded[char]; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/interfaces/package.json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | declare module "../package.json" { 11 | declare var version: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/react-native.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | /** 11 | * Interface declaration for React Native modules 12 | */ 13 | declare module "react-native" { 14 | declare class AsyncStorage { 15 | static getItem(path: string, cb: (err: string, value: string) => void): void; 16 | static setItem(path: string, value: string, cb: (err: string, value: string) => void): void; 17 | static removeItem(path: string, cb: (err: string, value: string) => void): void; 18 | static clear(): void; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/xmlhttprequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | declare module xmlhttprequest { 11 | declare var XMLHttpRequest: () => XMLHttpRequest; 12 | } 13 | -------------------------------------------------------------------------------- /src/isRevocableSession.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | export default function isRevocableSession(token: string): boolean { 13 | return token.indexOf('r:') > -1; 14 | } 15 | -------------------------------------------------------------------------------- /src/parseDate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | export default function parseDate(iso8601: string): ?Date { 13 | var regexp = new RegExp( 14 | '^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})' + 'T' + 15 | '([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})' + 16 | '(.([0-9]+))?' + 'Z$'); 17 | var match = regexp.exec(iso8601); 18 | if (!match) { 19 | return null; 20 | } 21 | 22 | var year = match[1] || 0; 23 | var month = (match[2] || 1) - 1; 24 | var day = match[3] || 0; 25 | var hour = match[4] || 0; 26 | var minute = match[5] || 0; 27 | var second = match[6] || 0; 28 | var milli = match[8] || 0; 29 | 30 | return new Date(Date.UTC(year, month, day, hour, minute, second, milli)); 31 | } 32 | -------------------------------------------------------------------------------- /src/unique.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import arrayContainsObject from './arrayContainsObject'; 13 | import ParseObject from './ParseObject'; 14 | 15 | export default function unique(arr: Array): Array { 16 | var uniques = []; 17 | arr.forEach((value) => { 18 | if (value instanceof ParseObject) { 19 | if (!arrayContainsObject(uniques, value)) { 20 | uniques.push(value); 21 | } 22 | } else { 23 | if (uniques.indexOf(value) < 0) { 24 | uniques.push(value); 25 | } 26 | } 27 | }); 28 | return uniques; 29 | } 30 | -------------------------------------------------------------------------------- /src/unsavedChildren.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Parse, LLC. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @flow 10 | */ 11 | 12 | import ParseFile from './ParseFile'; 13 | import ParseObject from './ParseObject'; 14 | import ParseRelation from './ParseRelation'; 15 | 16 | type EncounterMap = { 17 | objects: { [identifier: string]: ParseObject; }; 18 | files: Array; 19 | }; 20 | 21 | /** 22 | * Return an array of unsaved children, which are either Parse Objects or Files. 23 | * If it encounters any dirty Objects without Ids, it will throw an exception. 24 | */ 25 | export default function unsavedChildren( 26 | obj: ParseObject, 27 | allowDeepUnsaved?: boolean 28 | ): Array { 29 | var encountered = { 30 | objects: {}, 31 | files: [] 32 | }; 33 | var identifier = obj.className + ':' + obj._getId(); 34 | encountered.objects[identifier] = ( 35 | obj.dirty() ? obj : true 36 | ); 37 | var attributes = obj.attributes; 38 | for (var attr in attributes) { 39 | if (typeof attributes[attr] === 'object') { 40 | traverse(attributes[attr], encountered, false, !!allowDeepUnsaved); 41 | } 42 | } 43 | var unsaved = []; 44 | for (var id in encountered.objects) { 45 | if (id !== identifier && encountered.objects[id] !== true) { 46 | unsaved.push(encountered.objects[id]); 47 | } 48 | } 49 | return unsaved.concat(encountered.files); 50 | } 51 | 52 | function traverse( 53 | obj: ParseObject, 54 | encountered: EncounterMap, 55 | shouldThrow: boolean, 56 | allowDeepUnsaved: boolean 57 | ) { 58 | if (obj instanceof ParseObject) { 59 | if (!obj.id && shouldThrow) { 60 | throw new Error('Cannot create a pointer to an unsaved Object.'); 61 | } 62 | var identifier = obj.className + ':' + obj._getId(); 63 | if (!encountered.objects[identifier]) { 64 | encountered.objects[identifier] = ( 65 | obj.dirty() ? obj : true 66 | ); 67 | var attributes = obj.attributes; 68 | for (var attr in attributes) { 69 | if (typeof attributes[attr] === 'object') { 70 | traverse(attributes[attr], encountered, !allowDeepUnsaved, allowDeepUnsaved); 71 | } 72 | } 73 | } 74 | return; 75 | } 76 | if (obj instanceof ParseFile) { 77 | if (!obj.url() && encountered.files.indexOf(obj) < 0) { 78 | encountered.files.push(obj); 79 | } 80 | return; 81 | } 82 | if (obj instanceof ParseRelation) { 83 | return; 84 | } 85 | if (Array.isArray(obj)) { 86 | obj.forEach((el) => { 87 | traverse(el, encountered, shouldThrow, allowDeepUnsaved); 88 | }); 89 | } 90 | for (var k in obj) { 91 | if (typeof obj[k] === 'object') { 92 | traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /vendor/README.md: -------------------------------------------------------------------------------- 1 | This directory is temporary until the fixes we need in Babel's dead code elimination plugin make it into the npm module. 2 | -------------------------------------------------------------------------------- /vendor/babel-plugin-dead-code-elimination/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports["default"] = function (_ref) { 8 | var Plugin = _ref.Plugin; 9 | var t = _ref.types; 10 | 11 | function toStatements(node) { 12 | if (t.isBlockStatement(node)) { 13 | var hasBlockScoped = false; 14 | 15 | for (var i = 0; i < node.body.length; i++) { 16 | var bodyNode = node.body[i]; 17 | if (t.isBlockScoped(bodyNode)) hasBlockScoped = true; 18 | } 19 | 20 | if (!hasBlockScoped) { 21 | return node.body; 22 | } 23 | } 24 | 25 | return node; 26 | } 27 | 28 | var visitor = { 29 | ReferencedIdentifier: function ReferencedIdentifier(node, parent, scope) { 30 | var binding = scope.getBinding(node.name); 31 | if (!binding || binding.references > 1 || !binding.constant) return; 32 | if (binding.kind === "param" || binding.kind === "module") return; 33 | if (t.isExportDeclaration(binding.path.parent)) return; 34 | 35 | var replacement = binding.path.node; 36 | if (t.isVariableDeclarator(replacement)) { 37 | replacement = replacement.init; 38 | } 39 | if (!replacement) return; 40 | 41 | // ensure it's a "pure" type 42 | if (!scope.isPure(replacement, true)) return; 43 | 44 | if (t.isClass(replacement) || t.isFunction(replacement)) { 45 | // don't change this if it's in a different scope, this can be bad 46 | // for performance since it may be inside a loop or deeply nested in 47 | // hot code 48 | if (binding.path.scope.parent !== scope) return; 49 | } 50 | 51 | if (this.findParent(function (path) { 52 | return path.node === replacement; 53 | })) { 54 | return; 55 | } 56 | 57 | t.toExpression(replacement); 58 | scope.removeBinding(node.name); 59 | binding.path.dangerouslyRemove(); 60 | return replacement; 61 | }, 62 | 63 | "ClassDeclaration|FunctionDeclaration": function ClassDeclarationFunctionDeclaration(node, parent, scope) { 64 | var binding = scope.getBinding(node.id.name); 65 | if (binding && !binding.referenced) { 66 | this.dangerouslyRemove(); 67 | } 68 | }, 69 | 70 | VariableDeclarator: function VariableDeclarator(node, parent, scope) { 71 | if (!t.isIdentifier(node.id) || !scope.isPure(node.init, true)) return; 72 | visitor["ClassDeclaration|FunctionDeclaration"].apply(this, arguments); 73 | }, 74 | 75 | ConditionalExpression: function ConditionalExpression(node) { 76 | var evaluateTest = this.get("test").evaluateTruthy(); 77 | if (evaluateTest === true) { 78 | return node.consequent; 79 | } else if (evaluateTest === false) { 80 | return node.alternate; 81 | } 82 | }, 83 | 84 | BlockStatement: function BlockStatement() { 85 | var paths = this.get("body"); 86 | 87 | var purge = false; 88 | 89 | for (var i = 0; i < paths.length; i++) { 90 | var path = paths[i]; 91 | 92 | if (!purge && path.isCompletionStatement()) { 93 | purge = true; 94 | continue; 95 | } 96 | 97 | if (purge && !path.isFunctionDeclaration()) { 98 | path.dangerouslyRemove(); 99 | } 100 | } 101 | }, 102 | 103 | IfStatement: { 104 | exit: function exit(node) { 105 | var consequent = node.consequent; 106 | var alternate = node.alternate; 107 | var test = node.test; 108 | 109 | var evaluateTest = this.get("test").evaluateTruthy(); 110 | 111 | // we can check if a test will be truthy 100% and if so then we can inline 112 | // the consequent and completely ignore the alternate 113 | // 114 | // if (true) { foo; } -> { foo; } 115 | // if ("foo") { foo; } -> { foo; } 116 | // 117 | 118 | if (evaluateTest === true) { 119 | return toStatements(consequent); 120 | } 121 | 122 | // we can check if a test will be falsy 100% and if so we can inline the 123 | // alternate if there is one and completely remove the consequent 124 | // 125 | // if ("") { bar; } else { foo; } -> { foo; } 126 | // if ("") { bar; } -> 127 | // 128 | 129 | if (evaluateTest === false) { 130 | if (alternate) { 131 | return toStatements(alternate); 132 | } else { 133 | return this.dangerouslyRemove(); 134 | } 135 | } 136 | 137 | // remove alternate blocks that are empty 138 | // 139 | // if (foo) { foo; } else {} -> if (foo) { foo; } 140 | // 141 | 142 | if (t.isBlockStatement(alternate) && !alternate.body.length) { 143 | alternate = node.alternate = null; 144 | } 145 | 146 | // if the consequent block is empty turn alternate blocks into a consequent 147 | // and flip the test 148 | // 149 | // if (foo) {} else { bar; } -> if (!foo) { bar; } 150 | // 151 | 152 | if (t.isBlockStatement(consequent) && !consequent.body.length && t.isBlockStatement(alternate) && alternate.body.length) { 153 | node.consequent = node.alternate; 154 | node.alternate = null; 155 | node.test = t.unaryExpression("!", test, true); 156 | } 157 | } 158 | } 159 | }; 160 | 161 | return new Plugin("dead-code-elimination", { 162 | metadata: { 163 | group: "builtin-pre", 164 | experimental: true 165 | }, 166 | 167 | visitor: visitor 168 | }); 169 | }; 170 | 171 | module.exports = exports["default"]; 172 | -------------------------------------------------------------------------------- /vendor/babel-plugin-dead-code-elimination/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-dead-code-elimination", 3 | "private": true, 4 | "version": "1.0.3", 5 | "description": "Eliminate dead code. Temporary patch until the export fix makes it into master", 6 | "license": "MIT" 7 | } 8 | --------------------------------------------------------------------------------