diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 000000000..decb1c05a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,38 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: "\U0001F41B " +labels: Bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/---docs-fix-improvement-report.md b/.github/ISSUE_TEMPLATE/---docs-fix-improvement-report.md new file mode 100644 index 000000000..e13a0e4cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---docs-fix-improvement-report.md @@ -0,0 +1,18 @@ +--- +name: "\U0001F4D9 Docs fix/improvement report" +about: Bring attention to an area of the docs with code samples that need updating + or wording that could be improved +title: "\U0001F4D9 " +labels: Documentation Improvement +assignees: '' + +--- + +__Link to section:__ +Please provide a specific link to the section in question. In the docs, you can click on a heading and then copy the relative link from the address bar. + +__What is the issue?__ +Please provide a detailed explanation of the problem, quoting parts of the docs or using screenshots if appropriate. + +__Can you propose a solution?__ +What changes do you think should be made? Have you considered multiple solutions? Will this be suitable for all use cases? diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md new file mode 100644 index 000000000..36f9a017e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -0,0 +1,20 @@ +--- +name: "\U0001F4A1 Feature request" +about: Suggest an idea for this project +title: "\U0001F4A1 " +labels: Feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/---missing-content.md b/.github/ISSUE_TEMPLATE/---missing-content.md new file mode 100644 index 000000000..abb40d0ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---missing-content.md @@ -0,0 +1,14 @@ +--- +name: "\U0001F4D5 Missing content" +about: Bring attention to features that are undocumented +title: "\U0001F4D5" +labels: Missing Content +assignees: '' + +--- + +__References:__ +Please provide links to the relevant PRs in the Parse Server repo and SDK repos where applicable. + +__Proposed solution:__ +Can you provide links to the sections in the relevant guides where the new content should go? diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..8ca0e00e5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at codeofconduct@parseplatform.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..f11b38ba7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +BSD License + +For Parse Server software + +Copyright (c) 2015-present, Parse, LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Parse nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/README.md b/README.md index e82710b1d..57ce83b18 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Parse Docs +[![Join The Conversation](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server) +[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link] +[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link] +[![License][license-svg]][license-link] +[![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform) + + These are the markdown sources for all of the [Parse SDK guides](https://parse-community.github.io/#sdks). The content for the guides is stored in this repo, and we use Jekyll to generate a static site that is hosted on GitHub Pages. ## Repository Structure @@ -49,4 +56,8 @@ Finally, open http://localhost:4000/ in your web browser. ----- -As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. + +[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg +[license-link]: LICENSE +[open-collective-link]: https://opencollective.com/parse-server diff --git a/_app/main.js b/_app/main.js index 569625670..1ae89ddb0 100644 --- a/_app/main.js +++ b/_app/main.js @@ -110,7 +110,7 @@ App.Views = {}; this._headerHeights = headerHeights; this._sortedHeights = sortedHeights.sort(function(a,b) { return a - b; }); }, - + findBestLink: function() { var fromTop = this.scrollContent.scrollTop; // firefox doesn't like this so we fallback to window @@ -244,7 +244,7 @@ App.Views = {}; var self = this; $('.language-toggle').each(function(i, block) { $(block.children).each(function(i, node) { - // manually rendered + // manually rendered var isOpt1 = false; var isOpt2 = false; var toggleTarget; @@ -348,7 +348,7 @@ App.Views = {}; // create the TOC this.scrollContent = document.getElementsByTagName('body')[0]; - + // add toggles to code blocks if necessary if (this.platform === "ios" || this.platform === "osx" || this.platform === "macos") { new App.Views.Docs.Toggle({ @@ -418,7 +418,9 @@ App.Views = {}; const _mount = localStorage.getItem('parse-server-custom-mount'); const _protocol = localStorage.getItem('parse-server-custom-protocol'); const _appId = localStorage.getItem('parse-server-custom-appid'); + const _masterKey = localStorage.getItem('parse-server-custom-masterkey'); const _clientKey = localStorage.getItem('parse-server-custom-clientkey'); + const _restApiKey = localStorage.getItem('parse-server-custom-restapikey'); // set existing entries if (_url) { @@ -437,10 +439,18 @@ App.Views = {}; $(".custom-parse-server-appid").html(_appId); $("#parse-server-custom-appid").val(_appId); } + if (_masterKey) { + $(".custom-parse-server-masterkey").html(_masterKey); + $("#parse-server-custom-masterkey").val(_masterKey); + } if (_clientKey) { $(".custom-parse-server-clientkey").html(_clientKey); $("#parse-server-custom-clientkey").val(_clientKey); } + if (_restApiKey) { + $(".custom-parse-server-restapikey").html(_restApiKey); + $("#parse-server-custom-restapikey").val(_restApiKey); + } } // set url listener @@ -505,11 +515,26 @@ App.Views = {}; } }); + // set masterKey listener + $('#parse-server-custom-masterkey').keyup(function() { + var masterKey = $('#parse-server-custom-masterkey').val(); + if(!masterKey.match(/^[^\s]+$/i)) { + // not a valid masterKey + return; + } + // encode any html + masterKey = masterKey.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + $(".custom-parse-server-masterkey").html(masterKey); + if (typeof(Storage) !== "undefined") { + localStorage.setItem('parse-server-custom-masterkey', masterKey); + } + }); + // set clientKey listener $('#parse-server-custom-clientkey').keyup(function() { var clientKey = $('#parse-server-custom-clientkey').val(); if(!clientKey.match(/^[^\s]+$/i)) { - // not a valid appId + // not a valid clientKey return; } // encode any html @@ -520,6 +545,21 @@ App.Views = {}; } }); + // set restApiKey listener + $('#parse-server-custom-restapikey').keyup(function() { + var restApiKey = $('#parse-server-custom-restapikey').val(); + if(!restApiKey.match(/^[^\s]+$/i)) { + // not a valid restApiKey + return; + } + // encode any html + restApiKey = restApiKey.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + $(".custom-parse-server-restapikey").html(restApiKey); + if (typeof(Storage) !== "undefined") { + localStorage.setItem('parse-server-custom-restapikey', restApiKey); + } + }); + // set reset button $('#parse-server-custom-values-reset').click(function() { // reset defaults @@ -543,12 +583,20 @@ App.Views = {}; $("#parse-server-custom-appid").val(_default); localStorage.setItem('parse-server-custom-appid', _default); + _default = $("#parse-server-custom-masterkey").attr('defaultval'); + $(".custom-parse-server-masterkey").html(_default); + $("#parse-server-custom-masterkey").val(_default); + localStorage.setItem('parse-server-custom-masterkey', _default); + _default = $("#parse-server-custom-clientkey").attr('defaultval'); $(".custom-parse-server-clientkey").html(_default); $("#parse-server-custom-clientkey").val(_default); localStorage.setItem('parse-server-custom-clientkey', _default); - + _default = $("#parse-server-custom-restapikey").attr('defaultval'); + $(".parse-server-custom-restapikey").html(_default); + $("#parse-server-custom-restapikey").val(_default); + localStorage.setItem('parse-server-custom-restapikey', _default); }); }, diff --git a/_config.yml b/_config.yml index 684aa9bc2..ab9e91241 100644 --- a/_config.yml +++ b/_config.yml @@ -6,18 +6,17 @@ # 'jekyll serve'. If you change this file, please restart the server process. # Site settings -name: Parse Open Source Docs +name: Parse Platform Docs title: Parse Docs -email: community@parse.com -description: The open source GitHub page for the Parse platform. +email: community@parseplatform.org +description: The open source docs for the Parse platform SDKs & other guides. url: "http://docs.parseplatform.org" # the base hostname & protocol for your site repository: parse-community/docs -facebook_username: parseit -twitter_username: parseit +twitter_username: parseplatform github_username: parseplatform -stackoverflow_tag: parse.com +stackoverflow_tag: parse-platform serverfault_tag: parse -support_url: https://www.parse.com/help +support_url: https://community.parseplatform.org apis: osx: https://parseplatform.org/Parse-SDK-iOS-OSX/api/ android: https://parseplatform.org/Parse-SDK-Android/api/ diff --git a/_includes/android/files.md b/_includes/android/files.md index 2af7ff147..82c29ddce 100644 --- a/_includes/android/files.md +++ b/_includes/android/files.md @@ -2,7 +2,7 @@ ## The ParseFile -`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes). +`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data. Getting started with `ParseFile` is easy. First, you'll need to have the data in `byte[]` form and then create a `ParseFile` with it. In this example, we'll just use a string: @@ -94,4 +94,4 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } ``` -Note, however, that files can't be parceled if they are not saved on the server. If you try to do so, an exception will be thrown. If you are not sure if your file has been saved, plaese check for `!isDirty()` to be true before writing it to a parcel. +Note, however, that files can't be parceled if they are not saved on the server. If you try to do so, an exception will be thrown. If you are not sure if your file has been saved, please check for `!isDirty()` to be true before writing it to a parcel. diff --git a/_includes/android/handling-errors.md b/_includes/android/handling-errors.md index 3c173a40c..85dbdc7e1 100644 --- a/_includes/android/handling-errors.md +++ b/_includes/android/handling-errors.md @@ -1,5 +1,5 @@ # Handling Errors -Many of the methods on `ParseObject`, including `save()`, `delete()`, and `get()` will throw a `ParseException` on an invalid request, such as deleting or editing an object that no longer exists in the cloud, or when there is a network failure preventing communication with the Parse Cloud. You will need to catch and deal with these exceptions. +Many of the methods on `ParseObject`, including `save()`, `delete()`, and `get()` will throw a `ParseException` on an invalid request, such as deleting or editing an object that no longer exists in the database, or when there is a network failure preventing communication with your Parse Server. You will need to catch and deal with these exceptions. For more details, look at the [Android API]({{ site.apis.android }}). diff --git a/_includes/android/objects.md b/_includes/android/objects.md index 8b7a55e73..019fcbe05 100644 --- a/_includes/android/objects.md +++ b/_includes/android/objects.md @@ -16,7 +16,7 @@ Each `ParseObject` has a class name that you can use to distinguish different so ## Saving Objects -Let's say you want to save the `GameScore` described above to the Parse Cloud. The interface is similar to a `Map`, plus the `saveInBackground` method: +Let's say you want to save the `GameScore` described above to your Parse Server. The interface is similar to a `Map`, plus the `saveInBackground` method: ```java ParseObject gameScore = new ParseObject("GameScore"); @@ -169,7 +169,7 @@ query.getInBackground("xWMyZ4YEGZ", new GetCallback() { public void done(ParseObject gameScore, ParseException e) { if (e == null) { // Now let's update it with some new data. In this case, only cheatMode and score - // will get sent to the Parse Cloud. playerName hasn't changed. + // will get sent to your Parse Server. playerName hasn't changed. gameScore.put("score", 1338); gameScore.put("cheatMode", true); gameScore.saveInBackground(); @@ -212,7 +212,7 @@ Note that it is not currently possible to atomically add and remove items from a ## Deleting Objects -To delete an object from the Parse Cloud: +To delete an object from your Parse Server: ```java myObject.deleteInBackground(); @@ -226,7 +226,7 @@ You can delete a single field from an object with the `remove` method: // After this, the playerName field will be empty myObject.remove("playerName"); -// Saves the field deletion to the Parse Cloud +// Saves the field deletion to your Parse Server myObject.saveInBackground(); ``` @@ -384,7 +384,7 @@ To create a `ParseObject` subclass: 2. Add a `@ParseClassName` annotation. Its value should be the string you would pass into the `ParseObject` constructor, and makes all future class name references unnecessary. 3. Ensure that your subclass has a public default (i.e. zero-argument) constructor. You must not modify any `ParseObject` fields in this constructor. 4. Call `ParseObject.registerSubclass(YourClass.class)` in your `Application` constructor before calling `Parse.initialize()`. - The following code sucessfully implements and registers the `Armor` subclass of `ParseObject`: + The following code successfully implements and registers the `Armor` subclass of `ParseObject`: ```java // Armor.java @@ -485,7 +485,7 @@ protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable("object", object); } - + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { @@ -494,7 +494,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } ``` -That's it. `ParseObject` will parcel its internal state, along with unsaved childs and dirty changes. There are, however, a few things to be aware of when parceling objects that have ongoing operations, like save or delete. The SDK behavior differs depending on whether you have enabled the Local Datastore. +That's it. `ParseObject` will parcel its internal state, along with unsaved children and dirty changes. There are, however, a few things to be aware of when parceling objects that have ongoing operations, like save or delete. The SDK behavior differs depending on whether you have enabled the Local Datastore. ### Parceling with Local Datastore enabled @@ -507,7 +507,7 @@ This means that the `ParseObject` is internally notified about the operation res When the Local Datastore is disabled, and the parceled `ParseObject` has ongoing operations that haven't finished yet, the unparceled object will end up in a stale state. The unparceled object, being a different instance than the source object, - assumes that ongoing operations at the moment of parceling never took place -- will not update its internal state when the operations triggered by the source object +- will not update its internal state when the operations triggered by the source object The unfortunate consequence is that keys that were dirty before saving will still be marked as dirty for the unparceled object. This means, for instance, that any future call to `saveInBackground()` will send these dirty operations to the server again. This can lead to inconsistencies for operations like `increment`, since it might be performed twice. @@ -520,12 +520,12 @@ By default, `ParseObject` implementation parcels everything that is needed. If y @ParseClassName("Armor") public class Armor extends ParseObject { private int member; - + @Override protected void onSaveInstanceState(Bundle outState) { outState.putInt("member", member); } - + @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { member = savedInstanceState.getInt("member"); diff --git a/_includes/android/push-notifications.md b/_includes/android/push-notifications.md index 45b3bdb92..7968d2c2a 100644 --- a/_includes/android/push-notifications.md +++ b/_includes/android/push-notifications.md @@ -43,13 +43,13 @@ The Parse Android SDK will avoid making unnecessary requests. If a `ParseInstall There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting](#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section. -Sending notifications is often done from the Parse.com push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. +Sending notifications is often done from the Parse Dashboard push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. -However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app, as outlined [on our blog](http://blog.parse.com/2014/09/03/the-dangerous-world-of-client-push/). We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. +However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app. We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. -You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. +You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. ### Using Channels @@ -85,7 +85,7 @@ You can also get the set of channels that the current device is subscribed to us List subscribedChannels = ParseInstallation.getCurrentInstallation().getList("channels"); ``` -Neither the subscribe method nor the unsubscribe method blocks the thread it is called from. The subscription information is cached on the device's disk if the network is inaccessible and transmitted to the Parse Cloud as soon as the network is usable. This means you don't have to worry about threading or callbacks while managing subscriptions. +Neither the subscribe method nor the unsubscribe method blocks the thread it is called from. The subscription information is cached on the device's disk if the network is inaccessible and transmitted to your Parse Server as soon as the network is usable. This means you don't have to worry about threading or callbacks while managing subscriptions. #### Sending Pushes to Channels @@ -356,7 +356,7 @@ All of the above methods may be subclassed to customize the way your application ### Tracking Pushes and App Opens -The default implementation of `onPushOpen` will automatically track user engagment from pushes. If you choose not to use the `ParsePushBroadcastReceiver` or override the `onPushOpen` implementation, you may need to track your app open event manually. To do this, add the following to the `onCreate` method of the `Activity` or the `onReceive` method of the `BroadcastReceiver` which handles the `com.parse.push.intent.OPEN` Intent: +The default implementation of `onPushOpen` will automatically track user engagement from pushes. If you choose not to use the `ParsePushBroadcastReceiver` or override the `onPushOpen` implementation, you may need to track your app open event manually. To do this, add the following to the `onCreate` method of the `Activity` or the `onReceive` method of the `BroadcastReceiver` which handles the `com.parse.push.intent.OPEN` Intent: ```java ParseAnalytics.trackAppOpened(getIntent()); @@ -366,7 +366,7 @@ To track push opens, you should always pass the `Intent` to `trackAppOpened`. Pa Please be sure to set up your application to [save the Installation object](#installations). Push open tracking only works when your application's devices are associated with saved `Installation` objects. -You can view the open rate for a specific push notification on your Parse.com push console. You can also view your application's overall app open and push open graphs on the Parse analytics console. Our push open analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release. +You can view the open rate for a specific push notification on your Parse Dashboard push console. You can also view your application's overall app open and push open graphs on the Parse analytics console. Our push open analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release. ## Push Experiments diff --git a/_includes/android/queries.md b/_includes/android/queries.md index 049361590..a023d28a4 100644 --- a/_includes/android/queries.md +++ b/_includes/android/queries.md @@ -152,6 +152,19 @@ losingUserQuery.findInBackground(new FindCallback() { } }); ``` +

To filter rows based on objectId’s from pointers in a second table, you can use dot notation:

+ +```java +ParseQuery chartersOfTypeX = ParseQuery.getQuery("Charter"); +charterOfTypeX.equalTo('type', 'x'); + +ParseQuery groupsWithoutCharterX = ParseQuery.getQuery("Group"); +groupsWithoutCharterX.doesNotMatchKeyInQuery("objectId", "belongsTo.objectId", chartersOfTypeX); +groupsWithoutCharterX.findInBackground(new FindCallback() { + void done(List results, ParseException e) { + // results has the list of groups without charter x + }); +``` You can restrict the fields returned by calling `selectKeys` with a collection of keys. To retrieve documents that contain only the `score` and `playerName` fields (and also special built-in fields such as `objectId`, `createdAt`, and `updatedAt`): @@ -194,10 +207,6 @@ query.whereContainsAll("arrayKey", numbers); ## Queries on String Values -
- If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
- Use `whereStartsWith` to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets: ```java @@ -376,13 +385,27 @@ Now when you do any query with `fromLocalDatastore`, these objects will be inclu For instance, to query the cache first and then the network, ```java -final ParseQuery query = ... +final ParseQuery query = ParseQuery.getQuery("GameScore"); query.fromLocalDatastore().findInBackground().continueWithTask((task) -> { // Update UI with results from Local Datastore ... + ParseException error = task.getError(); + if(error == null){ + List gameScore = task.getResult(); + for(ParseObject game : gameScore){ + //... + } + } // Now query the network: return query.fromNetwork().findInBackground(); }, Task.UI_EXECUTOR).continueWithTask((task) -> { // Update UI with results from Network ... + ParseException error = task.getError(); + if(error == null){ + List gameScore = task.getResult(); + for(ParseObject game : gameScore){ + //... + } + } return task; }, Task.UI_EXECUTOR); ``` @@ -390,11 +413,18 @@ query.fromLocalDatastore().findInBackground().continueWithTask((task) -> { Or you might want to query the cache, and if that fails, fire a network call: ```java -final ParseQuery query = ... +final ParseQuery query = ParseQuery.getQuery("GameScore"); query.fromLocalDatastore().findInBackground().continueWithTask((task) -> { Exception error = task.getError(); if (error instanceof ParseException && ((ParseException) error).getCode() == ParseException.CACHE_MISS) { // No results from cache. Let's query the network. + ParseException error = task.getError(); + if(error == null){ + List gameScore = task.getResult(); + for(ParseObject game : gameScore){ + //... + } + } return query.fromNetwork().findInBackground(); } return task; diff --git a/_includes/arduino/cloud-code.md b/_includes/arduino/cloud-code.md index 39b5b00df..5b6fed760 100644 --- a/_includes/arduino/cloud-code.md +++ b/_includes/arduino/cloud-code.md @@ -1,6 +1,6 @@ # Cloud Functions -Cloud Functions allow you to run custom app logic in the Parse Cloud. This is especially useful for running complex app logic in the cloud so that you can reduce the memory footprint of your code on the IoT device. In a Cloud Function, you can query/save Parse data, send push notifications, and log analytics events. +Cloud Functions allow you to run custom app logic on your Parse Server. This is especially useful for running complex app logic in the cloud so that you can reduce the memory footprint of your code on the IoT device. In a Cloud Function, you can query/save Parse data, send push notifications, and log analytics events. You write your Cloud Code in JavaScript using the Parse JavaScript SDK. We provide a command-line tool to help you deploy your Cloud Code. See our [Cloud Code guide]({{ site.baseUrl }}/cloudcode/guide) for details. diff --git a/_includes/arduino/getting-started.md b/_includes/arduino/getting-started.md index 3a076adbc..3edf7e739 100644 --- a/_includes/arduino/getting-started.md +++ b/_includes/arduino/getting-started.md @@ -1,3 +1,8 @@ + +⚠️ The [Arduino SDK](https://github.com/parse-community/Parse-SDK-Android/tree/1.20.0) has been retired due to a lack of contribution and use. If you intent to continue using this SDK you can still fork the repo. If you are interested in maintaining it please make yourself known and we would be happy to unarchive it. + +----- + # Getting Started If you haven't installed the SDK yet, please [head over to the Arduino SDK repository](https://github.com/parse-community/Parse-SDK-Arduino) to install the SDK in the Arduino IDE. Note that this SDK requires the Arduino Yún and the Arduino IDE v1.6.0+. diff --git a/_includes/arduino/objects.md b/_includes/arduino/objects.md index 45df29470..18e72ddc0 100644 --- a/_includes/arduino/objects.md +++ b/_includes/arduino/objects.md @@ -16,7 +16,7 @@ Each object has a class name that you can use to distinguish different sorts of ## Saving Objects -Let's say you want to save the `Temperature` described above to the Parse Cloud. You would do the following: +Let's say you want to save the `Temperature` described above to your Parse Server. You would do the following: ```cpp ParseObjectCreate create; @@ -41,7 +41,7 @@ After this code runs, you will probably be wondering if anything really happened There are two things to note here. You didn't have to configure or set up a new Class called `Temperature` before running this code. Your Parse app lazily creates this Class for you when it first encounters it. -There are also a few fields you don't need to specify that are provided as a convenience. `objectId` is a unique identifier for each saved object. `createdAt` and`updatedAt` represent the time that each object was created and last modified in the Parse Cloud. Each of these fields is filled in by Parse, so they don't exist on a Parse Object until a save operation has completed. +There are also a few fields you don't need to specify that are provided as a convenience. `objectId` is a unique identifier for each saved object. `createdAt` and`updatedAt` represent the time that each object was created and last modified in your Parse Server. Each of these fields is filled in by Parse, so they don't exist on a Parse Object until a save operation has completed. ## Retrieving Objects diff --git a/_includes/arduino/sample-app.md b/_includes/arduino/sample-app.md deleted file mode 100644 index 5a26153de..000000000 --- a/_includes/arduino/sample-app.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sample App - -We prepared a [sample app](https://github.com/parseplatform/Anydevice) that demonstrates how to provision connected devices using a companion phone app such that connected devices can securely access user-specific data on the Parse Cloud. This sample app also demonstrates how to send push notifications between the phone app and connected devices. diff --git a/_includes/cloudcode/cloud-code-advanced.md b/_includes/cloudcode/cloud-code-advanced.md index beb6dca70..9204c5f49 100644 --- a/_includes/cloudcode/cloud-code-advanced.md +++ b/_includes/cloudcode/cloud-code-advanced.md @@ -8,7 +8,7 @@ A simple GET request would look like: ```javascript Parse.Cloud.httpRequest({ - url: 'http://www.parse.com/' + url: 'http://www.awesomewebsite.com/' }).then(function(httpResponse) { // success console.log(httpResponse.text); @@ -24,7 +24,7 @@ A GET request that specifies the port number would look like: ```javascript Parse.Cloud.httpRequest({ - url: 'http://www.parse.com:8080/' + url: 'http://www.awesomewebsite.com:8080/' }).then(function(httpResponse) { console.log(httpResponse.text); }, function(httpResponse) { diff --git a/_includes/cloudcode/cloud-code.md b/_includes/cloudcode/cloud-code.md index 0ffb03b7b..f53183a32 100644 --- a/_includes/cloudcode/cloud-code.md +++ b/_includes/cloudcode/cloud-code.md @@ -102,7 +102,6 @@ And finally, to call the same function from a JavaScript app: const params = { movie: "The Matrix" }; const ratings = await Parse.Cloud.run("averageStars", params); // ratings should be 4.5 -}); ``` In general, two arguments will be passed into cloud functions: @@ -144,8 +143,6 @@ Sometimes you want to execute long running functions, and you don't want to wait }); ``` -Note that calling `status.success` or `status.error` won't prevent any further execution of the job. - ## Running a Job Calling jobs is done via the REST API and is protected by the master key. @@ -221,6 +218,15 @@ Parse.Cloud.beforeSave("Review", (request) => { }); ``` +## Predefined Classes +If you want to use `beforeSave` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself, for example: + +```javascript +Parse.Cloud.beforeSave(Parse.User, async (request) => { + // code here +}) +``` + # afterSave Triggers In some cases, you may want to perform some action, such as a push, after an object has been saved. You can do this by registering a handler with the `afterSave` method. For example, suppose you want to keep track of the number of comments on a blog post. You can do that by writing a function like this: @@ -274,7 +280,13 @@ const afterSave = function afterSave(request) { ``` ## Predefined Classes -If you want to use `afterSave` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself. +If you want to use `afterSave` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself, for example: + +```javascript +Parse.Cloud.afterSave(Parse.User, async (request) => { + // code here +}) +``` # beforeDelete Triggers @@ -297,8 +309,14 @@ Parse.Cloud.beforeDelete("Album", (request) => { If the function throws, the `Album` object will not be deleted, and the client will get an error. Otherwise,the object will be deleted normally. -If you want to use `beforeDelete` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself. +## Predefined Classes +If you want to use `beforeDelete` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself, for example: +```javascript +Parse.Cloud.beforeDelete(Parse.User, async (request) => { + // code here +}) +``` # afterDelete Triggers @@ -320,7 +338,14 @@ The `afterDelete` handler can access the object that was deleted through `reques The client will receive a successful response to the delete request after the handler terminates, regardless of how the `afterDelete` terminates. For instance, the client will receive a successful response even if the handler throws an exception. Any errors that occurred while running the handler can be found in the Cloud Code log. -If you want to use `afterDelete` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself. +## Predefined Classes +If you want to use `afterDelete` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself, for example: + +```javascript +Parse.Cloud.afterDelete(Parse.User, async (request) => { + // code here +}) +``` # beforeFind Triggers @@ -328,7 +353,6 @@ If you want to use `afterDelete` for a predefined class in the Parse JavaScript In some cases you may want to transform an incoming query, adding an additional limit or increasing the default limit, adding extra includes or restrict the results to a subset of keys. You can do so with the `beforeFind` trigger. - ## Examples ```javascript @@ -381,10 +405,70 @@ Parse.Cloud.beforeFind('MyObject', (req) => { Parse.Cloud.beforeFind('MyObject2', (req) => { req.readPreference = 'SECONDARY_PREFERRED'; req.subqueryReadPreference = 'SECONDARY'; + req.includeReadPreference = 'PRIMARY'; }); ``` +## Predefined Classes +If you want to use `beforeFind` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself, for example: + +```javascript +Parse.Cloud.beforeFind(Parse.User, async (request) => { + // code here +}) +``` + +# afterFind Triggers + +*Available only on parse-server cloud code starting 2.2.25* + +In some cases you may want to manipulate the results of a query before they are sent to the client. You can do so with the `afterFind` trigger. + +``` +Parse.Cloud.afterFind('MyCustomClass', async (request) => { + // code here +}) +``` + +## Predefined Classes +If you want to use `afterFind` for a predefined class in the Parse JavaScript SDK (e.g. [Parse.User]({{ site.apis.js }}classes/Parse.User.html)), you should not pass a String for the first argument. Instead, you should pass the class itself, for example: + +```javascript +Parse.Cloud.afterFind(Parse.User, async (request) => { + // code here +}) +``` + +# beforeLogin Triggers + +*Available only on parse-server cloud code starting 3.3.0* + +Sometimes you may want to run custom validation on a login request. The `beforeLogin` trigger can be used for blocking an account from logging in (for example, if they are banned), recording a login event for analytics, notifying user by email if a login occurred at an unusual IP address and more. + +```javascript +Parse.Cloud.beforeLogin(async request => { + const { object: user } = request; + if(user.get('isBanned')) { + throw new Error('Access denied, you have been banned.') + } +}); +``` + +## Some considerations to be aware of + +- It waits for any promises to resolve +- The user is not available on the request object - the user has not yet been provided a session until after beforeLogin is successfully completed +- Like `afterSave` on `Parse.User`, it will not save mutations to the user unless explicitly saved + +### The trigger will run... +- On username & password logins +- On `authProvider` logins + +### The trigger won't run... +- On sign up +- If the login credentials are incorrect + # Using the Master Key in cloud code Set `useMasterKey:true` in the requests that require master key. diff --git a/_includes/common/data.md b/_includes/common/data.md index b2e0fd473..5e3bc2059 100644 --- a/_includes/common/data.md +++ b/_includes/common/data.md @@ -54,7 +54,7 @@ Objects in either format should contain keys and values that also satisfy the fo * Key names must contain only numbers, letters, and underscore, and must start with a letter. * No value may contain a hard newline '`\n`'. -Normally, when objects are saved to Parse, they are automatically assigned a unique identifier through the `objectId` field, as well as a `createdAt` field and `updatedAt` field which represent the time that the object was created and last modified in the Parse Cloud. These fields can be manually set when data is imported from a JSON file. Please keep in mind the following: +Normally, when objects are saved to Parse, they are automatically assigned a unique identifier through the `objectId` field, as well as a `createdAt` field and `updatedAt` field which represent the time that the object was created and last modified in your Parse Server. These fields can be manually set when data is imported from a JSON file. Please keep in mind the following: * Use a unique 10 character alphanumeric string as the value of your `objectId` fields. * Use a UTC timestamp in the ISO 8601 format when setting a value for the `createdAt` field or the `updatedAt` field. diff --git a/_includes/common/errors.md b/_includes/common/errors.md index 3693c6c5d..1e427a764 100644 --- a/_includes/common/errors.md +++ b/_includes/common/errors.md @@ -162,9 +162,6 @@ The following is a list of all the error codes that can be returned by the Parse |----------------------------------|------|---------------------------------------------------------------| | `RequestTimeout` | 124 | The request was slow and timed out. Typically this indicates that the request is too expensive to run. You may see this when a Cloud function did not finish before timing out, or when a `Parse.Cloud.httpRequest` connection times out. | | `InefficientQueryError` | 154 | An inefficient query was rejected by the server. Refer to the Performance Guide and slow query log. | -| `RequestLimitExceeded` | 155 | This application has exceeded its request limit (legacy Parse.com apps only). | -| `TemporaryRejectionError` | 159 | An application's requests are temporary rejected by the server (legacy Parse.com apps only). | -| `DatabaseNotMigratedError` | 428 | You should migrate your database as soon as possible (legacy Parse.com apps only). | {: .docs_table} ## Other issues diff --git a/_includes/common/server-customize.md b/_includes/common/server-customize.md index be44f2585..7fafafa01 100644 --- a/_includes/common/server-customize.md +++ b/_includes/common/server-customize.md @@ -5,6 +5,8 @@ Customize our docs with your server configuration. +Note: `masterKey` overrides all permissions. Keep this secret. + Protocol:
App Id: +Master Key: + Client Key: - + +Rest API Key: + - serverUrl: https://YOUR.PARSE-SERVER.HERE/parse/ - appId: APPLICATION_ID -- clientKey: CLIENT_KEY \ No newline at end of file +- masterKey: MASTER_KEY +- clientKey: CLIENT_KEY +- restApiKey: REST_API_KEY diff --git a/_includes/common/sessions.md b/_includes/common/sessions.md index 5ce149d3b..a25f41b7f 100644 --- a/_includes/common/sessions.md +++ b/_includes/common/sessions.md @@ -1,8 +1,8 @@ # Sessions -Sessions represent an instance of a user logged into a device. Sessions are automatically created when users log in or sign up. They are automatically deleted when users log out. There is one distinct `Session` object for each user-installation pair; if a user issues a login request from a device they're already logged into, that user's previous `Session` object for that Installation is automatically deleted. `Session` objects are stored on Parse in the Session class, and you can view them on the Parse.com Data Browser. We provide a set of APIs to manage `Session` objects in your app. +Sessions represent an instance of a user logged into a device. Sessions are automatically created when users log in or sign up. They are automatically deleted when users log out. There is one distinct `Session` object for each user-installation pair; if a user issues a login request from a device they're already logged into, that user's previous `Session` object for that Installation is automatically deleted. `Session` objects are stored on Parse in the Session class, and you can view them on the Parse Dashboard Data Browser. We provide a set of APIs to manage `Session` objects in your app. -`Session` is a subclass of a Parse `Object`, so you can query, update, and delete sessions in the same way that you manipulate normal objects on Parse. Because the Parse Cloud automatically creates sessions when you log in or sign up users, you should not manually create `Session` objects unless you are building a "Parse for IoT" app (e.g. Arduino or Embedded C). Deleting a `Session` will log the user out of the device that is currently using this session's token. +`Session` is a subclass of a Parse `Object`, so you can query, update, and delete sessions in the same way that you manipulate normal objects on Parse. Because Parse Server automatically creates sessions when you log in or sign up users, you should not manually create `Session` objects unless you are building a "Parse for IoT" app (e.g. Arduino or Embedded C). Deleting a `Session` will log the user out of the device that is currently using this session's token. Unlike other Parse objects, the `Session` class does not have Cloud Code triggers. So you cannot register a `beforeSave` or `afterSave` handler for the Session class. @@ -17,14 +17,14 @@ The `Session` object has these special fields: * `authProvider` could have values: `password`, `anonymous`, `facebook`, or `twitter`. * `restricted` (readonly): Boolean for whether this session is restricted. * Restricted sessions do not have write permissions on `User`, `Session`, and `Role` classes on Parse. Restricted sessions also cannot read unrestricted sessions. - * All sessions that the Parse Cloud automatically creates during user login/signup will be unrestricted. All sessions that the developer manually creates by saving a new `Session` object from the client (only needed for "Parse for IoT" apps) will be restricted. -* `expiresAt` (readonly): Approximate UTC date when this `Session` object will be automatically deleted. You can configure session expiration settings (either 1-year inactivity expiration or no expiration) in your app's Parse.com dashboard settings page. + * All sessions that Parse Server automatically creates during user login/signup will be unrestricted. All sessions that the developer manually creates by saving a new `Session` object from the client (only needed for "Parse for IoT" apps) will be restricted. +* `expiresAt` (readonly): Approximate UTC date when this `Session` object will be automatically deleted. You can configure session expiration settings (either 1-year inactivity expiration or no expiration) in your app's Parse Dashboard settings page. * `installationId` (can be set only once): String referring to the `Installation` where the session is logged in from. For Parse SDKs, this field will be automatically set when users log in or sign up. -All special fields except `installationId` can only be set automatically by the Parse Cloud. You can add custom fields onto `Session` objects, but please keep in mind that any logged-in device (with session token) can read other sessions that belong to the same user (unless you disable Class-Level Permissions, see below). +All special fields except `installationId` can only be set automatically by Parse Server. You can add custom fields onto `Session` objects, but please keep in mind that any logged-in device (with session token) can read other sessions that belong to the same user (unless you disable Class-Level Permissions, see below). ## Handling Invalid Session Token Error -With revocable sessions, your current session token could become invalid if its corresponding `Session` object is deleted from the Parse Cloud. This could happen if you implement a Session Manager UI that lets users log out of other devices, or if you manually delete the session via Cloud Code, REST API, or Data Browser. Sessions could also be deleted due to automatic expiration (if configured in app settings). When a device's session token no longer corresponds to a `Session` object on the Parse Cloud, all API requests from that device will fail with “Error 209: invalid session token”. +With revocable sessions, your current session token could become invalid if its corresponding `Session` object is deleted from your Parse Server. This could happen if you implement a Session Manager UI that lets users log out of other devices, or if you manually delete the session via Cloud Code, REST API, or Data Browser. Sessions could also be deleted due to automatic expiration (if configured in app settings). When a device's session token no longer corresponds to a `Session` object on your Parse Server, all API requests from that device will fail with “Error 209: invalid session token”. To handle this error, we recommend writing a global utility function that is called by all of your Parse request error callbacks. You can then handle the "invalid session token" error in this global function. You should prompt the user to login again so that they can obtain a new session token. This code could look like this: @@ -292,7 +292,7 @@ try { `Session` objects can only be accessed by the user specified in the user field. All `Session` objects have an ACL that is read and write by that user only. You cannot change this ACL. This means querying for sessions will only return objects that match the current logged-in user. -When you log in a user via a `User` login method, Parse will automatically create a new unrestricted `Session` object in the Parse Cloud. Same for signups and Facebook/Twitter logins. +When you log in a user via a `User` login method, Parse will automatically create a new unrestricted `Session` object in your Parse Server. Same for signups and Facebook/Twitter logins. Session objects manually created from client SDKs (by creating an instance of `Session`, and saving it) are always restricted. You cannot manually create an unrestricted sessions using the object creation API. @@ -310,7 +310,7 @@ Parse.Cloud.beforeSave("MyClass", function(request, response) { }); }); ``` -You can configure Class-Level Permissions (CLPs) for the Session class just like other classes on Parse. CLPs restrict reading/writing of sessions via the `Session` API, but do not restrict Parse Cloud's automatic session creation/deletion when users log in, sign up, and log out. We recommend that you disable all CLPs not needed by your app. Here are some common use cases for Session CLPs: +You can configure Class-Level Permissions (CLPs) for the Session class just like other classes on Parse. CLPs restrict reading/writing of sessions via the `Session` API, but do not restrict Parse Server's automatic session creation/deletion when users log in, sign up, and log out. We recommend that you disable all CLPs not needed by your app. Here are some common use cases for Session CLPs: * **Find**, **Delete** — Useful for building a UI screen that allows users to see their active session on all devices, and log out of sessions on other devices. If your app does not have this feature, you should disable these permissions. * **Create** — Useful for "Parse for IoT" apps (e.g. Arduino or Embedded C) that provision restricted user sessions for other devices from the phone app. You should disable this permission when building apps for mobile and web. For "Parse for IoT" apps, you should check whether your IoT device actually needs to access user-specific data. If not, then your IoT device does not need a user session, and you should disable this permission. diff --git a/_includes/dotnet/files.md b/_includes/dotnet/files.md index dda2e3553..f8883226c 100644 --- a/_includes/dotnet/files.md +++ b/_includes/dotnet/files.md @@ -2,7 +2,7 @@ ## The ParseFile -`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes). +`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data. Getting started with `ParseFile` is easy. First, you'll need to have the data in `byte[]` or `Stream` form and then create a `ParseFile` with it. In this example, we'll just use a string: diff --git a/_includes/dotnet/handling-errors.md b/_includes/dotnet/handling-errors.md index 0e3609f2d..984a54136 100644 --- a/_includes/dotnet/handling-errors.md +++ b/_includes/dotnet/handling-errors.md @@ -11,13 +11,13 @@ await user.SignUpAsync(); This will throw an `InvalidOperationException` because `SignUpAsync` was called without first setting the required properties (`Username` and `Password`). -The second type of error is one that occurs when interacting with the Parse Cloud over the network. These errors are either related to problems connecting to the cloud or problems performing the requested operation. Let's take a look at another example: +The second type of error is one that occurs when interacting with Parse Server over the network. These errors are either related to problems connecting to the cloud or problems performing the requested operation. Let's take a look at another example: ```cs await ParseObject.GetQuery("Note").GetAsync("thisObjectIdDoesntExist"); ``` -In the above code, we try to fetch an object with a non-existent `ObjectId`. The Parse Cloud will return an error -- so here's how to handle it properly: +In the above code, we try to fetch an object with a non-existent `ObjectId`. Parse Server will return an error -- so here's how to handle it properly: ```cs try diff --git a/_includes/dotnet/objects.md b/_includes/dotnet/objects.md index 5677e01a0..e82f8afdc 100644 --- a/_includes/dotnet/objects.md +++ b/_includes/dotnet/objects.md @@ -17,7 +17,7 @@ Each `ParseObject` has a class name that you can use to distinguish different so ## Saving Objects -Let's say you want to save the `GameScore` described above to the Parse Cloud. The interface is similar to an `IDictionary`, plus the `SaveAsync` method: +Let's say you want to save the `GameScore` described above to your Parse Server. The interface is similar to an `IDictionary`, plus the `SaveAsync` method: ```cs ParseObject gameScore = new ParseObject("GameScore"); @@ -35,7 +35,7 @@ createdAt:"2011-06-10T18:33:42Z", updatedAt:"2011-06-10T18:33:42Z" There are two things to note here. You didn't have to configure or set up a new Class called `GameScore` before running this code. Your Parse app lazily creates this Class for you when it first encounters it. -There are also a few fields you don't need to specify that are provided as a convenience. `ObjectId` is a unique identifier for each saved object. `CreatedAt` and `UpdatedAt` represent the time that each object was created and last modified in the Parse Cloud. Each of these fields is filled in by Parse, so they don't exist on a `ParseObject` until a save operation has completed. +There are also a few fields you don't need to specify that are provided as a convenience. `ObjectId` is a unique identifier for each saved object. `CreatedAt` and `UpdatedAt` represent the time that each object was created and last modified in your Parse Server. Each of these fields is filled in by Parse, so they don't exist on a `ParseObject` until a save operation has completed. ## Data Types @@ -178,7 +178,7 @@ You can delete a single field from an object with the `Remove` method: // After this, the playerName field will be empty myObject.Remove("playerName"); -// Saves the field deletion to the Parse Cloud +// Saves the field deletion to Parse Server await myObject.SaveAsync(); ``` diff --git a/_includes/dotnet/push-notifications.md b/_includes/dotnet/push-notifications.md index 77b29434e..a23ba8e43 100644 --- a/_includes/dotnet/push-notifications.md +++ b/_includes/dotnet/push-notifications.md @@ -57,13 +57,13 @@ While it is possible to modify a `ParseInstallation` just like you would a `Pars There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting](#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section. -Sending notifications is often done from the Parse.com push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. +Sending notifications is often done from the Parse Dashboard push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. -However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app, as outlined [on our blog](http://blog.parse.com/2014/09/03/the-dangerous-world-of-client-push/). We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. +However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app. We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. Settings for the Push client -You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. +You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. ### Using Channels @@ -314,7 +314,7 @@ await wpPush.SendAsync(); ## Scheduling Pushes -Sending scheduled push notifications is not currently supported by the .NET SDK. Take a look at the [REST API]({{ site.baseUrl }}/rest/guide/#scheduling-pushes), [JavaScript SDK]({{ site.baseUrl }}/js/guide/#scheduling-pushes) or the Parse.com push console. +Sending scheduled push notifications is not currently supported by the .NET SDK. Take a look at the [REST API]({{ site.baseUrl }}/rest/guide/#scheduling-pushes), [JavaScript SDK]({{ site.baseUrl }}/js/guide/#scheduling-pushes) or the Parse Dashboard push console. ## Receiving Pushes @@ -379,7 +379,7 @@ To track push opens, you should always pass your event handler's input args to ` Please be sure to set up your application to [save the Installation object](#installations) Push open tracking only works when your application's devices are associated with saved `Installation` objects. -You can view the open rate for a specific push notification on your Parse.com push console. You can also view your application's overall app open and push open graphs on the Parse analytics console. Our push open analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release. +You can view the open rate for a specific push notification on your Parse Dashboard push console. You can also view your application's overall app open and push open graphs on the Parse analytics console. Our push open analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release. #### Tracking on WinRT Applications diff --git a/_includes/dotnet/queries.md b/_includes/dotnet/queries.md index c64e64d9b..496114072 100644 --- a/_includes/dotnet/queries.md +++ b/_includes/dotnet/queries.md @@ -228,10 +228,6 @@ var query = ParseObject.GetQuery("MyClass") ## Queries on String Values -
- If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
- Use `WhereStartsWith` or a `StartsWith` LINQ query to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets: ```cs diff --git a/_includes/embedded_c/cloud-code.md b/_includes/embedded_c/cloud-code.md index 28733ca8c..d0ea10c98 100644 --- a/_includes/embedded_c/cloud-code.md +++ b/_includes/embedded_c/cloud-code.md @@ -1,6 +1,6 @@ # Cloud Functions -Cloud Functions allow you to run custom app logic in the Parse Cloud. This is especially useful for running complex app logic in the cloud so that you can reduce the memory footprint of your code on the IoT device. In a Cloud Function, you can query/save Parse data, send push notifications, and log analytics events. +Cloud Functions allow you to run custom app logic on your Parse Server. This is especially useful for running complex app logic in the cloud so that you can reduce the memory footprint of your code on the IoT device. In a Cloud Function, you can query/save Parse data, send push notifications, and log analytics events. You write your Cloud Code in JavaScript using the Parse JavaScript SDK. See our [Cloud Code guide]({{ site.baseUrl }}/cloudcode/guide/) for details. diff --git a/_includes/embedded_c/sample-app.md b/_includes/embedded_c/sample-app.md index 5a26153de..0fb1acc8e 100644 --- a/_includes/embedded_c/sample-app.md +++ b/_includes/embedded_c/sample-app.md @@ -1,3 +1,3 @@ # Sample App -We prepared a [sample app](https://github.com/parseplatform/Anydevice) that demonstrates how to provision connected devices using a companion phone app such that connected devices can securely access user-specific data on the Parse Cloud. This sample app also demonstrates how to send push notifications between the phone app and connected devices. +We prepared a [sample app](https://github.com/parseplatform/Anydevice) that demonstrates how to provision connected devices using a companion phone app such that connected devices can securely access user-specific data on Parse Server. This sample app also demonstrates how to send push notifications between the phone app and connected devices. diff --git a/_includes/graphql/customisation.md b/_includes/graphql/customisation.md new file mode 100644 index 000000000..086c56b0f --- /dev/null +++ b/_includes/graphql/customisation.md @@ -0,0 +1,245 @@ +# Customisation + +Although we automtically generate a GraphQL schema based on your Parse Server database, we have provided a number of ways in which to configure and extend this schema. + +## Configuration + +Whilst it's great to simply plug GraphQL into your Parse setup and immediately query any of your existing classes, you may find that this level of exposure is not suitable to your project. We have therefore provided a flexible way to limit which types, queries mutations are exposed within your GraphQL schema. + +### Configuration Options + +By default, no configuration is needed to get GraphQL working with your Parse Server. All of the following settings are completely optional, and can be provided or omitted as desired. To configure your schema, you simply need to provide a valid JSON object with the expected properties as described below: + +```typescript +// The properties with ? are optional + +interface ParseGraphQLConfiguration { + // All classes enabled by default + // Provide an empty array to disable all classes + enabledForClasses?: Array; + + // Selectively disable specific classes + disabledForClasses?: Array; + + // Provide an array of per-class settings + classConfigs?: Array<{ + + // You must provide a className + // Only provide one config object per class + className: string; + + type?: { + + // By default, all fields can be sent for + // a create or update mutation. Use this + // setting to limit to specific fields. + inputFields?: { + create?: Array; + update?: Array; + }; + + // By default, all fields can be resolved + // on a get or find query. Use this to limit + // which fields can be selected. + outputFields?: Array; + + // By default, all valid fields can be used + // to filter a query. Use this to limit + // which fields can be used to constrain a query. + constraintFields?: Array; + + // By default, all valid fields can be used + // to sort the results of a query. Use this to + // limit which fields can be used to sort a query + // and which direction that sort can be set to. + sortFields?: { + field: string; + asc: boolean; + desc: boolean; + }[]; + }; + + // By default, a get and find query type is created + // for all included classes. Use this to disable + // the available query types for this class. + query?: { + get?: boolean; + find?: boolean; + }; + + // By default, all write mutation types are + // exposed for all included classes. Use this to disable + // the available mutation types for this class. + mutation?: { + create?: boolean; + update?: boolean; + destroy?: boolean; + }; + }> +} +``` + +### Set or Update Configuration + +We have provided a public API in `ParseGraphQLServer` which accepts the above JSON object for setting (and updating) your Parse GraphQL Configuration, `setGraphQLConfig`: + +```javascript + const parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: parseServerConfig.graphQLPath, + playgroundPath: parseServerConfig.playgroundPath + }); + + const config = { + // ... ParseGraphQLConfiguration + }; + + await parseGraphQLServer.setGraphQLConfig(config); +``` + +### Include or Exclude Classes + +By default, all of your Parse classes, including the defaults such as `Parse.User`, `Parse.Session`, `Parse.Role` are added to the schema. You can restrict this using the `enabledForClassess` or `disabledForClassess` options, which accepts an array of class names. + + +In the following example, we limit our GraphQL schema to only expose the default `_User` class, along with a few custom classes: + +```javascript +{ + "enabledForClasses": ["_User", "Book", "Review", "Comment"], + "disabledForClasses": null +} +``` + +In the following example, we limit our GraphQL schema by hiding some sensitive classes: + +```javascript +{ + // undefined or null results in the default behaviour, i.e. include all classes + "enabledForClasses": undefined, + // override the included classes by filtering out the following: + "disabledForClasses": [ "UserSensitiveData", "ProductOrder", "Invoice" ] +} +``` + +### Input Types + +By default, we enrich the schema by generating a number of [Input Types](https://graphql.org/learn/schema/#input-types) for each class. This, as a healthy side-effect, improves development experience by providing type-completion and docs, though the true purpose is to define exactly what fields are exposed and useable per operation type. You can provide a `type` setting for any or each of your classes to limit which fields are exposed: + +In the following example, we have a custom class called `Review` where the fields `rating` and `body` are allowed on the `create` mutation, and the field `numberOfLikes` on the `update` mutation: + +```javascript +{ + "classConfigs": [ + { + "className": "Review", + "type": { + "inputFields": { + "create": ["rating", "body"], + "update": ["numberOfLikes"] + } + } + } + ] +} +``` + +You may decide to restrict which fields can be resolved when getting or finding records from a given class, for example, if you have a class called `Video` which includes a sensitive field `dmcaFlags`, you can hide this field by explicitly stating the fields that can be resolved: + +```javascript +{ + "classConfigs": [ + { + "className": "Video", + "type": { + "outputFields": ["name", "author", "numberOfViews", "comments", "cdnUrl"] + } + } + ] +} +``` + +In production-grade environments where performance optimisation is critical, complete control over query filters and sortability is required to ensure that unindexed queries are not executed. For this reason, we provide a way to limit which fields can be used to constrain a query, and which fields (including the direction) can be used to sort that query. + + +In the following example, we set the fields `name` and `age` as the only two that can be used to filter the `_User` class, and defining the `createdAt` and `age` fields the only sortable field whilst disabling the ascending direction on the `createdAt` field: + +```javascript +{ + "classConfigs": [ + { + "className": "_User", + "type": { + "constraintFields": ["name", "age"], + "sortFields": [ + { + "field": "createdAt", + "desc": true, + "asc": false + }, + { + "field": "age", + "desc": true, + "asc": true + } + ] + } + } + ] +} +``` + +### Queries + +By default, the schema exposes a `get` and `find` operation for each class, for example, `get_User` and `find_User`. You can disable either of these for any class in your schema, like so: + + +```javascript +{ + "classConfigs": [ + { + "className": "_User", + "query": { + "get": true, + "find": false + } + }, + { + "className": "Review", + "query": { + "get": false, + "find": true + } + } + ] +} +``` + +### Mutations + +By default, the schema exposes a `create`, `update` and `delete` operation for each class, for example, `create_User`, `update_User` and `delete_User`. You can disable any of these mutations for any class in your schema, like so: + + +```javascript +{ + "classConfigs": [ + { + "className": "_User", + "mutation": { + "create": true, + "update": true, + "destroy": true + } + }, + { + "className": "Review", + "mutation": { + "create": true, + "update": false, + "destroy": true + } + } + ] +} +``` + +**Note**: the `delete` mutation setting key is named `destroy` to avoid issues due to `delete` being a javascript reserved word. diff --git a/_includes/graphql/getting-started.md b/_includes/graphql/getting-started.md new file mode 100644 index 000000000..0ec2f3bcf --- /dev/null +++ b/_includes/graphql/getting-started.md @@ -0,0 +1,97 @@ +# Getting Started + +[GraphQL](https://graphql.org/), developed by Facebook, is an open-source data query and manipulation language for APIs. In addition to the traditional [REST API](/rest/guide/), Parse Server automatically generates a GraphQL API based on your current application schema. + +The easiest way to run the Parse GraphQL Server is using the CLI: + +```bash +$ npm install -g parse-server mongodb-runner +$ mongodb-runner start +$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --mountGraphQL --mountPlayground +``` + +Notes: +* Run `parse-server --help` or refer to [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for a complete list of Parse Server configuration options. +* ⚠️ Please do not use `--mountPlayground` option in production as anyone could access your API Playground and read or change your application's data. [Parse Dashboard](#running-parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps. +* ⚠️ The Parse GraphQL ```beta``` implementation is fully functional but discussions are taking place on how to improve it. So new versions of Parse Server can bring breaking changes to the current API. + +After running the CLI command, you should have something like this in your terminal: + +Parse GraphQL Server + +Since you have already started your Parse GraphQL Server, you can now visit [http://localhost:1337/playground](http://localhost:1337/playground) in your web browser to start playing with your GraphQL API. + +GraphQL Playground + +## Using Docker + +You can also run the Parse GraphQL API inside a Docker container: + +```bash +$ git clone https://github.com/parse-community/parse-server +$ cd parse-server +$ docker build --tag parse-server . +$ docker run --name my-mongo -d mongo +$ docker run --name my-parse-server --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --mountGraphQL --mountPlayground +``` + +After starting the server, you can visit [http://localhost:1337/playground](http://localhost:1337/playground) in your browser to start playing with your GraphQL API. + +⚠️ Please do not use `--mountPlayground` option in production as anyone could access your API Playground and read or change your application's data. [Parse Dashboard](#running-parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps. + +## Using Express.js + +You can also mount the GraphQL API in an Express.js application together with the REST API or solo: + +```js +const express = require('express'); +const { default: ParseServer, ParseGraphQLServer } = require('parse-server'); + +const app = express(); + +const parseServer = new ParseServer({ + databaseURI: 'mongodb://localhost:27017/test', + appId: 'APPLICATION_ID', + masterKey: 'MASTER_KEY', + serverURL: 'http://localhost:1337/parse' +}); + +const parseGraphQLServer = new ParseGraphQLServer( + parseServer, + { + graphQLPath: '/graphql', + playgroundPath: '/playground' + } +); + +app.use('/parse', parseServer.app); // (Optional) Mounts the REST API +parseGraphQLServer.applyGraphQL(app); // Mounts the GraphQL API +parseGraphQLServer.applyPlayground(app); // (Optional) Mounts the GraphQL Playground - do NOT use in Production + +app.listen(1337, function() { + console.log('REST API running on http://localhost:1337/parse'); + console.log('GraphQL API running on http://localhost:1337/graphql'); + console.log('GraphQL Playground running on http://localhost:1337/playground'); +}); +``` + +After starting the server, you can visit [http://localhost:1337/playground](http://localhost:1337/playground) in your browser to start playing with your GraphQL API. + +⚠️ Please do not mount the GraphQL Playground in production as anyone could access your API Playground and read or change your application's data. [Parse Dashboard](#running-parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps. + +## Running Parse Dashboard + +[Parse Dashboard](https://github.com/parse-community/parse-dashboard) is a standalone dashboard for managing your Parse Server apps, including your objects' schema and data, logs, jobs, and push notifications. Parse Dashboard also has a built-in GraphQL Playground that you can use to play around with your auto-generated Parse GraphQL API. It is the recommended option for production applications. + +The easiest way to run the Parse Dashboard is through its CLI: + +```bash +$ npm install -g parse-dashboard +$ parse-dashboard --dev --appId APPLICATION_ID --masterKey MASTER_KEY --serverURL "http://localhost:1337/parse" --graphQLServerURL "http://localhost:1337/graphql" --appName MyAppName +``` + +After starting the dashboard, you can visit [http://0.0.0.0:4040/apps/MyAppName/api_console/graphql](http://0.0.0.0:4040/apps/MyAppName/api_console/graphql) in your browser: + +Parse Dashboard GraphQL Playground + +To learn more about Parse Dashboard and its setup options, please visit [Parse Dashboard Repository](https://github.com/parse-community/parse-dashboard). \ No newline at end of file diff --git a/_includes/graphql/learning-more.md b/_includes/graphql/learning-more.md new file mode 100644 index 000000000..b6cc1280e --- /dev/null +++ b/_includes/graphql/learning-more.md @@ -0,0 +1,7 @@ +# Learning More + +If you look at the right side of your GraphQL Playground, you will see the DOCS and SCHEMA menus. They are automatically generated by analyzing your application schema and contain all operations that you can call for your application, including the automatic class queries and mutations. Please refer to them and learn more about everything that you can do with your Parse GraphQL API. + +GraphQL Docs + +Additionally, the [GraphQL Learn Section](https://graphql.org/learn/) is a very good source to start learning about the power of the GraphQL language. diff --git a/_includes/graphql/objects.md b/_includes/graphql/objects.md new file mode 100644 index 000000000..afceda635 --- /dev/null +++ b/_includes/graphql/objects.md @@ -0,0 +1,500 @@ +# Objects + +## Creating Objects + +### Generic Mutation + +Parse Server is schemaless in the sense that you don’t need to specify ahead of time what keys exist on each object. In fact, it generates a schema on the fly as you create objects. + +For creating an object, you simply need to set whatever key-value pairs you want, and the backend will store it. This operation can be performed through the GraphQL API using the generic `create` mutation. For example: + +```graphql +mutation CreateObject { + create( + className: "GameScore" + fields: { + score: 1337 + playerName: "Sean Plott" + cheatMode: false + } + ) { + objectId + createdAt + } +} +``` + +If you execute the code above in your GraphQL Playground, you should receive a response similar to this: + +```json +{ + "data": { + "create": { + "objectId": "EGyoD3goxn", + "createdAt": "2019-08-27T06:53:08.780Z" + } + } +} +``` + +In addition to creating the object, Parse Server automatically learns from it, keeps track of the application's schema, and generates custom operations for each class. For instance, after running the code above, you will notice a new `GameScore` class in the schema and a new set of `GameScore` CRUD operations in the GraphQL API. + +### Class Mutation + +For each class of your application's schema, Parse Server automatically generates a custom mutation for creating this class' objects through the GraphQL API. + +For example, if you have a class named `GameScore` in the schema, Parse Server automatically generates a new mutation called `createGameScore`, and you should be able to run the code below in your GraphQL Playground: + +```graphql +mutation CreateGameScore { + createGameScore(fields: { + score: 80075 + playerName: "Jang Min Chul" + cheatMode: false + }) { + objectId + createdAt + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "createGameScore": { + "objectId": "NNfsmFxMIv", + "createdAt": "2019-08-27T06:57:18.452Z" + } + } +} +``` + +## Getting an Object + +### Generic Query + +You can get an existing object by its `className` and `objectId` using the `get` query. For example: + +```graphql +query GetObject { + get(className: "GameScore" objectId: "EGyoD3goxn") +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "get": { + "objectId": "EGyoD3goxn", + "score": 1337, + "playerName": "Sean Plott", + "cheatMode": false, + "createdAt": "2019-08-27T06:53:08.780Z", + "updatedAt": "2019-08-27T06:53:08.780Z" + } + } +} +``` + +### Class Query + +For each class of your application's schema, Parse Server automatically generates a custom query for getting this class' objects through the GraphQL API. + +For example, if you have a class named `GameScore` in the schema, Parse Server automatically generates a new query called `gameScore`, and you should be able to run the code below in your GraphQL Playground: + +```graphql +query GameScore { + gameScore(objectId: "EGyoD3goxn") { + objectId + playerName + score + cheatMode + createdAt + updatedAt + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "gameScore": { + "objectId": "EGyoD3goxn", + "playerName": "Sean Plott", + "score": 1337, + "cheatMode": false, + "createdAt": "2019-08-27T06:53:08.780Z", + "updatedAt": "2019-08-27T06:53:08.780Z" + } + } +} +``` + +## Finding Objects + +### Generic Query + +You can retrieve multiple objects at once using the `find` query. For example: + +```graphql +query FindObjects { + find(className: "GameScore") { + count + results + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "find": { + "count": 2, + "results": [ + { + "objectId": "EGyoD3goxn", + "score": 1337, + "playerName": "Sean Plott", + "cheatMode": false, + "createdAt": "2019-08-27T06:53:08.780Z", + "updatedAt": "2019-08-27T06:53:08.780Z" + }, + { + "objectId": "NNfsmFxMIv", + "playerName": "Jang Min Chul", + "score": 80075, + "cheatMode": false, + "createdAt": "2019-08-27T06:57:18.452Z", + "updatedAt": "2019-08-27T06:57:18.452Z" + } + ] + } + } +} +``` + +### Class Query + +For each class of your application's schema, Parse Server automatically generates a custom query for finding this class' objects through the GraphQL API. + +For example, if you have a class named `GameScore` in the schema, Parse Server automatically generates a new query called `gameScores`, and you should be able to run the code below in your GraphQL Playground: + +```graphql +query GameScores { + gameScores { + count + results { + objectId + playerName + score + cheatMode + createdAt + updatedAt + } + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "gameScores": { + "count": 2, + "results": [ + { + "objectId": "EGyoD3goxn", + "playerName": "Sean Plott", + "score": 1337, + "cheatMode": false, + "createdAt": "2019-08-27T06:53:08.780Z", + "updatedAt": "2019-08-27T06:53:08.780Z" + }, + { + "objectId": "NNfsmFxMIv", + "playerName": "Jang Min Chul", + "score": 80075, + "cheatMode": false, + "createdAt": "2019-08-27T06:57:18.452Z", + "updatedAt": "2019-08-27T06:57:18.452Z" + } + ] + } + } +} +``` + +### Constraints + +You can use the `where` argument to add constraints to either a generic or a class find query. See the example below: + +```graphql +query ConstraintsExamples { + genericExample: find( + className: "GameScore" + where: { score: { _lt: 1500 } } + ) { + count + } + classExample: gameScores( + where: { score: { _gt: 1500 } } + ) { + count + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "genericExample": { + "count": 1 + }, + "classExample": { + "count": 1 + } + } +} +``` + +### Order + +You can use the `order` argument to select in which order the results should show up either in a generic or in a class find query. See the example below: + +```graphql +query OrderExamples { + genericExample: find( + className: "GameScore" + where: { cheatMode: false } + order: "-score" + ) { + results + } + classExample: gameScores( + where: { cheatMode: { _eq: false } } + order: [score_ASC] + ) { + results { + playerName + score + } + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "genericExample": { + "results": [ + { + "objectId": "NNfsmFxMIv", + "playerName": "Jang Min Chul", + "score": 80075, + "cheatMode": false, + "createdAt": "2019-08-27T06:57:18.452Z", + "updatedAt": "2019-08-27T06:57:18.452Z" + }, + { + "objectId": "EGyoD3goxn", + "score": 1337, + "playerName": "Sean Plott", + "cheatMode": false, + "createdAt": "2019-08-27T06:53:08.780Z", + "updatedAt": "2019-08-27T06:53:08.780Z" + } + ] + }, + "classExample": { + "results": [ + { + "playerName": "Sean Plott", + "score": 1337 + }, + { + "playerName": "Jang Min Chul", + "score": 80075 + } + ] + } + } +} +``` + +### Pagination + +You can use the `skip` and `limit` arguments to paginate the results either in a generic or in a class find query. See the example below: + +```graphql +query PaginationExamples { + genericExample: find( + className: "GameScore" + where: { cheatMode: false } + order: "-score" + skip: 0 + limit: 1 + ) { + results + } + classExample: gameScores( + where: { cheatMode: { _eq: false } } + order: [score_DESC] + skip: 1 + limit: 1 + ) { + results { + playerName + score + } + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "genericExample": { + "results": [ + { + "objectId": "NNfsmFxMIv", + "playerName": "Jang Min Chul", + "score": 80075, + "cheatMode": false, + "createdAt": "2019-08-27T06:57:18.452Z", + "updatedAt": "2019-08-27T06:57:18.452Z" + } + ] + }, + "classExample": { + "results": [ + { + "playerName": "Sean Plott", + "score": 1337 + } + ] + } + } +} +``` + +## Updating an Object + +### Generic Mutation + +You can update an existing object using the `update` mutation. You simply need to send its `className`, `objectId`, and the fields that need to be updated. Parse Server will only change the received fields, and keep the others as they are. For example: + +```graphql +mutation UpdateObject { + update( + className: "GameScore" + objectId: "NNfsmFxMIv" + fields: { ranking: 1 } + ) { + updatedAt + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "update": { + "updatedAt": "2019-08-27T07:14:04.346Z" + } + } +} +``` + +### Class Mutation + +For each class of your application's schema, Parse Server automatically generates a custom mutation for updating this class' objects through the GraphQL API. + +For example, if you have a class named `GameScore` in the schema, Parse Server automatically generates a new mutation called `updateGameScore`, and you should be able to run the code below in your GraphQL Playground: + +```graphql +mutation UpdateGameScore { + updateGameScore( + objectId: "EGyoD3goxn" + fields: { ranking: 2 } + ) { + updatedAt + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "updateGameScore": { + "updatedAt": "2019-08-27T07:15:05.351Z" + } + } +} +``` + +## Deleting an Object + +### Generic Mutation + +You can delete an existing object using the `delete` mutation. You simply need to send its `className`, and `objectId`. For example: + +```graphql +mutation DeleteObject { + delete(className: "GameScore" objectId: "NNfsmFxMIv") +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "delete": true + } +} +``` + +### Class Mutation + +For each class of your application's schema, Parse Server automatically generates a custom mutation for deleting this class' objects through the GraphQL API. + +For example, if you have a class named `GameScore` in the schema, Parse Server automatically generates a new mutation called `deleteGameScore`, and you should be able to run the code below in your GraphQL Playground: + +```graphql +mutation DeleteGameScore { + deleteGameScore(objectId: "EGyoD3goxn") { + objectId + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "deleteGameScore": { + "objectId": "EGyoD3goxn" + } + } +} +``` diff --git a/_includes/graphql/users.md b/_includes/graphql/users.md new file mode 100644 index 000000000..efee8a58b --- /dev/null +++ b/_includes/graphql/users.md @@ -0,0 +1,132 @@ +# Users + +In general, users have the same features as other objects, such as the flexible schema. The differences are that user objects must have a username and password, the password is automatically encrypted and stored securely, and Parse Server enforces the uniqueness of the username and email fields. + +Therefore you can manage users objects using the `createUser`, `user`, `users`, `updateUser`, `deleteUser`, and the generic operations with `_User` in the `className` argument. + +Additionally, you can use the `signUp`, `logIn`, and `logOut` operations, which will be presented in the following sections. + +## Signing Up + +Signing up a new user differs from creating another object in that the `username` and `password` fields are required. The `password` field is handled differently than the others; it is encrypted with bcrypt when stored in the database and never returned to any client request. + +You can ask Parse Server to [verify user email addresses]({{ site.baseUrl }}/parse-server/guide/#welcome-emails-and-email-verification) in your application settings. With this setting enabled, all new user registrations with an email field will generate an email confirmation at that address. You can check whether the user has verified their email with the `emailVerified` field. + +To sign up a new user, use the `signUp` mutation. For example: + +```graphql +mutation SignUp { + signUp(fields: { + username: "somedude" + password: "Parse_3.5_Rocks!" + }) { + objectId + createdAt + sessionToken + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "signUp": { + "objectId": "A13obRUwDE", + "createdAt": "2019-08-27T07:22:25.251Z", + "sessionToken": "r:caca30fa5c68f0ce467e7790a7208ff7" + } + } +} +``` + +Note that, in addition to the regular `objectId`, and `createdAt` fields, a new field called `sessionToken` has been returned. This token can be used to authenticate subsequent operations as this user. + +## Logging In + +After you allow users to sign up, you need to let them log in to their account with a username and password in the future. To do this, use the `logIn` mutation: + +```graphql +mutation LogIn { + logIn(fields: { username: "somedude" password: "Parse_3.5_Rocks!" }) { + objectId + username + sessionToken + createdAt + updatedAt + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "logIn": { + "objectId": "A13obRUwDE", + "username": "somedude", + "sessionToken": "r:28eeeed86ad96e88e120783b2ea612ef", + "createdAt": "2019-08-27T07:22:25.251Z", + "updatedAt": "2019-08-27T07:22:25.251Z" + } + } +} +``` + +Note that, when the user logs in, Parse Server generates a new `sessionToken` for future operations. + +## Using Session Token + +For authenticating an operation as a specific user, you need to pass the `X-Parse-Session-Token` header with its valid session token. + +You can easily do this in the GraphQL Playground. There is an option called `HTTP HEADERS` in its bottom left side. Use this option to replace the default `X-Parse-Master-Key` header by a valid `X-Parse-Session-Token` header. You should have something like this: + +Session Token Header + +After setting up the `X-Parse-Session-Token` header, any operation will run as this user. For example, you can run the code below to validate the session token and return its associated user: + +```graphql +query Viewer { + viewer { + username + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "viewer": { + "username": "somedude" + } + } +} +``` + +## Logging Out + +You can log out a user through the `logOut` mutation. You need to send the `X-Parse-Session-Token` header and run code like the below example: + +```graphql +mutation LogOut { + logOut { + username + } +} +``` + +The code above should resolve to something similar to this: + +```json +{ + "data": { + "logOut": { + "username": "somedude" + } + } +} +``` diff --git a/_includes/graphql/your-first-query.md b/_includes/graphql/your-first-query.md new file mode 100644 index 000000000..32df2cd99 --- /dev/null +++ b/_includes/graphql/your-first-query.md @@ -0,0 +1,19 @@ +# Your First Query - Health Check + +Now that you have set up your GraphQL environment, it is time to run your first query. Execute the following code in your GraphQL Playground to check your API's health: + +```graphql +query Health { + health +} +``` + +You should receive the following response: + +```json +{ + "data": { + "health": true + } +} +``` diff --git a/_includes/header.html b/_includes/header.html index 97f61a8e7..bda6a4b53 100644 --- a/_includes/header.html +++ b/_includes/header.html @@ -57,7 +57,7 @@ diff --git a/_includes/ios/files.md b/_includes/ios/files.md index d70e5039a..2ab5bbc84 100644 --- a/_includes/ios/files.md +++ b/_includes/ios/files.md @@ -2,7 +2,7 @@ ## The PFFileObject -`PFFileObject` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `PFObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes). +`PFFileObject` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `PFObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data. Getting started with `PFFileObject` is easy. First, you'll need to have the data in `NSData` form and then create a `PFFileObject` with it. In this example, we'll just use a string: @@ -142,6 +142,8 @@ file?.saveInBackground({ (success: Bool, error: Error?) in ``` -You can delete files that are referenced by objects using the [REST API]({{ site.baseUrl }}/rest/guide/#deleting-files). You will need to provide the master key in order to be allowed to delete a file. +##Deleting Files -If your files are not referenced by any object in your app, it is not possible to delete them through the REST API. You may request a cleanup of unused files in your app's Settings page. Keep in mind that doing so may break functionality which depended on accessing unreferenced files through their URL property. Files that are currently associated with an object will not be affected. +If you know the name of a file you can delete it using the [REST API]({{site.baseUrl}}/rest/guide/#deleting-files). Your master key is required for this operation. + +Note: Reguardless of the Parse Server storage configuration, deleting a `PFObject` with a `PFFileObject` does not delete the file itself meerly its reference. Additionally, Parse does **NOT** provide a way to find unreferenced file names in storage. diff --git a/_includes/ios/getting-started.md b/_includes/ios/getting-started.md index fc8856f88..1536bbd61 100644 --- a/_includes/ios/getting-started.md +++ b/_includes/ios/getting-started.md @@ -43,10 +43,20 @@ And you're off! Take a look at the public [documentation][docs] and start buildi **Initialise Parse SDK** -To initialize the Parse client, add the following to your AppDelegate.swift, in the application:didFinishLaunchingWithOptions: method. +To initialize the Parse client, add the following to your AppDelegate.swift file (AppDelegate.m for Objective-C), in the `application:didFinishLaunchingWithOptions:` method. +
+```objective_c +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [Parse initializeWithConfiguration:[ParseClientConfiguration configurationWithBlock:^(id configuration) { + configuration.applicationId = @"parseAppId"; + configuration.clientKey = @"parseClientKey"; + configuration.server = @"parseServerUrlString"; + }]]; + return YES; +} +``` ```swift -// AppDelegate.swift func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let parseConfig = ParseClientConfiguration { $0.applicationId = "parseAppId" @@ -54,7 +64,21 @@ func application(application: UIApplication, didFinishLaunchingWithOptions launc $0.server = "parseServerUrlString" } Parse.initialize(with: parseConfig) + return true } ``` +
+ +Make sure to import the Parse module at the top of any file in which you want to use the Parse SDK by including the follwing. + +
+```objective_c +#import +``` +```swift +import Parse +``` +
+ [releases]: https://github.com/parse-community/Parse-SDK-iOS-OSX/releases [docs]: http://docs.parseplatform.org/ios/guide diff --git a/_includes/ios/handling-errors.md b/_includes/ios/handling-errors.md index 1101f4ae9..f3420e159 100644 --- a/_includes/ios/handling-errors.md +++ b/_includes/ios/handling-errors.md @@ -17,7 +17,7 @@ user.signUp This will throw an `NSInternalInconsistencyException` because `signUp` was called without first setting the required properties (`username` and `password`). -The second type of error is one that occurs when interacting with the Parse Cloud over the network. These errors are either related to problems connecting to the cloud or problems performing the requested operation. Let's take a look at another example: +The second type of error is one that occurs when interacting with Parse Server over the network. These errors are either related to problems connecting to the cloud or problems performing the requested operation. Let's take a look at another example:
```objective_c @@ -36,7 +36,7 @@ func getMyNote() -> Void { ```
-In the above code, we try to fetch an object with a non-existent `objectId`. The Parse Cloud will return an error with an error code set in `code` and message in the error's `userInfo`. Here's how to handle it properly in your callback: +In the above code, we try to fetch an object with a non-existent `objectId`. Parse Server will return an error with an error code set in `code` and message in the error's `userInfo`. Here's how to handle it properly in your callback:
```objective_c @@ -70,7 +70,7 @@ func callbackForGet(result: PFObject?, error: NSError?) -> Void { ```
-The query might also fail because the device couldn't connect to the Parse Cloud. Here's the same callback but with a bit of extra code to handle that scenario explicitly: +The query might also fail because the device couldn't connect to your Parse Server. Here's the same callback but with a bit of extra code to handle that scenario explicitly:
```objective_c diff --git a/_includes/ios/local-datastore.md b/_includes/ios/local-datastore.md index 530bf0e04..b69642b63 100644 --- a/_includes/ios/local-datastore.md +++ b/_includes/ios/local-datastore.md @@ -1,14 +1,19 @@ # Local Datastore -The Parse iOS/OS X SDK provides a local datastore which can be used to store and retrieve `PFObject`s, even when the network is unavailable. To enable this functionality, add `libsqlite3.dylib` and add `isLocalDatastoreEnabled = true` to the `ParseClientConfiguration` block used in `Parse.initialize()`. +The Parse iOS/OS X SDK provides a local datastore which can be used to store and retrieve `PFObject`s, even when the network is unavailable. To enable this functionality add `isLocalDatastoreEnabled = true` to the `ParseClientConfiguration` block used in `Parse.initialize()` or call `Parse.enableLocalDatastore()` prior to initializing Parse.
```objective_c @implementation AppDelegate - (void)application:(UIApplication *)application didFinishLaunchWithOptions:(NSDictionary *)options { - [Parse enableLocalDatastore]; - [Parse setApplicationId:@"parseAppId" clientKey:@"parseClientKey"]; + ParseClientConfiguration *configuration = [ParseClientConfiguration configurationWithBlock:^(id configuration) { + configuration.applicationId = @"parseAppId"; + configuration.clientKey = @"parseClientKey"; + configuration.server = @"parseServerUrlString"; + configuration.localDatastoreEnabled = YES; + }]; + [Parse initializeWithConfiguration:configuration]; } @end @@ -74,28 +79,28 @@ Storing objects is great, but it's only useful if you can then get the objects b ```objective_c PFQuery *query = [PFQuery queryWithClassName:@"GameScore"]; [query fromLocalDatastore]; -[[query getObjectInBackgroundWithId:@"xWMyZ4YE"] continueWithBlock:^id(BFTask *task) { - if (task.error) { - // Something went wrong. - return task; - } +[query getObjectInBackgroundWithId:"" block:^(PFObject * _Nullable object, NSError * _Nullable error) { + if (!error) { + // Success + } else { + // Fail! + } +} // task.result will be your game score return task; }]; ``` + ```swift let query = PFQuery(className: "GameScore") query.fromLocalDatastore() -query.getObjectInBackgroundWithId("xWMyZ4YE").continueWithBlock { - (task: BFTask!) -> AnyObject in - if let error = task.error { - // Something went wrong. - return task; +query.getObjectInBackground(withId: "string") { (object, error) in + if error == nil { + // Success! + } else { + // Failure! } - - // task.result will be your game score - return task; } ```
diff --git a/_includes/ios/objects.md b/_includes/ios/objects.md index 8be4805dd..41055d5cc 100644 --- a/_includes/ios/objects.md +++ b/_includes/ios/objects.md @@ -16,7 +16,7 @@ Each `PFObject` has a class name that you can use to distinguish different sorts ## Saving Objects -Let's say you want to save the `GameScore` described above to the Parse Cloud. The interface is similar to a `NSMutableDictionary`, plus the `saveInBackground` method: +Let's say you want to save the `GameScore` described above to a Parse Server. The interface is similar to a `NSMutableDictionary`. The `saveInBackgroundWithBlock` function:
```objective_c @@ -24,7 +24,7 @@ PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"]; gameScore[@"score"] = @1337; gameScore[@"playerName"] = @"Sean Plott"; gameScore[@"cheatMode"] = @NO; -[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { +[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { // The object has been saved. } else { @@ -37,18 +37,17 @@ let gameScore = PFObject(className:"GameScore") gameScore["score"] = 1337 gameScore["playerName"] = "Sean Plott" gameScore["cheatMode"] = false -gameScore.saveInBackground { - (success: Bool, error: Error?) in - if (success) { - // The object has been saved. - } else { - // There was a problem, check error.description - } +gameScore.saveInBackground { (succeeded, error) in + if (succeeded) { + // The object has been saved. + } else { + // There was a problem, check error.description + } } ```
-After this code runs, you will probably be wondering if anything really happened. To make sure the data was saved, you can look at the Data Browser in your app on Parse. You should see something like this: +After this code runs, you will probably be wondering if anything really happened. If [Parse Dashboard](https://github.com/parse-community/parse-dashboard) is implemented for your server, you can verify the data was saved in the data browser. You should see something like this: ```js objectId: "xWMyZ4YEGZ", score: 1337, playerName: "Sean Plott", cheatMode: false, @@ -57,9 +56,7 @@ createdAt:"2011-06-10T18:33:42Z", updatedAt:"2011-06-10T18:33:42Z" There are two things to note here. You didn't have to configure or set up a new Class called `GameScore` before running this code. Your Parse app lazily creates this Class for you when it first encounters it. -There are also a few fields you don't need to specify that are provided as a convenience. `objectId` is a unique identifier for each saved object. `createdAt` and `updatedAt` represent the time that each object was created and last modified in the Parse Cloud. Each of these fields is filled in by Parse, so they don't exist on a `PFObject` until a save operation has completed. - -Note: You can use the `saveInBackgroundWithBlock` method to provide additional logic to run after the save completes. +There are also a few fields you don't need to specify that are provided and set by the system as a convenience. `objectId` is a unique identifier for each saved object. `createdAt` and `updatedAt` represent the time that each object was created or last modified and saved to the Parse Server. Each of these fields is filled in by Parse Server, so they don't exist on a `PFObject` until the first save operation has been completed. ## Retrieving Objects @@ -69,22 +66,20 @@ Saving data to the cloud is fun, but it's even more fun to get that data out aga ```objective_c PFQuery *query = [PFQuery queryWithClassName:@"GameScore"]; [query getObjectInBackgroundWithId:@"xWMyZ4YEGZ" block:^(PFObject *gameScore, NSError *error) { - // Do something with the returned PFObject in the gameScore variable. - NSLog(@"%@", gameScore); + if (!error) { + // Success! + } else { + // Failure! + } }]; -// The InBackground methods are asynchronous, so any code after this will run -// immediately. Any code that depends on the query result should be moved -// inside the completion block above. ``` ```swift let query = PFQuery(className:"GameScore") -query.getObjectInBackground(withId: "xWMyZEGZ") { (gameScore: PFObject?, error: Error?) in - if let error = error { - //The query returned an error - print(error.localizedDescription) +query.getObjectInBackground(withId: "xWMyZEGZ") { (gameScore, error) in + if error == nil { + // Success! } else { - //The object has been retrieved - print(gameScore) + // Fail! } } ``` @@ -123,22 +118,35 @@ let acl = gameScore.acl
If you need to refresh an object you already have with the latest data that - is in the Parse Cloud, you can call the `fetch` method like so: +is in the database, you can use the `fetchInBackgroundWithBlock:` or `fetchInBackgroundWithTarget:selector:` methods. +
```objective_c -[myObject fetch]; +[myObject fetchInBackgroundWithBlock:^(PFObject * _Nullable object, NSError * _Nullable error) { + if (!error) { + // Success! + } else { + // Failure! + } +}]; ``` ```swift -myObject.fetch() +myObject.fetchInBackground { (object, error) in + if error == nil { + // Success! + } else { + // Failure! + } +} ```
-Note: In a similar way to the `save` methods, you can use the `fetchInBackgroundWithBlock` or `fetchInBackgroundWithTarget:selector:` methods to provide additional logic which will run after fetching the object. +Note: In a similar way to the `save` methods, you can use the throwable `fetch` or `fetchIfNeeded` methods, or asyncronous task without completion. `fetchInBackground` ## The Local Datastore -Parse also lets you store objects in a [local datastore](#local-datastore) on the device itself. You can use this for data that doesn't need to be saved to the cloud, but this is especially useful for temporarily storing data so that it can be synced later. To enable the datastore, add `libsqlite3.dylib` and add `isLocalDatastoreEnabled = true` to the `ParseClientConfiguration` block in your `AppDelegate` `application:didFinishLaunchWithOptions:` before calling `Parse.initialize()`. Once the local datastore is enabled, you can store an object by pinning it. +Parse also lets you store objects in a [local datastore](#local-datastore) on the device itself. You can use this for data that doesn't need to be saved to the cloud, but this is especially useful for temporarily storing data so that it can be synced later. To enable the datastore, add `isLocalDatastoreEnabled = true` to the `ParseClientConfiguration` block in your `AppDelegate` `application:didFinishLaunchWithOptions:`, or call `Parse.enableLocalDatastore()` before calling `Parse.initialize()`. Once the local datastore is enabled, you can store an object by pinning it.
```objective_c @@ -161,7 +169,7 @@ As with saving, this recursively stores every object and file that `gameScore` p ### Retrieving Objects from the Local Datastore -Storing an object is only useful if you can get it back out. To get the data for a specific object, you can use a `PFQuery` just like you would while on the network, but using the `fromLocalDatastore` method to tell it where to get the data. +Storing an object is only useful if you can get it back out. To get the data for a specific object, you can use a `PFQuery` just like you would while on the network, but using the `fromLocalDatastore:` method to tell it where to get the data.
```objective_c @@ -192,7 +200,7 @@ query.getObjectInBackground(withId: "xWMyZEGZ").continueWith { (task: BFTask -If you already have an instance of the object, you can instead use the `fetchFromLocalDatastoreInBackground` method. +If you already have an instance of the object, you can instead use the `fetchFromLocalDatastoreInBackground:` method.
```objective_c @@ -223,7 +231,7 @@ object.fetchFromLocalDatastoreInBackground().continueWith { (task: BFTask ```objective_c @@ -236,7 +244,7 @@ gameScore.unpinInBackground() ## Saving Objects Offline -Most save functions execute immediately, and inform your app when the save is complete. If you don't need to know when the save has finished, you can use `saveEventually` instead. The advantage is that if the user currently doesn't have a network connection, `saveEventually` will store the update on the device until a network connection is re-established. If your app is closed before the connection is back, Parse will try again the next time the app is opened. All calls to `saveEventually` (and `deleteEventually`) are executed in the order they are called, so it is safe to call `saveEventually` on an object multiple times. +Most save functions execute immediately, and inform your app when the save is complete. For a network consious soltion on non-priority save requests use `saveEventually`. Not only does it retry saving upon regaining network connection, but If your app is closed prior to save completion Parse will try the next time the app is opened. Additionally, all calls to `saveEventually` (and `deleteEventually`) are executed in the order they are called, making it safe to call `saveEventually` on an object multiple times.
```objective_c @@ -344,43 +352,39 @@ gameScore.saveInBackground() ```
-Note that it is not currently possible to atomically add and remove items from an array in the same save. - You will have to call `save` in between every different kind of array operation. +Note that it is not currently possible to atomically add and remove items from an array in the same save using. You will have to call `save` in between every different kind of array operation. ## Deleting Objects -To delete an object from the cloud: -
-```objective_c -[gameScore deleteInBackground]; -``` -```swift -gameScore.deleteInBackground() -``` -
+There are a few ways to delete a `PFObject`. For basic asynchronous deletion of a single object call the objects `deleteInBackground` function. If you prefer to recieve a callback you can use the `deleteInBackgroundWithBlock:` or `deleteInBackgroundWithTarget:selector:` methods. If you want to block the calling thread, you can use the `delete` method. Lastly, `deleteEventually` is a network conscious option that deletes when possible but does not guarantee a timeframe for the tasks completion. -If you want to run a callback when the delete is confirmed, you can use the `deleteInBackgroundWithBlock:` or `deleteInBackgroundWithTarget:selector:` methods. If you want to block the calling thread, you can use the `delete` method. +For deleting multiple objects use the `PFObject` static function `deleteAllInBackground` to delete an array of objects asynchronously. The same can be done while blocking the calling thread using `deleteAll`. Lastly, to recieve a callback after deleting objects asyncronously use `deleteAllInBackground:block:` as demonstrated below. -You can delete a single field from an object with the `removeObjectForKey` method:
```objective_c -// After this, the playerName field will be empty -[gameScore removeObjectForKey:@"playerName"]; - -// Saves the field deletion to the Parse Cloud -[gameScore saveInBackground]; +[PFObject deleteAllInBackground:objectArray block:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // The array of objects was successfully deleted. + } else { + // There was an error. Check the errors localizedDescription. + } +}]; ``` ```swift -// After this, the playerName field will be empty -gameScore.remove(forKey: "playerName") - -// Saves the field deletion to the Parse Cloud -gameScore.saveInBackground() +PFObject.deleteAll(inBackground: objectArray) { (succeeded, error) in + if (succeeded) { + // The array of objects was successfully deleted. + } else { + // There was an error. Check the errors localizedDescription. + } +} ```
+Note: Deleting an object from the server that contains a `PFFileObject` does **NOT** delete the file from storage. Instead, an objects deletion only deletes the data referencing the stored file. To delete the data from storage you must use the [REST API]({{site.baseUrl}}/rest/guide/#deleting-files). For more info about `PFFileObject`, please see the [Files](#files) section. + ## Relational Data Objects can have relationships with other objects. To model this behavior, any `PFObject` can be used as a value in other `PFObject`s. Internally, the Parse framework will store the referred-to object in just one place, to maintain consistency. @@ -422,16 +426,19 @@ myComment.saveInBackground() ```
-You can also link objects using just their `objectId`s like so: +Note: Saving an object with a relational pointer to another object will save both objects. However, two new objects with pointers to each other will cause a error for having a circular dependency. + +### Object Relationships With Minimal Data + +You can link objects without even fetching data by initializing `PFObjects` with only the class name and the objects `objectId` like so:
```objective_c -// Add a relation between the Post with objectId "1zEcyElZ80" and the comment -myComment[@"parent"] = [PFObject objectWithoutDataWithClassName:@"Post" objectId:@"1zEcyElZ80"]; +myComment[@"post"] = [PFObject objectWithoutDataWithClassName:@"Post" objectId:@"1zEcyElZ80"]; ``` ```swift // Add a relation between the Post with objectId "1zEcyElZ80" and the comment -myComment["parent"] = PFObject(withoutDataWithClassName:"Post", objectId:"1zEcyElZ80") +myComment["post"] = PFObject(withoutDataWithClassName: "Post", objectId: "1zEcyElZ80") ```
@@ -439,17 +446,20 @@ By default, when fetching an object, related `PFObject`s are not fetched. These
```objective_c -PFObject *post = fetchedComment[@"parent"]; -[post fetchIfNeededInBackgroundWithBlock:^(PFObject *post, NSError *error) { - NSString *title = post[@"title"]; - // do something with your title variable +PFObject *post = myComment[@"post"]; +[post fetchInBackgroundWithBlock:^(PFObject * _Nullable object, NSError * _Nullable error) { + NSString *title = post[@"title"]; + if (title) { // do something with title } }]; ``` ```swift let post = myComment["parent"] as! PFObject -post.fetchIfNeededInBackground { (post: PFObject?, error: Error?) in - let title = post?["title"] as? String - // do something with your title variable +post.fetchIfNeededInBackground { (object, error) in + if let title = post["title"] as? String { + // do something with your title variable + } else if let errorString = error?.localizedDescription { + print(errorString) + } } ```
@@ -461,78 +471,75 @@ You can also model a many-to-many relation using the `PFRelation` object. This PFUser *user = [PFUser currentUser]; PFRelation *relation = [user relationForKey:@"likes"]; [relation addObject:post]; -[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // The post has been added to the user's likes relation. - } else { - // There was a problem, check error.description - } +[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // The post has been added to the user's likes relation. + } else { + // There was a problem, check error.description + } }]; ``` ```swift -let user = PFUser.current() -let relation = user?.relation(forKey: "likes") -relation?.add(post) -user?.saveInBackground(block: { (success: Bool, error: Error?) in - if (success) { +guard let user = PFUser.current() else { return } +let relation = user.relation(forKey: "likes") +relation.add(post) +user.saveInBackground { (succeeded, error) in + if (succeeded) { // The post has been added to the user's likes relation. } else { // There was a problem, check error.description } -}) - +} ```
-You can remove a post from the `PFRelation` with something like: - -
-```objective_c -[relation removeObject:post]; -``` -```swift -relation?.remove(post) -``` -
+You can remove a post from the `PFRelation` similarly using the `removeObject:` function followed by saving the parent object. By default, the list of objects in this relation are not downloaded. You can get the list of `Post`s by using calling `findObjectsInBackgroundWithBlock:` on the `PFQuery` returned by `query`. The code would look like:
```objective_c -[[relation query] findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { - if (error) { - // There was an error - } else { - // objects has all the Posts the current user liked. - } +[[relation query] findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) { + if (error) { + // There was an error + } else { + // objects has all the Posts the current user liked. + } }]; ``` ```swift -relation?.query().findObjectsInBackground(block: { (objects: [PFObject]?, error: Error?) in - if let error = error { - // There was an error - print(error.localizedDescription) +relation.query().findObjectsInBackground { (object, error) in + if error == nil { + // Success } else { - // objects has all the Posts the current user liked. + // Failure! } }) ```
-If you want only a subset of the `Post`s you can add extra constraints to the `PFQuery` returned by `query` like this: +You can add constraints to a `PFRelation`'s query by adding constraints to the `PFQuery` returned by its `query` parameter as demonstrated below:
```objective_c PFQuery *query = [relation query]; -// Add other query constraints. +[query whereKey:"category" equalTo:@"development"]; +PFObject *object = [query getFirstObject]; // Query first object found +if (object) { + // Do something with object +} ``` ```swift -var query = relation?.query() -// Add other query constraints. -``` +var query = relation.query() +query.whereKey("category", equalTo: "development") + +// Query first object found +if let object = try? query.getFirstObject() { + // Do something with object +} ```
-For more details on `PFQuery` please look at the query portion of this guide. A `PFRelation` behaves similar to an `NSArray` of `PFObject`, so any queries you can do on arrays of objects (other than `includeKey:`) you can do on `PFRelation`. +You can learn more about queries by visiting the [PFQuery](#queries) section. A `PFRelation` behaves similarly to an array of `PFObject` yet has a built in query that's capable of everything a standard `PFQuery` is other than `includeKey:`. ## Data Types @@ -596,7 +603,7 @@ bigObject.saveInBackground() ```
-We do not recommend storing large pieces of binary data like images or documents on `PFObject`. `PFObject`s should not exceed 128 kilobytes in size. We recommend you use `PFFileObject`s to store images, documents, and other types of files. You can do so by instantiating a `PFFileObject` object and setting it on a field. See [Files](#files) for more details. +We do not recommend storing large pieces of binary data like images or documents on `PFObject`. We recommend you use `PFFileObject`s to store images, documents, and other types of files. You can do so by instantiating a `PFFileObject` object and setting it on a field. See [Files](#files) for more details. For more information about how Parse handles data, check out our documentation on [Data](#data). @@ -612,7 +619,7 @@ shield[@"fireProof"] = @NO; shield[@"rupees"] = @50; ``` ```swift -var shield = PFObject(className:"Armor") +var shield = PFObject(className: "Armor") shield["displayName"] = "Wooden Shield" shield["fireProof"] = false shield["rupees"] = 50 @@ -638,20 +645,12 @@ shield.rupees = 50 ### Subclassing PFObject -To create a `PFObject` subclass: +To create a subclass: -1. Declare a subclass which conforms to the `PFSubclassing` protocol. -2. Implement the class method `parseClassName`. This is the string you would pass to `initWithClassName:` and makes all future class name references unnecessary. -3. Import `PFObject+Subclass` in your .m file. This implements all methods in `PFSubclassing` beyond `parseClassName`. -4. Call `[YourClass registerSubclass]` before Parse `setApplicationId:clientKey:`. +1. Declare a subclass of `PFObject` which conforms to the `PFSubclassing` protocol. +2. Implement the static method `parseClassName` and return the string you would pass to `initWithClassName:`. This makes all future class name references unnecessary. -An easy way to do this is with your class' [+load](https://developer.apple.com/reference/objectivec/nsobject/1418815-load?language=objc) (Obj-C only) or with [initialize](https://developer.apple.com/reference/objectivec/nsobject/1418639-initialize) (both Obj-C and Swift) methods. - -Please note that the `initialize` method is not called until the class receives its first message, meaning that you need to call any instance or class method on your subclass before it will be registered with Parse SDK. - -The following code successfully declares, implements, and registers the `Armor` subclass of `PFObject`: - - +Note: Objective-C developers should Import `PFObject+Subclass` in your .m file. This implements all methods in `PFSubclassing` beyond `parseClassName`. ### Properties & Methods @@ -659,8 +658,6 @@ Adding custom properties and methods to your `PFObject` subclass helps encapsula `PFObject` supports dynamic synthesizers just like `NSManagedObject`. Declare a property as you normally would, but use `@dynamic` rather than `@synthesize` in your .m file. The following example creates a `displayName` property in the `Armor` class: - - You can access the displayName property using `armor.displayName` or `[armor displayName]` and assign to it using `armor.displayName = @"Wooden Shield"` or `[armor setDisplayName:@"Wooden Sword"]`. Dynamic properties allow Xcode to provide autocomplete and catch typos. `NSNumber` properties can be implemented either as `NSNumber`s or as their primitive counterparts. Consider the following example: @@ -688,16 +685,19 @@ If you need more complicated logic than simple property access, you can declare PFImageView *view = [[PFImageView alloc] initWithImage:kPlaceholderImage]; view.file = self.iconFile; [view loadInBackground]; + return view; } ``` + ```swift -@NSManaged var iconFile: PFFileObject +@NSManaged var iconFile: PFFileObject! func iconView() -> UIImageView { let view = PFImageView(imageView: PlaceholderImage) view.file = iconFile view.loadInBackground() + return view } ``` @@ -705,4 +705,4 @@ func iconView() -> UIImageView { ### Initializing Subclasses -You should create new objects with the `object` class method. This constructs an autoreleased instance of your type and correctly handles further subclassing. To create a reference to an existing object, use `objectWithoutDataWithObjectId:`. +You should initialize new instances of subclassses with standard initialization methods. To create a new instance of an existing Parse object, use the inherited `PFObject` class function `objectWithoutDataWithObjectId:`, or create a new object and set the objectId property manually. diff --git a/_includes/ios/push-notifications.md b/_includes/ios/push-notifications.md index b88603b70..ac028faa3 100644 --- a/_includes/ios/push-notifications.md +++ b/_includes/ios/push-notifications.md @@ -4,6 +4,8 @@ Push Notifications are a great way to keep your users engaged and informed about If you haven't installed the SDK yet, please [head over to the Push QuickStart]({{ site.baseUrl }}/parse-server/guide/#push-notifications-quick-start) to get our SDK up and running. +Please note that client push is not available with Parse Server due to it being a significant security risk, it is recommended that to trigger push notifications from your iOS app you run a cloud function that sends the push using the `masterKey`. If you must use client push, you could fork Parse Server and enable it or alternatively [Back4App](https://www.back4app.com) offer it as an option for testing purposes only. + ## Setting Up Push If you want to start using push, start by completing the [Push Notifications QuickStart]({{ site.baseUrl }}/parse-server/guide/#push-notifications-quick-start) to learn how to configure your app. Come back to this guide afterwards to learn more about the push features offered by Parse. @@ -75,13 +77,9 @@ The Parse SDK will avoid making unnecessary requests. If a `PFInstallation` is s There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting](#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section. -Sending notifications is often done from the Parse.com push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. - -However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app, as outlined [on our blog](http://blog.parse.com/2014/09/03/the-dangerous-world-of-client-push/). We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. +Sending notifications is often done from the Parse Dashboard push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). -Configuring push client settings - -You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. +You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. ### Using Channels @@ -141,50 +139,6 @@ let subscribedChannels = PFInstallation.currentInstallation().channels If you plan on changing your channels from Cloud Code or the data browser, note that you'll need to call some form of `fetch` prior to this line in order to get the most recent channels. -#### Sending Pushes to Channels - -In the iOS/OS X SDK, the following code can be used to alert all subscribers of the "Giants" channel that their favorite team just scored. This will display a notification center alert to iOS/OS X users and a system tray notification to Android users. - -
-```objective_c -// Send a notification to all devices subscribed to the "Giants" channel. -PFPush *push = [[PFPush alloc] init]; -[push setChannel:@"Giants"]; -[push setMessage:@"The Giants just scored!"]; -[push sendPushInBackground]; -``` -```swift -// Send a notification to all devices subscribed to the "Giants" channel. -let push = PFPush() -push.setChannel("Giants") -push.setMessage("The Giants just scored!") -push.sendPushInBackground() -``` -
- -If you want to target multiple channels with a single push notification, you can use an `NSArray` of channels. - -
-```objective_c -NSArray *channels = [NSArray arrayWithObjects:@"Giants", @"Mets", nil]; -PFPush *push = [[PFPush alloc] init]; - -// Be sure to use the plural 'setChannels'. -[push setChannels:channels]; -[push setMessage:@"The Giants won against the Mets 2-3."]; -[push sendPushInBackground]; -``` -```swift -let channels = [ "Giants", "Mets" ]; -let push = PFPush() - -// Be sure to use the plural 'setChannels'. -push.setChannels(channels) -push.setMessage("The Giants won against the Mets 2-3.") -push.sendPushInBackground() -``` -
- ### Using Advanced Targeting While channels are great for many applications, sometimes you need more precision when targeting the recipients of your pushes. Parse allows you to write a query for any subset of your `Installation` objects using the [querying API](#queries) and to send them a push. @@ -231,353 +185,12 @@ installation.saveInBackground() ``` -#### Sending Pushes to Queries - -Once you have your data stored on your `Installation` objects, you can use a `PFQuery` to target a subset of these devices. `Installation` queries work just like any other [Parse query](#queries), but we use the special static method `[PFInstallation query]` to create it. We set this query on our `PFPush` object, before sending the notification. - -
-```objective_c -// Create our Installation query -PFQuery *pushQuery = [PFInstallation query]; -[pushQuery whereKey:@"injuryReports" equalTo:@YES]; - -// Send push notification to query -PFPush *push = [[PFPush alloc] init]; -[push setQuery:pushQuery]; // Set our Installation query -[push setMessage:@"Willie Hayes injured by own pop fly."]; -[push sendPushInBackground]; -``` -```swift -// Create our Installation query -let pushQuery = PFInstallation.query() -pushQuery.whereKey("injuryReports", equalTo: true) - -// Send push notification to query -let push = PFPush() -push.setQuery(pushQuery) // Set our Installation query -push.setMessage("Willie Hayes injured by own pop fly.") -push.sendPushInBackground() -``` -
- -We can even use channels with our query. To send a push to all subscribers of the "Giants" channel but filtered by those who want score update, we can do the following: - -
-```objective_c -// Create our Installation query -PFQuery *pushQuery = [PFInstallation query]; -[pushQuery whereKey:@"channels" equalTo:@"Giants"]; // Set channel -[pushQuery whereKey:@"scores" equalTo:@YES]; - -// Send push notification to query -PFPush *push = [[PFPush alloc] init]; -[push setQuery:pushQuery]; -[push setMessage:@"Giants scored against the A's! It's now 2-2."]; -[push sendPushInBackground]; -``` -```swift -// Create our Installation query -let pushQuery = PFInstallation.query() -pushQuery.whereKey("channels", equalTo:"Giants") // Set channel -pushQuery.whereKey("scores", equalTo:true) - -// Send push notification to query -let push = PFPush() -push.setQuery(pushQuery) // Set our Installation query -push.setMessage("Giants scored against the A's! It's now 2-2.") -push.sendPushInBackground() -``` -
- -If we store relationships to other objects in our `Installation` class, we can also use those in our query. For example, we could send a push notification to all users near a given location like this. - -
-```objective_c -// Find users near a given location -PFQuery *userQuery = [PFUser query]; -[userQuery whereKey:@"location" nearGeoPoint:stadiumLocation withinMiles:@1] - -// Find devices associated with these users -PFQuery *pushQuery = [PFInstallation query]; -[pushQuery whereKey:@"user" matchesQuery:userQuery]; - -// Send push notification to query -PFPush *push = [[PFPush alloc] init]; -[push setQuery:pushQuery]; // Set our Installation query -[push setMessage:@"Free hotdogs at the Parse concession stand!"]; -[push sendPushInBackground]; -``` -```swift -// Find users near a given location -let userQuery = PFUser.query() -userQuery.whereKey("location", nearGeoPoint: stadiumLocation, withinMiles: 1) - -// Find devices associated with these users -let pushQuery = PFInstallation.query() -pushQuery.whereKey("user", matchesQuery: userQuery) - -// Send push notification to query -let push = PFPush() -push.setQuery(pushQuery) // Set our Installation query -push.setMessage("Free hotdogs at the Parse concession stand!") -push.sendPushInBackground() -``` -
- -## Sending Options - -Push notifications can do more than just send a message. On iOS/OS X, pushes can also include the sound to be played, the badge number to display as well as any custom data you wish to send. An expiration date can also be set for the notification in case it is time sensitive. - -### Customizing your Notifications - -If you want to send more than just a message, you will need to use an `NSDictionary` to package all of the data. There are some reserved fields that have a special meaning. - -* **`alert`**: the notification's message. -* **`badge`**: _(iOS/OS X only)_ the value indicated in the top right corner of the app icon. This can be set to a value or to `Increment` in order to increment the current value by 1. -* **`sound`**: _(iOS/OS X only)_ the name of a sound file in the application bundle. -* **`content-available`**: _(iOS only)_ If you are a writing an app using the Remote Notification Background Mode [ introduced in iOS7](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS7.html#//apple_ref/doc/uid/TP40013162-SW10) (a.k.a. "Background Push"), set this value to 1 to trigger a background download. -* **`category`**: _(iOS only)_ the identifier of the [`UNNotification​Category`](https://developer.apple.com/reference/usernotifications/unnotificationcategory) for this push notification. -* **`uri`**: _(Android only)_ an optional field that contains a URI. When the notification is opened, an `Activity` associate with opening the URI is launched. -* **`title`**: _(Android, Windows 8, and Windows Phone 8 only)_ the value displayed in the Android system tray or Windows toast notification. - -For example, to send a notification that increases the current badge number by 1 and plays a custom sound, you can do the following: - -
-```objective_c -NSDictionary *data = @{ - @"alert" : @"The Mets scored! The game is now tied 1-1!", - @"badge" : @"Increment", - @"sound" : @"cheering.caf" -}; -PFPush *push = [[PFPush alloc] init]; -[push setChannels:@[ @"Mets" ]]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let data = [ - "alert" : "The Mets scored! The game is now tied 1-1!", - "badge" : "Increment", - "sound" : "cheering.caf" -] -let push = PFPush() -push.setChannels(["Mets"]) -push.setData(data) -push.sendPushInBackground() -``` -
- -It is also possible to specify your own data in this dictionary. As we'll see in the [Receiving Notifications](#receiving-pushes) section, you will have access to this data only when the user opens your app via the notification. This can be useful for displaying a different view controller when a user opens certain notifications. - -
-```objective_c -NSDictionary *data = @{ - @"alert" : @"Ricky Vaughn was injured in last night's game!", - @"name" : @"Vaughn", - @"newsItem" : @"Man bites dog" -}; -PFPush *push = [[PFPush alloc] init]; -[push setQuery:injuryReportsQuery]; -[push setChannel:@"Indians"]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let data = [ - "alert" : "Ricky Vaughn was injured in last night's game!", - "name" : "Vaughn", - "newsItem" : "Man bites dog" -] -let push = PFPush() -push.setQuery(injuryReportsdata) -push.setChannel("Indians") -push.setData(data) -push.sendPushInBackground() -``` -
- -Whether your push notifications increment the app's badge or set it to a specific value, your app will eventually need to clear its badge. This is covered in [Clearing the Badge](#receiving-pushes). - -### Setting an Expiration Date - -When a user's device is turned off or not connected to the internet, push notifications cannot be delivered. If you have a time sensitive notification that is not worth delivering late, you can set an expiration date. This avoids needlessly alerting users of information that may no longer be relevant. - -There are two methods provided by the `PFPush` class to allow setting an expiration date for your notification. The first is `expireAtDate:` which simply takes an `NSDate` specifying when Parse should stop trying to send the notification. - -
-```objective_c -// Create date object for tomorrow -NSDateComponents *comps = [[NSDateComponents alloc] init]; -[comps setYear:2015]; -[comps setMonth:8]; -[comps setDay:14]; -NSCalendar *gregorian = - [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; -NSDate *date = [gregorian dateFromComponents:comps]; - -// Send push notification with expiration date -PFPush *push = [[PFPush alloc] init]; -[push expireAtDate:date]; -[push setQuery:everyoneQuery]; -[push setMessage:@"Season tickets on sale until August 8th!"]; -[push sendPushInBackground]; -``` -```swift -// Create date object for tomorrow -let comps = NSDateComponents() -comps.year = 2015 -comps.month = 8 -comps.day = 14 -let gregorian = NSCalendar(calendarIdentifier: NSGregorianCalendar) -let date = gregorian!.dateFromComponents(comps); - -// Send push notification with expiration -let push = PFPush() -push.expireAtDate(date) -push.setQuery(everyoneQuery) -push.setMessage("Season tickets on sale until August 8th!") -push.sendPushInBackground() -``` -
- -There is however a caveat with this method. Since device clocks are not guaranteed to be accurate, you may end up with inaccurate results. For this reason, the `PFPush` class also provides the `expireAfterTimeInterval:` method which accepts an `NSTimeInterval` object. The notification will expire after the specified interval has elapsed. - -
-```objective_c -// Create time interval -NSTimeInterval interval = 60*60*24*7; // 1 week - -// Send push notification with expiration interval -PFPush *push = [[PFPush alloc] init]; -[push expireAfterTimeInterval:interval]; -[push setQuery:everyoneQuery]; -[push setMessage:@"Season tickets on sale until next week!"]; -[push sendPushInBackground]; -``` -```swift -// Create time interval -let interval = 60*60*24*7; // 1 week - -// Send push notification with expiration interval - -let push = PFPush() -push.expireAfterTimeInterval(interval) -push.setQuery(everyoneQuery) -push.setMessage("Season tickets on sale until next week!") -push.sendPushInBackground() -``` -
- -### Targeting by Platform - -If you build a cross platform app, it is possible you may only want to target devices of a particular operating system. Advanced Targeting allow you to filter which of these devices are targeted. - -The following example would send a different notification to Android, iOS, and Windows users. - -
-```objective_c -PFQuery *query = [PFInstallation query]; -[query whereKey:@"channels" equalTo:@"suitcaseOwners"]; - -// Notification for Android users -[query whereKey:@"deviceType" equalTo:@"android"]; -PFPush *androidPush = [[PFPush alloc] init]; -[androidPush setMessage:@"Your suitcase has been filled with tiny robots!"]; -[androidPush setQuery:query]; -[androidPush sendPushInBackground]; - -// Notification for iOS users -[query whereKey:@"deviceType" equalTo:@"ios"]; -PFPush *iOSPush = [[PFPush alloc] init]; -[iOSPush setMessage:@"Your suitcase has been filled with tiny apples!"]; -[iOSPush setChannel:@"suitcaseOwners"]; -[iOSPush setQuery:query]; -[iOSPush sendPushInBackground]; - -// Notification for Windows 8 users -[query whereKey:@"deviceType" equalTo:@"winrt"]; -PFPush *winPush = [[PFPush alloc] init]; -[winPush setMessage:@"Your suitcase has been filled with tiny glass!"]; -[winPush setQuery:query]; -[winPush sendPushInBackground]; - -// Notification for Windows Phone 8 users -[query whereKey:@"deviceType" equalTo:@"winphone"]; -PFPush *wpPush = [[PFPush alloc] init]; -[wpPush setMessage:@"Your suitcase is very hip; very metro."]; -[wpPush setQuery:query]; -[wpPush sendPushInBackground]; -``` -```swift -let query = PFInstallation.query() -if let query = query { - query.whereKey("channels", equalTo: "suitcaseOwners") - - // Notification for Android users - query.whereKey("deviceType", equalTo: "android") - let androidPush = PFPush() - androidPush.setMessage("Your suitcase has been filled with tiny robots!") - androidPush.setQuery(query) - androidPush.sendPushInBackground() - - // Notification for iOS users - query.whereKey("deviceType", equalTo: "ios") - let iOSPush = PFPush() - iOSPush.setMessage("Your suitcase has been filled with tiny apples!") - iOSPush.setChannel("suitcaseOwners") - iOSPush.setQuery(query) - iOSPush.sendPushInBackground() - - // Notification for Windows 8 users - query.whereKey("deviceType", equalTo: "winrt") - let winPush = PFPush() - winPush.setMessage("Your suitcase has been filled with tiny glass!") - winPush.setQuery(query) - winPush.sendPushInBackground() - - // Notification for Windows Phone 8 users - query.whereKey("deviceType", equalTo: "winphone") - let wpPush = PFPush() - wpPush.setMessage("Your suitcase is very hip; very metro.") - wpPush.setQuery(query) - wpPush.sendPushInBackground() -} -``` -
- -## Scheduling Pushes - -Sending scheduled push notifications is not currently supported by the iOS or OS X SDKs. Take a look at the [REST API]({{ site.baseUrl }}/rest/guide/#scheduling-pushes), [JavaScript SDK]({{ site.baseUrl }}/js/guide/#scheduling-pushes) or the Parse.com push console. - ## Receiving Pushes -As we saw in the [Customizing Your Notification](#sending-options) section, it is possible to send arbitrary data along with your notification message. We can use this data to modify the behavior of your app when a user opens a notification. For example, upon opening a notification saying that a friend commented on a user's picture, it would be nice to display this picture. +It is possible to send arbitrary data along with your notification message, which is explained in the [Sending Options](https://docs.parseplatform.org/js/guide/#sending-options) section in the JavaScript docs. We can use this data to modify the behavior of your app when a user opens a notification. For example, upon opening a notification saying that a friend commented on a user's picture, it would be nice to display this picture. This could be done by sending the `objectId` of the picture in the push payload and then fetching the image once the user opens the notification. Due to the package size restrictions imposed by Apple, you need to be careful in managing the amount of extra data sent, since it will cut down on the maximum size of your message. For this reason, it is recommended that you keep your extra keys and values as small as possible. -
-```objective_c -NSDictionary *data = @{ - @"alert" : @"James commented on your photo!", - @"p" : @"vmRZXZ1Dvo" // Photo's object id -}; -PFPush *push = [[PFPush alloc] init]; -[push setQuery:photoOwnerQuery]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let data = [ - "alert" : "James commented on your photo!", - "p" : "vmRZXZ1Dvo" // Photo's object id -] -let push = PFPush() -push.setQuery(photoOwnerQuery) -push.setData(data) -push.sendPushInBackground() -``` -
- ### Responding to the Payload When an app is opened from a notification, the data is made available in the `application:didFinishLaunchingWithOptions:` methods through the `launchOptions` dictionary. @@ -678,7 +291,7 @@ You can read more about handling push notifications in Apple's [Local and Push N ### Tracking Pushes and App Opens -To track your users' engagement over time and the effect of push notifications, we provide some hooks in the `PFAnalytics` class. You can view the open rate for a specific push notification on the Parse.com push console. You can also view overall app open and push open graphs are on the Parse analytics console. Our analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release. +To track your users' engagement over time and the effect of push notifications, we provide some hooks in the `PFAnalytics` class. You can view the open rate for a specific push notification on the Parse Dashboard push console. You can also view overall app open and push open graphs are on the Parse analytics console. Our analytics graphs are rendered in real time, so you can easily verify that your application is sending the correct analytics events before your next release. This section assumes that you've already set up your application to [save the Installation object](#installations). Push open tracking only works when your application's devices are associated with saved `Installation` objects. @@ -781,7 +394,7 @@ To track analytics around local notifications, note that `application:didReceive #### Clearing the Badge -A good time to clear your app's badge is usually when your app is opened. Setting the badge property on the current installation will update the application icon badge number and ensure that the latest badge value will be persisted to the server on the next save. All you need to do is: +A good time to clear your app's badge is usually when your app is opened. Setting the badge property on the current installation will update the application icon badge number and ensure that the latest badge value will be persisted to the server on the next save. All you need to do is:
```objective_c @@ -822,7 +435,7 @@ For each push campaign sent through the Parse web push console, you can allocate After you send the push, you can come back to the push console to see in real time which version resulted in more push opens, along with other metrics such as statistical confidence interval. It's normal for the number of recipients in each group to be slightly different because some devices that we had originally allocated to that experiment group may have uninstalled the app. It's also possible for the random group assignment to be slightly uneven when the test audience size is small. Since we calculate open rate separately for each group based on recipient count, this should not significantly affect your experiment results. -Getting experiment results +Getting experiment results If you are happy with the way one message performed, you can send that to the rest of your app's devices (i.e. the “Launch Group”). This step only applies to A/B tests where you vary the message. @@ -873,37 +486,9 @@ If you're unsure about the answer to any of the above questions, read on! ### Confirm that the push campaign was created -Having everything set up correctly in your Parse app won't help if your request to send a push notification does not reach Parse. The first step in debugging a push issue is to confirm that the push campaign is listed in your push logs. You can find these logs by visiting your app's [Dashboard](https://github.com/parse-community/parse-dashboard) and clicking on Push. - -If the push notification campaign is not showing up on that list, the issue is quite simple to resolve. Go back to your push notification sending code and make sure to check for any error responses. If you're using any of the client SDKs, make sure to listen for and catch any errors that may be returned. For example, you could log errors like so: - -
-```objective_c -[push sendPushInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (success) { - NSLog(@"The push campaign has been created."); - } else if (error.code == kPFErrorPushMisconfigured) { - NSLog(@"Could not send push. Push is misconfigured: %@", error.description); - } else { - NSLog(@"Error sending push: %@", error.description); - } -}]; -``` -```swift -push.sendPushInBackgroundWithBlock { - (success: Bool , error: NSError?) -> Void in - if (success) { - print("The push campaign has been created."); - } else if (error!.code == 112) { - print("Could not send push. Push is misconfigured: \(error!.description)."); - } else { - print("Error sending push: \(error!.description)."); - } -} -``` -
+Having everything set up correctly in your Parse app won't help if your request to send a push notification does not reach Parse. The first step in debugging a push issue is to confirm that the push campaign is listed in your push logs. You can find these logs by visiting your app's Dashboard and clicking on Push. -Please note that SDKs that use a Client Key, such as the iOS/OS X SDKs, can only send push notifications if Client Push is enabled in your Parse app's Push Notification settings. Otherwise, you'll only be able to send pushes from the web console, the REST API, or by using the JavaScript SDK from Cloud Code. We strongly encourage developers to turn off Client Push before releasing their app publicly unless your use case allows for any amount of arbitrary pushes to be sent by any of your users. You can read our security guide for more information. +If the push notification campaign is not showing up on that list, the issue is quite simple to resolve. Go back to your push notification sending code and make sure to check for any error responses. ### Verify your Targeting diff --git a/_includes/ios/queries.md b/_includes/ios/queries.md index 449921b18..8454ef44b 100644 --- a/_includes/ios/queries.md +++ b/_includes/ios/queries.md @@ -530,10 +530,6 @@ query.whereKey("arrayKey", containsAllObjectsIn:[2, 3, 4]) ## Queries on String Values -
- If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
- Use `whereKey:hasPrefix:` to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets:
diff --git a/_includes/ios/user-interface.md b/_includes/ios/user-interface.md index 61689155f..b85a4d755 100644 --- a/_includes/ios/user-interface.md +++ b/_includes/ios/user-interface.md @@ -2,11 +2,9 @@ At the end of the day, users of your app are going to be interacting with UIKit components. -[ParseUI](https://github.com/parse-community/ParseUI-iOS) is an opensource collection of a handy user interface components aimed to streamline and simplify user authentication, displaying lists of data, and other common app elements. +ParseUI is an opensource collection of a handy user interface components aimed to streamline and simplify user authentication, displaying lists of data, and other common app elements. -Please note that [ParseUI](https://github.com/parse-community/ParseUI-iOS) is not included inside the main Parse iOS SDK. - -To learn more on how to install it - follow the instructions on the official [GitHub page](https://github.com/parse-community/ParseUI-iOS). +ParseUI can be installed by leveraging Cocoapods 'subspecs', simply add `pod 'Parse/UI'` to your Podfile and run `pod install`. Once installed just use `import Parse` to use ParseUI. More details can be found on the official [GitHub page](https://github.com/parse-community/Parse-SDK-iOS-OSX#other-installation-options) ## PFLogInViewController @@ -23,7 +21,7 @@ logInController.delegate = self; ```swift var logInController = PFLogInViewController() logInController.delegate = self -self.presentViewController(logInController, animated:true, completion: nil) +self.present(logInController, animated:true, completion: nil) ```
@@ -50,11 +48,11 @@ Any of the above features can be turned on or off. The options can be set using | PFLogInFieldsDismissButton); ``` ```swift - logInController.fields = [PFLogInFields.UsernameAndPassword, - PFLogInFields.LogInButton, - PFLogInFields.SignUpButton, - PFLogInFields.PasswordForgotten, - PFLogInFields.DismissButton] +logInController.fields = [PFLogInFields.usernameAndPassword, + PFLogInFields.logInButton, + PFLogInFields.signUpButton, + PFLogInFields.passwordForgotten, + PFLogInFields.dismissButton] ```
@@ -74,9 +72,9 @@ logInController.fields = (PFLogInFieldsUsernameAndPassword | PFLogInFieldsTwitter); ``` ```swift -logInController.fields = [PFLogInFields.UsernameAndPassword, - PFLogInFields.Facebook, - PFLogInFields.Twitter] +logInController.fields = [PFLogInFields.usernameAndPassword, + PFLogInFields.facebook, + PFLogInFields.twitter] ``` @@ -93,7 +91,7 @@ logInController.facebookPermissions = @[ @"friends_about_me" ]; var logInController = PFLogInViewController() logInController.delegate = self logInController.facebookPermissions = [ "friends_about_me" ] -self.presentViewController(logInController, animated:true, completion:nil) +self.present(logInController, animated:true, completion:nil) ``` @@ -114,11 +112,11 @@ When the user signs in or cancels, the `PFLogInViewController` notifies the dele ``` ```swift func logInViewController(controller: PFLogInViewController, didLogInUser user: PFUser!) -> Void { - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) } -func logInViewControllerDidCancelLogIn(controller: PFLogInViewController) -> Void { - self.dismissViewControllerAnimated(true, completion: nil) +func logInViewControllerDidCancelLog(in controller: PFLogInViewController) -> Void { + self.dismiss(animated: true, completion: nil) } ``` @@ -148,15 +146,15 @@ You might want to use your own logo or background image. You can achieve this by @end ``` ```swift -class MyLogInViewController : PFLogInViewController { +class MyLogInViewController: PFLogInViewController { override func viewDidLoad() { super.viewDidLoad() - self.view.backgroundColor = UIColor.darkGrayColor() + self.view.backgroundColor = .darkGray - let logoView = UIImageView(image: UIImage(named:"logo.png")) - self.logInView.logo = logoView + let logoView = UIImageView(image: UIImage(named:"logo.png")) + self.logInView?.logo = logoView } } @@ -174,7 +172,7 @@ logInController.signUpController = [[MySignUpViewController alloc] init]; ```swift let logInController = MyLogInViewController() logInController.signUpController = MySignUpViewController() -self.presentViewController(logInController, animated: true, completion: nil) +self.present(logInController, animated: true, completion: nil) ``` @@ -246,7 +244,7 @@ signUpController.delegate = self; ```swift let signUpController = PFSignUpViewController() signUpController.delegate = self -self.presentViewController(signUpController, animated: true, completion: nil) +self.present(signUpController, animated: true, completion: nil) ``` @@ -274,15 +272,11 @@ signUpController.fields = (PFSignUpFieldsUsernameAndPassword | PFSignUpFieldsDismissButton); ``` ```swift -signUpController.fields = (PFSignUpFields.UsernameAndPassword - | PFSignUpFields.SignUpButton - | PFSignUpFields.Email - | PFSignUpFields.Additional - | PFSignUpFields.DismissButton) +signUpController.fields = [.usernameAndPassword, .signUpButton, .email, .additional, .dismissButton] ``` -Essentially, you use the bitwise or operator (`|`) to chain up all the options you want to include in the sign up screen, and assign the value to `fields`. Similarly, you can turn off any field by omitting it in the assignment to fields. +Essentially, you create an array (in Swift), or use the bitwise or operator (for objective-c), to chain all of the options you want to include in the sign up screen, and assign the value to `fields`. Similarly, you can turn off any field by omitting it in the assignment to fields. ### Responding to Sign Up Success, Failure or Cancellation @@ -300,11 +294,11 @@ When the user signs up or cancels, the `PFSignUpViewController` notifies the del ``` ```swift func signUpViewController(signUpController: PFSignUpViewController, didSignUpUser user: PFUser) -> Void { - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) } func signUpViewControllerDidCancelSignUp(signUpController: PFSignUpViewController) -> Void { - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) } ``` @@ -340,10 +334,10 @@ class MySignUpViewController : PFSignUpViewController { override func viewDidLoad() { super.viewDidLoad() - self.view.backgroundColor = UIColor.darkGrayColor() + self.view.backgroundColor = .darkGray - let logoView = UIImageView(image: UIImage(named: "logo.png")) - self.signUpView.logo = logoView // 'logo' can be any UIView + let logoView = UIImageView(image: UIImage(named: "logo.png")) + self.signUpView?.logo = logoView // 'logo' can be any UIView } } ``` @@ -362,12 +356,12 @@ Often you will want to run some client-side validation on the sign up informatio } ``` ```swift -func signUpViewController(signUpController: PFSignUpViewController!, - shouldBeginSignUp info: [NSObject : AnyObject]!) -> Bool { - if let password = info?["password"] as? String { - return password.utf16Count >= 8 - } - return false +func signUpViewController(_ signUpController: PFSignUpViewController, + shouldBeginSignUp info: [String : String]) -> Bool { + if let password = info["password"] { + return password.utf16.count >= 8 + } + return false } ``` `info` is a dictionary that contains all sign up fields, such as username, password, email, and additional. @@ -402,20 +396,20 @@ class MySignUpViewController : PFSignUpViewController { override func viewDidLoad() { super.viewDidLoad() - self.signUpView.usernameField.placeholder = "phone" + self.signUpView?.usernameField?.placeholder = "phone" } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - self.signUpView.signUpButton.frame = CGRectMake(...) // Set a different frame. + self.signUpView?.signUpButton?.frame = CGRectMake(...) // Set a different frame. } } ``` -Developer interested in this kind of customization should take a look at the interface of [`PFSignUpView`](http://parseplatform.org/Parse-SDK-iOS-OSX/api/Classes/PFSignUpView.html), where all customizable properties are documented. +Developers interested in this kind of customization should take a look at the interface of [`PFSignUpView`](http://parseplatform.org/Parse-SDK-iOS-OSX/api/Classes/PFSignUpView.html), where all customizable properties are documented. ### Portrait and Landscape @@ -504,55 +498,55 @@ The easiest way to understand this class is with an example. This subclass of `P @end ``` ```swift -class SimpleTableViewController : PFQueryTableViewController { - - override init(style: UITableViewStyle, className: String?) { - super.init(style: style, className: className) - parseClassName = "Todo" - pullToRefreshEnabled = true - paginationEnabled = true - objectsPerPage = 25 - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - parseClassName = "Todo" - pullToRefreshEnabled = true - paginationEnabled = true - objectsPerPage = 25 - } - - override func queryForTable() -> PFQuery { - let query = PFQuery(className: self.parseClassName!) +class SimpleTableViewController: PFQueryTableViewController { + + override init(style: UITableView.Style, className: String?) { + super.init(style: style, className: className) + parseClassName = "Todo" + pullToRefreshEnabled = true + paginationEnabled = true + objectsPerPage = 25 + } - // If no objects are loaded in memory, we look to the cache first to fill the table - // and then subsequently do a query against the network. - if self.objects!.count == 0 { - query.cachePolicy = .CacheThenNetwork - } + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + parseClassName = "Todo" + pullToRefreshEnabled = true + paginationEnabled = true + objectsPerPage = 25 + } - query.orderByDescending("createdAt") + override func queryForTable() -> PFQuery { + let query = PFQuery(className: self.parseClassName!) - return query + // If no objects are loaded in memory, we look to the cache first to fill the table + // and then subsequently do a query against the network. + if self.objects!.count == 0 { + query.cachePolicy = .cacheThenNetwork } - override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? { - let cellIdentifier = "cell" + query.order(byDescending: "createdAt") - var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? PFTableViewCell - if cell == nil { - cell = PFTableViewCell(style: .Subtitle, reuseIdentifier: cellIdentifier) - } + return query + } - // Configure the cell to show todo item with a priority at the bottom - if let object = object { - cell!.textLabel?.text = object["text"] as? String - let priority = object["priority"] as? String - cell!.detailTextLabel?.text = "Priority \(priority)" - } + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? { + let cellIdentifier = "cell" - return cell + var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? PFTableViewCell + if cell == nil { + cell = PFTableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) } + + // Configure the cell to show todo item with a priority at the bottom + if let object = object { + cell!.textLabel?.text = object["text"] as? String + let priority = object["priority"] as? String + cell!.detailTextLabel?.text = "Priority \(String(describing: priority))" + } + + return cell + } } ``` @@ -588,21 +582,21 @@ A good starting point to learn more is to look at the [API for the class](http:/ @end ``` ```swift -override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? { - let identifier = "cell" +func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? { + let identifier = "cell" - var cell = tableView.dequeueReusableCellWithIdentifier(identifier) as? PFTableViewCell - if cell == nil { - cell = PFTableViewCell(style: .Default, reuseIdentifier: identifier) - } + var cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? PFTableViewCell + if cell == nil { + cell = PFTableViewCell(style: .default, reuseIdentifier: identifier) + } - if let object = object { - cell?.textLabel?.text = object["title"] as? String - cell?.imageView?.image = UIImage(named: "placeholder.jpg") - cell?.imageView?.file = object["thumbnail"] as? PFFileObject - } + if let object = object { + cell?.textLabel?.text = object["title"] as? String + cell?.imageView?.image = UIImage(named: "placeholder.jpg") + cell?.imageView?.file = object["thumbnail"] as? PFFileObject + } - return cell + return cell } ``` @@ -703,21 +697,21 @@ Many apps need to display table view cells which contain images stored in the Pa ``` ```swift func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - let identifier = "cell" - var cell = tableView.dequeueReusableCellWithIdentifier(identifier) as? PFTableViewCell - if cell == nil { - cell = PFTableViewCell(style: .Default, reuseIdentifier: identifier) - } - - if let title = object["title"] as? String { - cell!.textLabel.text = title - } - if let thumbnail = object["thumbnail"] as? PFFileObject { - cell!.imageView.image = UIImage(named: "placeholder.jpg") - cell!.imageView.file = thumbnail - } - - return cell! + let identifier = "cell" + var cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? PFTableViewCell + if cell == nil { + cell = PFTableViewCell(style: .default, reuseIdentifier: identifier) + } + + if let title = object["title"] as? String { + cell!.textLabel.text = title + } + if let thumbnail = object["thumbnail"] as? PFFileObject { + cell!.imageView?.image = UIImage(named: "placeholder.jpg") + cell!.imageView.file = thumbnail + } + + return cell! } ``` diff --git a/_includes/ios/users.md b/_includes/ios/users.md index cc8fb25a2..7e2ada75f 100644 --- a/_includes/ios/users.md +++ b/_includes/ios/users.md @@ -518,12 +518,18 @@ There's also two code changes you'll need to make. First, add the following to y ``` ```swift import FBSDKCoreKit -import ParseFacebookUtilsV4 +import Parse // AppDelegate.swift -func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - Parse.setApplicationId("parseAppId", clientKey:"parseClientKey") - PFFacebookUtils.initializeFacebookWithApplicationLaunchOptions(launchOptions) +func application(application: UIApplicatiofunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Initialize Parse. + let parseConfig = ParseClientConfiguration { + $0.applicationId = "parseAppId" + $0.clientKey = "parseClientKey" + $0.server = "parseServerUrlString" + } + Parse.initialize(with: parseConfig) + PFFacebookUtils.initializeFacebook(applicationLaunchOptions: launchOptions) } ``` @@ -547,18 +553,30 @@ Next, add the following handlers in your app delegate. } ``` ```swift -func application(application: UIApplication, - openURL url: NSURL, - sourceApplication: String?, - annotation: AnyObject?) -> Bool { - return FBSDKApplicationDelegate.sharedInstance().application(application, - openURL: url, - sourceApplication: sourceApplication, - annotation: annotation) +func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { + + return FBSDKApplicationDelegate.sharedInstance().application( + application, + open: url, + sourceApplication: sourceApplication, + annotation: annotation + ) + +} + +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + + return FBSDKApplicationDelegate.sharedInstance().application( + app, + open: url, + sourceApplication: options[.sourceApplication] as? String, + annotation: options[.annotation] + ) + } //Make sure it isn't already declared in the app delegate (possible redefinition of func error) -func applicationDidBecomeActive(application: UIApplication) { +func applicationDidBecomeActive(_ application: UIApplication) { FBSDKAppEvents.activateApp() } ``` @@ -587,8 +605,8 @@ There are two main ways to use Facebook with your Parse users: (1) to log in (or }]; ``` ```swift -PFFacebookUtils.logInInBackgroundWithReadPermissions(permissions) { - (user: PFUser?, error: NSError?) -> Void in +PFFacebookUtils.logInInBackground(withReadPermissions: permissions) { + (user: PFUser?, error: Error?) in if let user = user { if user.isNew { print("User signed up and logged in through Facebook!") diff --git a/_includes/js/files.md b/_includes/js/files.md index 123944812..79f27c39f 100644 --- a/_includes/js/files.md +++ b/_includes/js/files.md @@ -2,7 +2,7 @@ ## Creating a Parse.File -`Parse.File` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `Parse.Object`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes). +`Parse.File` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `Parse.Object`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data. Getting started with `Parse.File` is easy. There are a couple of ways to create a file. The first is with a base64-encoded String. @@ -110,6 +110,7 @@ Parse.Cloud.httpRequest({ url: profilePhoto.url() }).then(function(response) { // The file contents are in response.buffer. }); ``` +##Deleting Files You can delete files that are referenced by objects using the [REST API]({{ site.baseUrl }}/rest/guide/#deleting-files). You will need to provide the master key in order to be allowed to delete a file. diff --git a/_includes/js/live-queries.md b/_includes/js/live-queries.md index 269802916..e463f659d 100644 --- a/_includes/js/live-queries.md +++ b/_includes/js/live-queries.md @@ -9,9 +9,11 @@ Note: Live Queries is supported only in [Parse Server](https://github.com/parse- ```javascript let query = new Parse.Query('Game'); -let subscription = query.subscribe(); +let subscription = await query.subscribe(); ``` +* Since release `2.3.0` of Parse JS SDK, the `query.subscribe()` function returns a `Promise` that resolves to the subscription object. Previous releases return the subscription object directly and require you to write `let subscription = query.subscribe();` instead. + The subscription you get is actually an event emitter. For more information on event emitter, check [here](https://nodejs.org/api/events.html). You'll get the LiveQuery events through this `subscription`. The first time you call subscribe, we'll try to open the WebSocket connection to the LiveQuery server for you. ## Event Handling diff --git a/_includes/js/objects.md b/_includes/js/objects.md index f12ba03cf..bb9c4310b 100644 --- a/_includes/js/objects.md +++ b/_includes/js/objects.md @@ -89,6 +89,36 @@ However, when using `extends`, the SDK is not automatically aware of your subcla Parse.Object.registerSubclass('Monster', Monster); ``` +Similarly, you can use `extends` with `Parse.User`. + +```javascript +class CustomUser extends Parse.User { + constructor(attributes) { + super(attributes); + } + + doSomething() { + return 5; + } +} +Parse.Object.registerSubclass('CustomUser', CustomUser); +``` + +In addition to queries, `logIn` and `signUp` will return the subclass `CustomUser`. + +```javascript +const customUser = new CustomUser({ foo: 'bar' }); +customUser.setUsername('username'); +customUser.setPassword('password'); +customUser.signUp().then((user) => { + // user is an instance of CustomUser + user.doSomething(); // return 5 + user.get('foo'); // return 'bar' +}); +``` + +`CustomUser.logIn` and `CustomUser.signUp` will return the subclass `CustomUser` (SDK v2.3.0). + ## Saving Objects Let's say you want to save the `GameScore` described above to the Parse Cloud. The interface is similar to a `Backbone.Model`, including the `save` method: @@ -142,6 +172,34 @@ gameScore.save({ }); ``` +### Saving Nested Objects +You may add a `Parse.Object` as the value of a property in another `Parse.Object`. By default, when you call `save()` on the parent object, all nested objects will be created and/or saved as well in a batch operation. This feature makes it really easy to manage relational data as you don't have to take care of creating the objects in any specific order. + +```javascript +var Child = Parse.Object.extend("Child"); +var child = new Child(); + +var Parent = Parse.Object.extend("Parent"); +var parent = new Child(); + +parent.save({child: child}); +// Automatically the object Child is created on the server +// just before saving the Parent +``` + +In some scenarios, you may want to prevent this default chain save. For example, when saving a team member's profile that points to an account owned by another user to which you don't have write access. In this case, setting the option `cascadeSave` to `false` may be useful: + +```javascript +var TeamMember = Parse.Object.extend("TeamMember"); +var = new TeamMember(); +teamMember.set('owninerAccount', ownerAccount); // Suppose `ownerAccount` has been created earlier. + +teamMember.save(null, { cascadeSave: false }); +// Will save `teamMember` wihout attempting to save or modify `ownerAccount` + +``` + + ## Retrieving Objects Saving data to the cloud is fun, but it's even more fun to get that data out again. If the `Parse.Object` has been uploaded to the server, you can use the `objectId` to get it using a `Parse.Query`: @@ -187,6 +245,14 @@ myObject.fetch().then((myObject) => { }); ``` +If you need to check if an object has been fetched, you can call the `isDataAvailable()` method: + +```javascript +if (!myObject.isDataAvailable()) { + await myObject.fetch(); +} +``` + ## Updating Objects Updating an object is simple. Just set some new data on it and call the save method. For example: diff --git a/_includes/js/push-notifications.md b/_includes/js/push-notifications.md index efc4fcb4b..d18745236 100644 --- a/_includes/js/push-notifications.md +++ b/_includes/js/push-notifications.md @@ -26,7 +26,7 @@ This class has several special fields that help you manage and target devices. * **`channels`**: An array of the channels to which a device is currently subscribed. * **`timeZone`**: The current time zone where the target device is located. This value is synchronized every time an `Installation` object is saved from the device. * **`deviceType`**: The type of device, "ios", "android", "winrt", "winphone", or "dotnet"_(readonly)_. -* **`pushType`**: This field is reserved for directing Parse to the push delivery network to be used. If the device is registered to receive pushes via FCM, this field will be marked "gcm". If this device is not using FCM, and is using Parse's push notification service, it will be blank _(readonly)_. +* **`pushType`**: This field is reserved for directing Parse to the push delivery network to be used. If the device is registered to receive pushes via FCM, this field will be marked "gcm". If this device is not using FCM, and is using Parse's push notification service, it will be blank _(readonly)_. * **`installationId`**: Universally Unique Identifier (UUID) for the device used by Parse. It must be unique across all of an app's installations. _(readonly)_. * **`deviceToken`**: The Apple or Google generated token used to deliver messages to the APNs or FCM push networks respectively. * **`channelUris`**: The Microsoft-generated push URIs for Windows devices. @@ -39,13 +39,13 @@ This class has several special fields that help you manage and target devices. There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting](#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section. -Sending notifications is often done from the Parse.com push console, the [REST API](#sending-pushes) or from [Cloud Code](#sending-pushes). Since the JavaScript SDK is used in Cloud Code, this is the place to start if you want to send pushes from your Cloud Functions. However, if you decide to send notifications from the JavaScript SDK outside of Cloud Code or any of the other client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. +Sending notifications is often done from the Parse Dashboard push console, the [REST API](#sending-pushes) or from [Cloud Code](#sending-pushes). Since the JavaScript SDK is used in Cloud Code, this is the place to start if you want to send pushes from your Cloud Functions. However, if you decide to send notifications from the JavaScript SDK outside of Cloud Code or any of the other client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app. -However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app, as outlined [on our blog](http://blog.parse.com/2014/09/03/the-dangerous-world-of-client-push/). We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. +However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app. We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production. -You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. +You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs. @@ -129,7 +129,7 @@ Parse.Push.send({ If we store relationships to other objects in our `Installation` class, we can also use those in our query. For example, we could send a push notification to all users near a given location like this. - ```javascript +```javascript // Find users near a given location var userQuery = new Parse.Query(Parse.User); userQuery.withinMiles("location", stadiumLocation, 1.0); @@ -163,7 +163,9 @@ If you want to send more than just a message, you can set other fields in the `d * **`alert`**: the notification's message. * **`badge`**: _(iOS only)_ the value indicated in the top right corner of the app icon. This can be set to a value or to `Increment` in order to increment the current value by 1. * **`sound`**: _(iOS only)_ the name of a sound file in the application bundle. -* **`content-available`**: _(iOS only)_ If you are a writing an app using the Remote Notification Background Mode [introduced in iOS7](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS7.html#//apple_ref/doc/uid/TP40013162-SW10) (a.k.a. "Background Push"), set this value to 1 to trigger a background download. +* **`content-available`**: _(iOS only)_ If you are a writing an app using the Remote Notification Background Mode [introduced in iOS7](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS7.html#//apple_ref/doc/uid/TP40013162-SW10) (a.k.a. "Background Push"), set this value to 1 to trigger a background download. You also have to set `push_type` starting iOS 13 and watchOS 6. +* **`push_type`**: _(iOS only)_ The type of the notification. The value is `alert` or `background`. Specify `alert` when the delivery of your notification displays an alert, plays a sound, or badges your app's icon. Specify `background` for silent notifications that do not interact with the user. Defaults to `alert` if no value is set. Required when delivering notifications to devices running iOS 13 and later, or watchOS 6 and later. +* **`priority`**: _(iOS only)_ The priority of the notification. Specify 10 to send the notification immediately. Specify 5 to send the notification based on power considerations on the user’s device. ([More detailed documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns)) * **`category`**: _(iOS only)_ the identifier of the [`UNNotification​Category`](https://developer.apple.com/reference/usernotifications/unnotificationcategory) for this push notification. * **`uri`**: _(Android only)_ an optional field that contains a URI. When the notification is opened, an `Activity` associated with opening the URI is launched. * **`title`**: _(Android only)_ the value displayed in the Android system tray notification. @@ -187,7 +189,7 @@ Parse.Push.send({ }); ``` -It is also possible to specify your own data in this dictionary. As explained in the Receiving Notifications section for [iOS]({{ site.baseUrl }}/ios/guide/#scheduling-pushes) and [Android]({{ site.baseUrl }}/android/guide/#scheduling-pushes), iOS will give you access to this data only when the user opens your app via the notification and Android will provide you this data in the `Intent` if one is specified. +It is also possible to specify your own data in this dictionary. As explained in the Receiving Notifications section for [iOS]({{ site.baseUrl }}/ios/guide/#receiving-pushes) and [Android]({{ site.baseUrl }}/android/guide/#receiving-pushes), iOS will give you access to this data only when the user opens your app via the notification and Android will provide you this data in the `Intent` if one is specified. ```javascript var query = new Parse.Query(Parse.Installation); diff --git a/_includes/js/queries.md b/_includes/js/queries.md index 2d8da30f7..56ea5c325 100644 --- a/_includes/js/queries.md +++ b/_includes/js/queries.md @@ -18,7 +18,7 @@ alert("Successfully retrieved " + results.length + " scores."); for (let i = 0; i < results.length; i++) { var object = results[i]; alert(object.id + ' - ' + object.get('playerName')); -} +} ``` ## Query Constraints @@ -57,6 +57,28 @@ You can skip the first results by setting `skip`. In the old Parse hosted backen query.skip(10); // skip the first 10 results ``` +If you want to know the total number of rows in a table satisfying your query, for e.g. pagination purposes - you can use `withCount`. + +**Note:** Enabling this flag will change the structure of response, see the example below. + +Let's say you have 200 rows in a table called `GameScore`: + +```javascript +const GameScore = Parse.Object.extend("GameScore"); +const query = new Parse.Query(GameScore); + +query.limit(25); + +const results = await query.find(); // [ GameScore, GameScore, ...] + +// to include count: +query.withCount(); +const response = await query.find(); // { results: [ GameScore, ... ], count: 200 } +``` +⚠️ Сount operations can be slow and expensive. + +If you only want to get the count without objects - use [Counting Objects](#counting-objects). + For sortable types like numbers and strings, you can control the order in which results are returned: ```javascript @@ -130,6 +152,19 @@ losingUserQuery.doesNotMatchKeyInQuery("hometown", "city", teamQuery); const results = await losingUserQuery.find(); ``` +To filter rows based on `objectId`'s from pointers in a second table, you can use dot notation: + +```javascript +const rolesOfTypeX = new Parse.Query('Role'); +rolesOfTypeX.equalTo('type', 'x'); + +const groupsWithRoleX = new Parse.Query('Group'); +groupsWithRoleX.matchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); +groupsWithRoleX.find().then(function(results) { + // results has the list of groups with role x +}); +``` + You can restrict the fields returned by calling `select` with a list of keys. To retrieve documents that contain only the `score` and `playerName` fields (and also special built-in fields such as `objectId`, `createdAt`, and `updatedAt`): ```javascript @@ -141,6 +176,18 @@ query.find().then(function(results) { }); ``` +Similarly, use `exclude` to remove undesired fields while retrieving the rest: + +```javascript +var GameScore = Parse.Object.extend("GameScore"); +var query = new Parse.Query(GameScore); +query.exclude("playerName"); +query.find().then(function(results) { + // Now each result will have all fields except `playerName` +}); +``` + + The remaining fields can be fetched later by calling `fetch` on the returned objects: ```javascript @@ -170,10 +217,6 @@ query.containsAll("arrayKey", [2, 3, 4]); ## Queries on String Values -
- If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
- Use `startsWith` to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets: ```javascript @@ -420,7 +463,7 @@ You can group by a field. ```javascript // score is the field. $ before score lets the database know this is a field var pipeline = [ - group: { objectId: '$score' } + { group: { objectId: '$score' } } ]; var query = new Parse.Query("User"); query.aggregate(pipeline) @@ -437,7 +480,7 @@ You can apply collective calculations like $sum, $avg, $max, $min. ```javascript // total will be a newly created field to hold the sum of score field var pipeline = [ - group: { objectId: null, total: { $sum: '$score' } } + { group: { objectId: null, total: { $sum: '$score' } } } ]; var query = new Parse.Query("User"); query.aggregate(pipeline) @@ -453,7 +496,7 @@ Project pipeline is similar to `keys` or `select`, add or remove existing fields ```javascript var pipeline = [ - project: { name: 1 } + { project: { name: 1 } } ]; var query = new Parse.Query("User"); query.aggregate(pipeline) @@ -485,7 +528,7 @@ You can match by comparison. ```javascript var pipeline = [ - match: { score: { $gt: 15 } } + { match: { score: { $gt: 15 } } } ]; var query = new Parse.Query("User"); query.aggregate(pipeline) @@ -528,3 +571,15 @@ query.distinct("age") // There was an error. }); ``` + +## Read Preference + +When using a MongoDB replica set, you can use the `query.readPreference(readPreference, includeReadPreference, subqueryReadPreference)` function to choose from which replica the objects will be retrieved. The `includeReadPreference` argument chooses from which replica the included pointers will be retrieved and the `subqueryReadPreference` argument chooses in which replica the subqueries will run. The possible values are `PRIMARY` (default), `PRIMARY_PREFERRED`, `SECONDARY`, `SECONDARY_PREFERRED`, or `NEAREST`. If the `includeReadPreference` argument is not passed, the same replica chosen for `readPreference` will be also used for the includes. The same rule applies for the `subqueryReadPreference` argument. + +```javascript +query.readPreference( + 'SECONDARY', + 'SECONDARY_PREFERRED', + 'NEAREST' +); +``` diff --git a/_includes/js/users.md b/_includes/js/users.md index 9ee50a222..09545e9c3 100644 --- a/_includes/js/users.md +++ b/_includes/js/users.md @@ -251,45 +251,45 @@ Using our Facebook integration, you can associate an authenticated Facebook user To start using Facebook with Parse, you need to: -1. [Set up a Facebook app](https://developers.facebook.com/apps), if you haven't already. Choose the "Website with Facebook Login" option under "Select how your app integrates with Facebook" and enter your site's URL. -2. Add your application's Facebook Application ID on your Parse application's settings page. -3. Follow [these instructions](https://developers.facebook.com/docs/javascript/quickstart/) for loading the Facebook JavaScript SDK into your application. -4. Replace your call to `FB.init()` with a call to `Parse.FacebookUtils.init()`. For example, if you load the Facebook JavaScript SDK asynchronously, your `fbAsyncInit` function will look like this: +1. [Create a Facebook Developer account](https://developers.facebook.com/). +2. [Create an app](https://developers.facebook.com/apps). +3. In your app Dashboard, add a product -> Facebook Login. +4. [Add appIds to Parse Server auth configuration](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication) or pass `facebookAppIds` into configuration -```javascript -<script> +```html + ``` The function assigned to `fbAsyncInit` is run as soon as the Facebook JavaScript SDK has completed loading. Any code that you want to run after the Facebook JavaScript SDK is loaded should be placed within this function and after the call to `Parse.FacebookUtils.init()`. If you encounter any issues that are Facebook-related, a good resource is the [official getting started guide from Facebook](https://developers.facebook.com/docs/reference/javascript/). -If you encounter issues that look like they're being returned from Parse's servers, try removing your Facebook application's App Secret from your app's settings page. - -There are two main ways to use Facebook with your Parse users: (1) logging in as a Facebook user and creating a `Parse.User`, or (2) linking Facebook to an existing `Parse.User`. +There are two main ways to use Facebook with your Parse users: (1) [logging in as a Facebook user](#login--signup) and creating a `Parse.User`, or (2) [linking Facebook](#linking) to an existing `Parse.User`. ### Login & Signup @@ -297,7 +297,7 @@ There are two main ways to use Facebook with your Parse users: (1) logging in as ```javascript try { - const users = await = Parse.FacebookUtils.logIn(); + const users = await Parse.FacebookUtils.logIn(); if (!user.existed()) { alert("User signed up and logged in through Facebook!"); } else { @@ -313,15 +313,13 @@ When this code is run, the following happens: 1. The user is shown the Facebook login dialog. 2. The user authenticates via Facebook, and your app receives a callback. 3. Our SDK receives the Facebook data and saves it to a `Parse.User`. If it's a new user based on the Facebook ID, then that user is created. -4. Your `success` callback is called with the user. -You may optionally provide a comma-delimited string that specifies what [permissions](https://developers.facebook.com/docs/authentication/permissions/) your app requires from the Facebook user. For example: +You may optionally provide a comma-delimited string that specifies what [permissions](https://developers.facebook.com/docs/authentication/permissions/) your app requires from the Facebook user. For example: ```javascript const user = await Parse.FacebookUtils.logIn("user_likes,email"); ``` -`Parse.User` integration doesn't require any permissions to work out of the box (ie. `null` or specifying no permissions is perfectly acceptable). [Read more about permissions on Facebook's developer guide.](https://developers.facebook.com/docs/reference/api/permissions/)
It is up to you to record any data that you need from the Facebook user after they authenticate. To accomplish this, you'll need to do a graph query using the Facebook SDK. @@ -345,6 +343,8 @@ if (!Parse.FacebookUtils.isLinked(user)) { The steps that happen when linking are very similar to log in. The difference is that on successful login, the existing `Parse.User` is updated with the Facebook information. Future logins via Facebook will now log the user into their existing account. +For advanced API: If you have a Facebook `access_token`, you can use [linkWith()](#linking-1). + If you want to unlink Facebook from a user, simply do this: ```javascript @@ -362,68 +362,22 @@ Our library manages the `FB` object for you. The `FB` singleton is synchronized ## Linking Users -Parse allows you to link your users with services like Twitter and Facebook, enabling your users to sign up or log into your application using their existing identities. This is accomplished through \_linkWith() method by providing authentication data for the service you wish to link to a user in the `authData` field. Once your user is associated with a service, the `authData` for the service will be stored with the user and is retrievable by logging in. - -`authData` is a JSON object with keys for each linked service containing the data below. In each case, you are responsible for completing the authentication flow (e.g. OAuth 1.0a using hellojs on client side) to obtain the information the the service requires for linking. - -### Facebook `authData` - -```javascript -{ - "facebook": { - "id": "user's Facebook id number as a string", - "access_token": "an authorized Facebook access token for the user", - "expiration_date": "token expiration date of the format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - } -} -``` -Learn more about [Facebook login](https://developers.facebook.com/docs/authentication/). +Parse allows you to link your users with [3rd party authentication ]({{ site.baseUrl }}/parse-server/guide/#oauth-and-3rd-party-authentication), enabling your users to sign up or log into your application using their existing identities. This is accomplished through `_linkWith` method by providing authentication data for the service you wish to link to a user in the `authData` field. Once your user is associated with a service, the `authData` for the service will be stored with the user and is retrievable by logging in. -### Twitter `authData` - -```javascript -{ - "twitter": { - "id": "user's Twitter id number as a string", - "screen_name": "user's Twitter screen name", - "consumer_key": "your application's consumer key", - "consumer_secret": "your application's consumer secret", - "auth_token": "an authorized Twitter token for the user with your application", - "auth_token_secret": "the secret associated with the auth_token" - } -} -``` - -Learn more about [Twitter login](https://dev.twitter.com/docs/auth/implementing-sign-twitter). - -### Anonymous user `authData` - -```javascript -{ - "anonymous": { - "id": "random UUID with lowercase hexadecimal digits" - } -} -``` +`authData` is a JSON object with keys for each linked service containing the data below. ### Signing Up and Logging In -Signing a user up with a linked service and logging them in with that service uses the same \_linkWith() mothod, in which the `authData` for the user is specified. For example, to sign up or log in with a user's Twitter account: +Signing a user up with a linked service and logging them in with that service uses the same `_linkWith()` method, in which the `authData` for the user is specified. ```javascript -let myAuthData = { - "id": "12345678", - "screen_name": "ParseIt", - "consumer_key": "SaMpLeId3X7eLjjLgWEw", - "consumer_secret": "SaMpLew55QbMR0vTdtOACfPXa5UdO2THX1JrxZ9s3c", - "auth_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "auth_token_secret": "SaMpLeEb13SpRzQ4DAIzutEkCE2LBIm2ZQDsP3WUU" -} -let user = new Parse.User(); -user._linkWith('twitter', { authData: myAuthData }).then(function(user){ - // user -}); +const myAuthData = { + id: '12345678' +}; +const user = new Parse.User(); +await user._linkWith('providerName', { authData: myAuthData }); ``` + Parse then verifies that the provided `authData` is valid and checks to see if a user is already associated with this data. If so, it returns a status code of `200 OK` and the details (including a `sessionToken` for the user): ```javascript @@ -441,13 +395,8 @@ With a response body like: "objectId": "uMz0YZeAqc", "sessionToken": "r:samplei3l83eerhnln0ecxgy5", "authData": { - "twitter": { + "providerName": { "id": "12345678", - "screen_name": "ParseIt", - "consumer_key": "SaMpLeId3X7eLjjLgWEw", - "consumer_secret": "SaMpLew55QbMR0vTdtOACfPXa5UdO2THX1JrxZ9s3c", - "auth_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "auth_token_secret": "SaMpLeEb13SpRzQ4DAIzutEkCE2LBIm2ZQDsP3WUU" } } } @@ -469,32 +418,103 @@ The body of the response will contain the `objectId`, `createdAt`, `sessionToken } ``` -### Linking - -Linking an existing user with a service like Facebook or Twitter uses the same method \_linkWith() to associate `authData` with the user. For example, linking a user with a Facebook account would use a request like this: +#### Linking un-authenticated users +To create a link to an un-authenticated user (for example in cloud code), options can be passed to `_linkWith()` to either use the `masterKey` or pass a `sessionToken`. ```javascript -let myAuthData = { - id: "123456789", - "access_token": "SaMpLeAAibS7Q55FSzcERWIEmzn6rosftAr7pmDME10008bWgyZAmv7mziwfacNOhWkgxDaBf8a2a2FCc9Hbk9wAsqLYZBLR995wxBvSGNoTrEaL", - "expiration_date": "2012-02-28T23:49:36.353Z" +const myAuthData = { + id: xzx5tt123, + access: token } -let user = new Parse.User(); -user.id = "uMz0YZeAqc"; -user._linkWith("facebook", { authData: myAuthData }).then(function(user){ - // user is linked now -}); + +const user = await Parse.Query(Parse.User).get(userId); + +await user._linkWith( + 'providerName', + { authData: myAuthData }, + { useMasterKey: true } +); ``` -After linking your user to a service, you can authenticate them using matching `authData`. -### Unlinking +On rest, web, mobile, or TV clients, users can then login using the `CustomAdapter` by passing `myAuthData`: + +```javascript +const loggedIn = await Parse.User.logInWith('CustomAdapter', { authData: myAuthData}); + +``` + +### Custom Authentication Module + +Parse Server supports many [3rd Party Authenications]({{ site.baseUrl }}/parse-server/guide/#oauth-and-3rd-party-authentication). +It is possible to `linkWith` any 3rd Party Authentication by creating a custom authentication module. -Unlinking an existing user with a service also uses \_linkWith() method to clear `authData` from the user by setting the `authData` for the service to `null`. For example, unlinking a user with a Facebook account would use a request like this: +[Read more about Auth Provider Documentation](https://github.com/parse-community/Parse-SDK-JS/blob/master/src/interfaces/AuthProvider.js) +Note: This is an example, you can handle your own authentication (if you don't have authData), restoreAuthentication and deauthenticate methods. + +A minimal `CustomAuth.js` module: ```javascript -let user = new Parse.User(); -user.id = "uMz0YZeAqc"; -user._linkWith("facebook", {}).then(function(user){ - // user is unlinked now +function validateAuthData(authData, options) { + return Promise.resolve({}) +} + +function validateAppId(appIds, authData, options) { + return Promise.resolve({}); +} + +module.exports = { + validateAppId, + validateAuthData, +}; +``` + +Configure the server to make the `CustomAuth` available: +```javascript +const CustomAuth = require('./CustomAuth'); + +const api = new ParseServer({ + ... + auth: { + myAuth: { + module: CustomAuth, + option1: 'hello', + option2: 'world', + } + } + ... }); +... +app.use('/parse', api); +``` + +Use the `CustomAuth`: +```javascript +const provider = { + authenticate: () => Promise.resolve(), + restoreAuthentication() { + return true; + }, + + getAuthType() { + return 'myAuth'; + }, + + getAuthData() { + return { + authData: { + id: 1234, + }, + }; + }, +}; +// Must register before linking +Parse.User._registerAuthenticationProvider(provider); +const user = new Parse.User(); +user.setUsername('Alice'); +user.setPassword('sekrit'); +await user.signUp(); +await user._linkWith(provider.getAuthType(), provider.getAuthData()); +user._isLinked(provider); // true +// Unlink +await user._unlinkFrom(provider.getAuthType()); ``` diff --git a/_includes/menu.html b/_includes/menu.html index a701acbad..8d1a497ae 100644 --- a/_includes/menu.html +++ b/_includes/menu.html @@ -11,12 +11,7 @@

Stay Connected

  • - - - -
  • -
  • - +
  • @@ -26,7 +21,7 @@

    Stay Connected

  • - +
  • diff --git a/_includes/parse-server/deploying-glitch-mlab.md b/_includes/parse-server/deploying-glitch-mlab.md new file mode 100644 index 000000000..d40930d57 --- /dev/null +++ b/_includes/parse-server/deploying-glitch-mlab.md @@ -0,0 +1,65 @@ +## Deploying to Glitch and mLab + +Before you start, you'll need: + + - mLab account (for free MongoDB) + +### Step 1: Creating your database on mLab + +[mLab](https://mlab.com) provides a Database-as-a-Service for MongoDB. They include a free tier for small sandbox databases. Create an account on mLab and then use the Single-node, Sandbox plan to get a (free) database up and running. Within the mLab wizard, you'll need to be sure to create a user that has access to connect to the new database. Upon completion, you should be able to construct a Mongo DB connection string like the following: + +``` +mongodb://yourusername:yourpassword@yourmlabdatabaseaddress.mlab.com:yourdatabaseport/yourdatabasename +``` + +### Step 2: Running parse-server-example on Glitch + +[Glitch](https://glitch.com) provides an easy way to instantly create and deploy Node.js applications for free. We will use it to run the [parse-server-example](https://github.com/parse-community/parse-server-example) application. + +To get the example server up and running for quick testing, you can simply click the button below: + +[![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/import/github/parse-community/parse-server-example?APP_ID=myAppId&MASTER_KEY=your_master_key_here&DATABASE_URI=your_mlab_database_uri_here&SERVER_URL=https://project-name.glitch.me/parse&PARSE_SERVER_LOGS=/tmp) + +Now that the import is complete, we'll need to make two small changes to the ```🗝️.env``` file, which stores private environment variables. + +It should look like the following: + +``` +# Environment Config + +# store your secrets and config variables in here +# only invited collaborators will be able to see your .env values + +# reference these in your code with process.env.SECRET + +SECRET= +MADE_WITH= + +# note: .env is a shell file so there can't be spaces around = +APP_ID=myAppId +MASTER_KEY=your_master_key_here +DATABASE_URI=your_mlab_database_uri_here +SERVER_URL=https://project-name.glitch.me/parse +PARSE_SERVER_LOGS=/tmp + +``` +First, change the ```DATABASE_URI``` value to your mLab connection string from step 1. + +Next, change the ```project-name``` portion of the ```SERVER_URL``` value to the name of the project that was created. So, if clicking the button creates ```electric-dinner.glitch.me```, your ```SERVER_URL``` value would be ```https://electric-dinner.glitch.me/parse```. + +You can delete the ```SECRET``` and ```MADE_WITH``` lines, but there's no harm in leaving them there. + +It is important, for this tutorial, to leave the ```APP_ID``` as ```myAppId``` as the "test" page hard-codes that and expects that value. + +If you'd like to keep this project, [create an account on Glitch](https://glitch.com/help/how-do-i-create-an-account-on-glitch/). Projects created as an anonymous user expire after five days. You can read more about the technical restrictions on free Glitch projects [here](https://glitch.com/help/restrictions/). + + +### Step 3: Testing + +Once you're finished making your changes to your ```🗝️.env``` file, Glitch will automatically build and deploy your application. If you use the Logs feature within Glitch (click on ToolsLogs), you should see this when your app is deployed: + +``` +parse-server-example running on port 3000. +``` + +You should then be able to use the "Show" button to launch the application in the browser and get to a page that urges you to star the parse-server GitHub repository. To access the test harness page, add a trailing ```/test``` to your URL. This should take you to a page that will allow you to exercise a few parts of the Parse Server Javascript SDK and create a dummy collection and record in your MongoDB. If you're able to complete steps one through three on this test page, Parse Server is up and running. Optionally, you can go back to mLab.com and take a look at the data that was stored by the test harness to get a feel for how Parse Server stores data in MongoDB. diff --git a/_includes/parse-server/deploying-hyperdev-mlab.md b/_includes/parse-server/deploying-hyperdev-mlab.md deleted file mode 100644 index c4fb8536b..000000000 --- a/_includes/parse-server/deploying-hyperdev-mlab.md +++ /dev/null @@ -1,62 +0,0 @@ -## Deploying to HyperDev and mLab - -Before you start, you'll need: - - - GitHub account (for HyperDev import feature) - - mLab account (for free MongoDB) - -### Step 1: Creating your database on mLab - -[mLab](https://mlab.com) provides a Database-as-a-Service for MongoDB. They include a free tier for small sandbox databases. Create an account on mLab and then use the Single-node, Sandbox plan to get a (free) database up and running. Within the mLab wizard, you'll need to be sure to create a user that has access to connect to the new database. Upon completion, you should be able to construct a Mongo DB connection string like the following: - -``` -mongodb://yourusername:yourpassword@yourmlabdatabaseaddress.mlab.com:yourdatabaseport/yourdatabasename -``` - -### Step 2: Running parse-server-example on HyperDev - -[HyperDev](https://hyperdev.com) provides a great way to instantly create and deploy Node.js applications for free. We will use it to run the [parse-server-example](https://github.com/parse-community/parse-server-example) application. - -Access hyperdev.com and you'll be given a sandbox app with a random name (usually of the form ```noun-noun```). Sign in to HyperDev using your GitHub account. Then, use the "Advanced Options" menu to import code from GitHub. When prompted for the repository to import, enter - -``` -parse-community/parse-server-example -``` - -Now that the import is complete, we'll need to configure the application to point to our mLab database. Modify the ```.env``` file, which stores private environment variables. - -Use the following template: - -``` -# Environment Config - -# store your secrets and config variables in here -# only invited collaborators will be able to see your .env values - -# reference these in your code with process.env.SECRET - -APP_ID=myAppId -MASTER_KEY=your_master_key_here -DATABASE_URI=mongodb://yourusername:yourpassword@yourmlabdatabaseaddress.mlab.com:yourdatabaseport/yourdatabasename - -#your SERVER_URL will depend on whatever URL hyperdev/glitch gives you -SERVER_URL=https://noun-noun.glitch.me/parse - -#hyperdev will only let you log to this folder -PARSE_SERVER_LOGS_FOLDER=/tmp - -# note: .env is a shell file so there can't be spaces around '= -``` - -It is important, for this tutorial, to leave the ```APP_ID``` as ```myAppId``` as the "test" page hard-codes that and expects that value. - -### Step 3: Testing - -Once you're finished making your changes to your .env file, HyperDev will automatically build and deploy your application. If you use the Logs feature within HyperDev (there's a Logs button), you should see this when your app is deployed: - -``` -Launching node index.js -parse-server-example running on port 3000. -``` - -You should then be able to use the "Show" button to launch the application in the browser and get to a page that urges you to star the parse-server GitHub repository. To access the test harness page, add a trailing ```/test``` to your URL. This should take you to a page that will allow you to exercise a few parts of the Parse Server Javascript SDK and create a dummy collection and record in your MongoDB. If you're able to completes steps one through three on this test page, Parse Server is up and running. Optionally, you can go back to mLab.com and take a look at the data that was stored by the test harness to get a feel for how Parse Server stores data in MongoDB. diff --git a/_includes/parse-server/deploying.md b/_includes/parse-server/deploying.md index d2832730d..df51dda83 100644 --- a/_includes/parse-server/deploying.md +++ b/_includes/parse-server/deploying.md @@ -4,4 +4,4 @@ The fastest and easiest way to start using Parse Server is to run MongoDB and Pa {% include_relative _includes/parse-server/deploying-heroku-mlab.md %} -{% include_relative _includes/parse-server/deploying-hyperdev-mlab.md %} +{% include_relative _includes/parse-server/deploying-glitch-mlab.md %} diff --git a/_includes/parse-server/file-adapters.md b/_includes/parse-server/file-adapters.md index 9a1cbf455..37d35ec91 100644 --- a/_includes/parse-server/file-adapters.md +++ b/_includes/parse-server/file-adapters.md @@ -105,9 +105,12 @@ new S3Adapter(accessKey, secretKey, bucket, options) | secretKey | The AWS secret key for the user | Required. | | bucket | The name of your S3 bucket. | Required. | | options | JavaScript object (map) that can contain: | | -| region | Key in `options`. Set the AWS region to connect to. | Optional. Default: 'us-east-1' | -| bucketPrefix | Key in `options`. Set to create all the files with the specified prefix added to the filename. Can be used to put all the files for an app in a folder with 'folder/'. | Optional. Default: '' | -| directAccess | Key in `options`. Controls whether reads are going directly to S3 or proxied through your Parse Server. If set to true, files will be made publicly accessible, and reads will not be proxied. | Optional. Default: false | +| region | Key in `options`. Set the AWS region to connect to. | Optional. Default: `us-east-1` | +| bucketPrefix | Key in `options`. Set to create all the files with the specified prefix added to the filename. Can be used to put all the files for an app in a folder with 'folder/'. | Optional. Default: `null` | +| directAccess | Key in `options`. Controls whether reads are going directly to S3 or proxied through your Parse Server. If set to true, files will be made publicly accessible, and reads will not be proxied. | Optional. Default: `false` | +| baseUrl | Key in `options`. The base URL the file adapter uses to determine the file location for direct access. | Optional. Default: `null`. To be used when `directAccess=true`. When set the file adapter returns a file URL in format `baseUrl/bucketPrefix` + `filename`. Example for `baseUrl='http://domain.com/folder'` and `bucketPrefix='prefix_'` the returned file location is `http://domain.com/folder/prefix_file.txt`. | +| baseUrlDirect | Key in `options`. Is `true` if the file adapter should ignore the bucket prefix when determining the file location for direct access. | Optional. Default: `false`. To be used when `directAccess=true` and `baseUrl` is set. When set to `true`, the file adapter returns a file URL in format `baseUrl/filename`. Example for `baseUrl='http://domain.com/folder'` and `baseUrlDirect=true` the returned file location is `http://domain.com/folder/file.txt`. | +| globalCacheControl | Key in `options`. The `Cache-Control` http header to set in the file request. | Optional. Default: `null`. Example: `public, max-age=86400` for 24 hrs caching. More info [here](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1). | ## Configuring `GCSAdapter` diff --git a/_includes/parse-server/keys.md b/_includes/parse-server/keys.md index 03c039d88..a9a8bb12c 100644 --- a/_includes/parse-server/keys.md +++ b/_includes/parse-server/keys.md @@ -8,4 +8,4 @@ However, you have the option to specify any of these four keys upon initializati Starting `parse-server` 2.6.5, it is possible to specify a `readOnlyMasterKey`. When using this key instead of the masterKey, the server will perform all `read` operations as if they were executing with the `masterKey` but will fail to execute any `write` operation. -This key is expecially powerful when used with `parse-dashboard`. Please refer to [Parse Dashboard](https://github.com/parse-community/parse-dashboard/)'s documentation for more information. +This key is especially powerful when used with `parse-dashboard`. Please refer to [Parse Dashboard](https://github.com/parse-community/parse-dashboard/)'s documentation for more information. diff --git a/_includes/parse-server/live-query.md b/_includes/parse-server/live-query.md index 31fdf8e26..7786b4240 100644 --- a/_includes/parse-server/live-query.md +++ b/_includes/parse-server/live-query.md @@ -30,7 +30,7 @@ httpServer.listen(port); var parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer); ``` -The `ws` protocol URL of the LiveQuery server is the hostname and port which the `httpServer` is listening to. For example, if the `httpSever` is listening to `localhost:8080`, the `ws` protocol of the LiveQuery server is `ws://localhost:8080/`. We will allow you to customize the path of `ws` protocol URL of the LiveQuery server later, currently it is fixed and you can not set path. +The `ws` protocol URL of the LiveQuery server is the hostname and port which the `httpServer` is listening to. For example, if the `httpServer` is listening to `localhost:8080`, the `ws` protocol of the LiveQuery server is `ws://localhost:8080/`. We will allow you to customize the path of `ws` protocol URL of the LiveQuery server later, currently it is fixed and you can not set path. ## Client Setup diff --git a/_includes/parse-server/push-notifications.md b/_includes/parse-server/push-notifications.md index acec7fd9a..93aa34f03 100644 --- a/_includes/parse-server/push-notifications.md +++ b/_includes/parse-server/push-notifications.md @@ -16,10 +16,12 @@ However, there are a few caveats: * Scheduled push is not supported ## API -We support most of the sending options similar to the hosted Parse.com service. Check the detailed doc [here]({{ site.baseUrl }}/rest/guide/#sending-options). Parse Server supports the following: +We support most of the sending options. Check the detailed doc [here]({{ site.baseUrl }}/rest/guide/#sending-options). Parse Server supports the following: * `channels` to target installations by channels * `where` to target installations by `ParseQuery` +* `priority` under `data` for iOS push priority +* `push_type` under `data` for iOS push type * `alert` under `data` for notification message * number `badge` under `data` for iOS badge number * `sound` under `data` for iOS sound @@ -282,7 +284,9 @@ If you're interested in any of these features, [don't hesitate to jump in and se ### Silent Notifications -If you have migrated from Parse.com Push and you are seeing situations where silent notifications are failing to deliver, please ensure that your payload is setting the `content-available` attribute to Int(1) and not "1". This value will be explicitly checked. +If you are seeing situations where silent notifications are failing to deliver, please ensure that your payload is setting the `content-available` attribute to Int(1) (or just 1 as in javascript) and not "1". This value will be explicitly checked. + +When sending a push notification to APNs you also have to set `push_type` to `background` for delivering silent notifications to devices running iOS 13 and later, or watchOS 6 or later. ### PPNS diff --git a/_includes/parse-server/third-party-auth.md b/_includes/parse-server/third-party-auth.md index 702018d93..b3b3a0ab0 100644 --- a/_includes/parse-server/third-party-auth.md +++ b/_includes/parse-server/third-party-auth.md @@ -2,6 +2,7 @@ Parse Server supports 3rd party authentication with +* Apple * Facebook * Facebook AccountKit * Github @@ -11,6 +12,7 @@ Parse Server supports 3rd party authentication with * Janrain Engage * LinkedIn * Meetup +* PhantAuth * QQ * Spotify * Twitter @@ -87,7 +89,6 @@ Two ways to [retrieve access token](https://developers.facebook.com/docs/account { "twitter": { "id": "user's Twitter id number as a string", - "screen_name": "user's Twitter screen name", "consumer_key": "your application's consumer key", "consumer_secret": "your application's consumer secret", "auth_token": "an authorized Twitter token for the user with your application", @@ -96,7 +97,19 @@ Two ways to [retrieve access token](https://developers.facebook.com/docs/account } ``` -Learn more about [Twitter login](https://dev.twitter.com/docs/auth/implementing-sign-twitter). +The options passed to Parse server: +```js +{ + auth: { + twitter: { + consumer_key: "", // REQUIRED + consumer_secret: "" // REQUIRED + }, + } +} +``` + +Learn more about [Twitter login](https://developer.twitter.com/en/docs/twitter-for-websites/log-in-with-twitter/guides/implementing-sign-in-with-twitter). ### Anonymous user `authData` @@ -108,6 +121,36 @@ Learn more about [Twitter login](https://dev.twitter.com/docs/auth/implementing- } ``` +### Apple `authData` + +As of Parse Server 3.5.0 you can use [Sign In With Apple](https://developer.apple.com/sign-in-with-apple/get-started/). + +```js +{ + "apple": { + "id": "user", + "token": "the identity token for the user" + } +} +``` + +Using Apple Sign In on a iOS device will give you a `ASAuthorizationAppleIDCredential.user` string for the user identifier, which can be match the `sub` component of the JWT identity token. +Using Apple Sign In through the Apple JS SDK or through the REST service will only give you the JWT identity token (`id_token`) which you'll have to decompose to obtain the user identifier in its `sub` component. As an example you could use something like `JSON.parse(atob(token.split(".")[1])).sub`. + +#### Configuring parse-server for Sign In with Apple + +```js +{ + auth: { + apple: { + client_id: "", // optional (for extra validation), use the Service ID from Apple. + }, + } +} +``` + +Learn more about [Sign In With Apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple). + ### Github `authData` ```js @@ -167,6 +210,21 @@ Google oauth supports validation of id_token's and access_token's. } ``` +### PhantAuth `authData` + +As of Parse Server 3.7.0 you can use [PhantAuth](https://www.phantauth.net/). + +```js +{ + "phantauth": { + "id": "user's PhantAuth sub (string)", + "access_token": "an authorized PhantAuth access token for the user", + } +} +``` + +Learn more about [PhantAuth](https://www.phantauth.net/). + ### QQ `authData` ```js diff --git a/_includes/parse-server/using-parse-sdks.md b/_includes/parse-server/using-parse-sdks.md index b3ed4103f..e34e40859 100644 --- a/_includes/parse-server/using-parse-sdks.md +++ b/_includes/parse-server/using-parse-sdks.md @@ -12,7 +12,7 @@ let configuration = ParseClientConfiguration { $0.clientKey = "" $0.server = "http://localhost:1337/parse" } -Parse.initializeWithConfiguration(configuration) +Parse.initialize(with: configuration) ``` _Objective-C_ diff --git a/_includes/php/files.md b/_includes/php/files.md index b1b9fd416..ad0a08483 100644 --- a/_includes/php/files.md +++ b/_includes/php/files.md @@ -2,7 +2,7 @@ ## Creating a ParseFile -`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes). +`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data. Getting started with `ParseFile` is easy. There are a couple of ways to create a file. The first is to provide the contents of the file. diff --git a/_includes/php/objects.md b/_includes/php/objects.md index 903636f78..53835bf40 100644 --- a/_includes/php/objects.md +++ b/_includes/php/objects.md @@ -83,6 +83,14 @@ If you need to refresh an object you already have with the latest data that $gameScore->fetch(); ``` +If you need to check if an object has been fetched, you can call the `isDataAvailable()` method: + +```php +if (!$gameScore->isDataAvailable()) { + $gameScore->fetch(); +} +``` + ## Updating Objects Updating an object is simple. Just set some new data on it and call the save method. For example: diff --git a/_includes/php/push-notifications.md b/_includes/php/push-notifications.md index 1593e921f..8689af602 100644 --- a/_includes/php/push-notifications.md +++ b/_includes/php/push-notifications.md @@ -31,7 +31,7 @@ This class has several special fields that help you manage and target devices. There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting]({{ site.baseUrl }}/php/guide/#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section. -You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the web console as long as no sends have happened yet. After you send the push, the web console shows push analytics graphs. +You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the web console as long as no sends have happened yet. After you send the push, the web console shows push analytics graphs. ### Using Channels @@ -288,27 +288,27 @@ if(ParsePush::hasStatus($response)) { // Retrieve PushStatus object $pushStatus = ParsePush::getStatus($response); - + // get push status string $status = $pushStatus->getPushStatus(); - + if($status == "succeeded") { // handle a successful push request - + } else if($status == "running") { // handle a running push request - + } else { // push request did not succeed - + } - + // get # pushes sent $sent = $pushStatus->getPushesSent(); - + // get # pushes failed $failed = $pushStatus->getPushesFailed(); - + } ``` diff --git a/_includes/php/queries.md b/_includes/php/queries.md index 5a80b961b..c7a7fc22d 100644 --- a/_includes/php/queries.md +++ b/_includes/php/queries.md @@ -63,6 +63,32 @@ You can skip the first results by setting `skip`. In the old Parse hosted backen $query->skip(10); // skip the first 10 results ``` +### With Count + +If you want to know the total number of rows in a table satisfying your query, for e.g. pagination purposes - you can use `withCount`. + +**Note:** Using this feature will change the structure of response, see the example below. + +Let's say you have 200 rows in a table called `GameScore`: + +```php +$query = new ParseQuery('GameScore'); +$query->withCount(); +$query->limit(25); + +$response = $query->find(); +$response['count'] // Returns 200 the total number of objects dispite limit / skip +$response['results'] // Returns 25 objects + +// As of PHP 7.1 you can use Array Destructuring +['count' => $count, 'results' => $results] = $query->find(); + +// Use $count and $results +``` +⚠️ Сount operations can be slow and expensive. + +If you only want to get the count without objects - use [Counting Objects](#counting-objects). + ### Ascending and Descending For sortable types like numbers and strings, you can control the order in which results are returned: @@ -161,6 +187,18 @@ $results = $losingUserQuery->find(); ### Select +To filter rows based on `objectId`'s from pointers in a second table, you can use dot notation: + +```javascript +$rolesOfTypeX = new ParseQuery('Role'); +$rolesOfTypeX->equalTo('type', 'x'); + +$groupsWithRoleX = new ParseQuery('Group'); +$groupsWithRoleX->matchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); +$results = $groupsWithRoleX->find(); +// results has the list of groups with role x +``` + You can restrict the fields returned by calling `select` with an array of keys. To retrieve documents that contain only the `score` and `playerName` fields (and also special built-in fields such as `objectId`, `createdAt`, and `updatedAt`): ```php @@ -270,10 +308,6 @@ $query->containsAll("arrayKey", [2, 3, 4]); ## Queries on String Values -
    - If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
    - Use `startsWith` to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets: ```php @@ -447,3 +481,15 @@ $monster = Monster::spawn(200); echo monster->strength(); // Displays 200. echo monster->hasSuperHumanStrength(); // Displays true. ``` + +## Read Preference + +When using a MongoDB replica set, you can use the `$query->readPreference(readPreference, includeReadPreference, subqueryReadPreference)` function to choose from which replica the objects will be retrieved. The `includeReadPreference` argument chooses from which replica the included pointers will be retrieved and the `subqueryReadPreference` argument chooses in which replica the subqueries will run. The possible values are `PRIMARY` (default), `PRIMARY_PREFERRED`, `SECONDARY`, `SECONDARY_PREFERRED`, or `NEAREST`. If the `includeReadPreference` argument is not passed, the same replica chosen for `readPreference` will be also used for the includes. The same rule applies for the `subqueryReadPreference` argument. + +```javascript +$query->readPreference( + "SECONDARY", + "SECONDARY_PREFERRED", + "NEAREST" +); +``` diff --git a/_includes/php/setup.md b/_includes/php/setup.md index 08a9a6976..4d7cbe667 100644 --- a/_includes/php/setup.md +++ b/_includes/php/setup.md @@ -101,7 +101,7 @@ If you wish to build one yourself make sure your http client implements ```Parse ## Alternate CA File -It is possible that your local setup may not be able to verify with peers over SSL/TLS. This may especially be the case if you do not have control over your local installation, such as for shared hosting. +It is possible that your local setup may not be able to verify with peers over [SSL/TLS](https://hosting.review/web-hosting-glossary/#12). This may especially be the case if you do not have control over your local installation, such as for shared hosting. If this is the case you may need to specify a Certificate Authority bundle. You can download such a bundle from http://curl.haxx.se/ca/cacert.pem to use for this purpose. This one happens to be a Mozilla CA certificate store, you don't necessarily have to use this one but it's recommended. @@ -111,4 +111,4 @@ Once you have your bundle you can set it as follows: // ** Use an Absolute path for your file! ** // holds one or more certificates to verify the peer with ParseClient::setCAFile(__DIR__ . '/certs/cacert.pem'); -``` \ No newline at end of file +``` diff --git a/_includes/rest/analytics.md b/_includes/rest/analytics.md index a51bd3f06..1158ab46b 100644 --- a/_includes/rest/analytics.md +++ b/_includes/rest/analytics.md @@ -27,7 +27,7 @@ In the example below, the `at` parameter is optional. If omitted, the current se
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: application/json" \
       -d '{
           }' \
    @@ -40,7 +40,7 @@ connection.connect()
     connection.request('POST', '/parse/events/AppOpened', json.dumps({
          }), {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    @@ -61,7 +61,7 @@ Say your app offers search functionality for apartment listings, and you want to
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: application/json" \
       -d '{
             "dimensions": {
    @@ -84,7 +84,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    @@ -98,7 +98,7 @@ Parse Analytics can even be used as a lightweight error tracker — simply invok
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: application/json" \
       -d '{
             "dimensions": {
    @@ -117,7 +117,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    diff --git a/_includes/rest/cloud-code.md b/_includes/rest/cloud-code.md
    index 5ec555ce2..6ef8b69c7 100644
    --- a/_includes/rest/cloud-code.md
    +++ b/_includes/rest/cloud-code.md
    @@ -9,7 +9,7 @@ Cloud Functions can be called using the REST API. For example, to call the Cloud
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: application/json" \
       -d '{}' \
       https://YOUR.PARSE-SERVER.HERE/parse/functions/hello
    @@ -21,7 +21,7 @@ connection.connect()
     connection.request('POST', '/parse/functions/hello', json.dumps({
          }), {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    @@ -41,7 +41,7 @@ Similarly, you can trigger a background job from the REST API. For example, to t
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
    +  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
       -H "Content-Type: application/json" \
       -d '{"plan":"paid"}' \
       https://YOUR.PARSE-SERVER.HERE/parse/jobs/userMigration
    @@ -54,7 +54,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
    -       "X-Parse-Master-Key": "${MASTER_KEY}",
    +       "X-Parse-Master-Key": "${MASTER_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    diff --git a/_includes/rest/config.md b/_includes/rest/config.md
    index 3334dd037..17bd17ade 100644
    --- a/_includes/rest/config.md
    +++ b/_includes/rest/config.md
    @@ -10,7 +10,7 @@ After that you will be able to fetch the config on the client by sending a `GET`
     
    
     curl -X GET \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       https://YOUR.PARSE-SERVER.HERE/parse/config
     
    
    @@ -19,7 +19,7 @@ connection = httplib.HTTPSConnection('YOUR
     connection.connect()
     connection.request('GET', '/parse/config', '', {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}"
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}"
          })
     result = json.loads(connection.getresponse().read())
     print result
    @@ -39,14 +39,14 @@ The response body is a JSON object containing all the configuration parameters i
     
     You can also update the config by sending a `PUT` request to config URL. Here is a simple example that will update the `Parse.Config`:
     
    -
    
     curl -X PUT \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
    +  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
       -d "{\"params\":{\"winningNumber\":43}}"
       https://YOUR.PARSE-SERVER.HERE/parse/config
     
    +
    
     var request = require('request');
     return request({
    @@ -62,7 +62,6 @@ return request({
       }
     })
     
    -
    The response body is a JSON object containing a simple boolean value in the `result` field. diff --git a/_includes/rest/files.md b/_includes/rest/files.md index 070f93eb2..1fa321e14 100644 --- a/_includes/rest/files.md +++ b/_includes/rest/files.md @@ -8,7 +8,7 @@ To upload a file to Parse, send a POST request to the files URL, postfixed with
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: text/plain" \
       -d 'Hello, World!' \
       https://YOUR.PARSE-SERVER.HERE/parse/files/hello.txt
    @@ -19,7 +19,7 @@ connection = httplib.HTTPSConnection('YOUR
     connection.connect()
     connection.request('POST', '/parse/files/hello.txt', 'Hello, World!', {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "text/plain"
          })
     result = json.loads(connection.getresponse().read())
    @@ -49,7 +49,7 @@ To upload an image, the syntax is a little bit different. Here's an example that
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: image/jpeg" \
       --data-binary '@myPicture.jpg' \
       https://YOUR.PARSE-SERVER.HERE/parse/files/pic.jpg
    @@ -60,7 +60,7 @@ connection = httplib.HTTPSConnection('YOUR
     connection.connect()
     connection.request('POST', '/parse/files/pic.jpg', open('myPicture.jpg', 'rb').read(), {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "image/jpeg"
          })
     result = json.loads(connection.getresponse().read())
    @@ -76,7 +76,7 @@ After files are uploaded, you can associate them with Parse objects:
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: application/json" \
       -d '{
             "name": "Andrew",
    @@ -101,7 +101,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    @@ -120,7 +120,7 @@ Users holding the master key are allowed to delete files using the REST API. To
     
    
     curl -X DELETE \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
    +  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
       https://YOUR.PARSE-SERVER.HERE/parse/files/...profile.png
     
    
    @@ -129,7 +129,7 @@ connection = httplib.HTTPSConnection('YOUR
     connection.connect()
     connection.request('DELETE', '/parse/files/...profile.png', '', {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-Master-Key": "${MASTER_KEY}"
    +       "X-Parse-Master-Key": "${MASTER_KEY}"
          })
     result = json.loads(connection.getresponse().read())
     print result
    diff --git a/_includes/rest/geopoints.md b/_includes/rest/geopoints.md
    index 49478b5c5..ede1baab7 100644
    --- a/_includes/rest/geopoints.md
    +++ b/_includes/rest/geopoints.md
    @@ -10,7 +10,7 @@ To associate a point with an object you will need to embed a `GeoPoint` data typ
     
    
     curl -X POST \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -H "Content-Type: application/json" \
       -d '{
             "location": {
    @@ -33,7 +33,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}",
            "Content-Type": "application/json"
          })
     result = json.loads(connection.getresponse().read())
    @@ -49,7 +49,7 @@ Now that you have a bunch of objects with spatial coordinates, it would be nice
     
    
     curl -X GET \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -G \
       --data-urlencode 'limit=10' \
       --data-urlencode 'where={
    @@ -78,7 +78,7 @@ params = urllib.urlencode({"limit":10,"where":json.dumps({
     connection.connect()
     connection.request('GET', '/parse/classes/PlaceObject?%s' % params, '', {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}"
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}"
          })
     result = json.loads(connection.getresponse().read())
     print result
    @@ -120,7 +120,7 @@ To limit the search to a maximum distance add a `$maxDistanceInMiles` (for miles
     
    
     curl -X GET \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -G \
       --data-urlencode 'where={
             "location": {
    @@ -150,7 +150,7 @@ params = urllib.urlencode({"where":json.dumps({
     connection.connect()
     connection.request('GET', '/parse/classes/PlaceObject?%s' % params, '', {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}"
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}"
          })
     result = json.loads(connection.getresponse().read())
     print result
    @@ -163,7 +163,7 @@ It's also possible to query for the set of objects that are contained within a p
     
    
     curl -X GET \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -G \
       --data-urlencode 'where={
             "location": {
    @@ -209,7 +209,7 @@ params = urllib.urlencode({"where":json.dumps({
     connection.connect()
     connection.request('GET', '/parse/classes/PizzaPlaceObject?%s' % params, '', {
            "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}"
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}"
          })
     result = json.loads(connection.getresponse().read())
     print result
    @@ -223,8 +223,8 @@ It's also possible to query for the set of objects that are contained within or
     
    
     curl -X GET \
    -  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
    +  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    +  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
       -G \
       --data-urlencode 'where={
             "location": {
    @@ -279,8 +279,8 @@ params = urllib.urlencode({"where":json.dumps({
          })})
     connection.connect()
     connection.request('GET', '/1/classes/PizzaPlaceObject?%s' % params, '', {
    -       "X-Parse-Application-Id": "${APPLICATION_ID}",
    -       "X-Parse-REST-API-Key": "${REST_API_KEY}"
    +       "X-Parse-Application-Id": "${APPLICATION_ID}",
    +       "X-Parse-REST-API-Key": "${REST_API_KEY}"
          })
     result = json.loads(connection.getresponse().read())
     print result
    diff --git a/_includes/rest/hooks.md b/_includes/rest/hooks.md
    index a68403764..6fe19762a 100644
    --- a/_includes/rest/hooks.md
    +++ b/_includes/rest/hooks.md
    @@ -51,10 +51,11 @@ Note that trigger name can only be one of `beforeSave`, `afterSave`, `beforeDele
     ## Fetch functions
     To fetch the list of all cloud functions you deployed or created, use:
     
    +
    
     curl -X GET \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
    +  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
       -H "Content-Type: application/json" \
       https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions
     
    @@ -64,14 +65,16 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/hooks/functions', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) print result
    +
    The output is a json object with one key: "results" whose value is a list of cloud functions. +
    
     {
       "results": [
    @@ -85,10 +88,11 @@ The output is a json object with one key: "results" whose value is a list of clo
     
     To fetch a single cloud function with a given name, use:
     
    +
    
     curl -X GET \
       -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
    -  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
    +  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
       -H "Content-Type: application/json" \
       https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions/sendMessage
     
    @@ -98,12 +102,13 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/hooks/functions/sendMessage', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) print result
    +
The output is a json object with one key: "results" whose value is a list of cloud functions with the given name. @@ -118,10 +123,12 @@ The output is a json object with one key: "results" whose value is a list of clo ## Fetch triggers To fetch the list of all cloud triggers you deployed or created, use: + +

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers
 
@@ -131,14 +138,16 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/hooks/triggers', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) print result
+
The output is a json object with one key: "results" whose value is a list of cloud triggers. +

 {
   "results": [
@@ -159,10 +168,12 @@ The output is a json object with one key: "results" whose value is a list of clo
 
To fetch a single cloud trigger, use: + +

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers/Scores/beforeSave
 
@@ -172,17 +183,19 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/hooks/triggers/Scores/beforeSave', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) print result
+
The path looks like `/hooks/triggers/className/triggerName` where `triggerName` can be one of `beforeSave`, `afterSave`, `beforeDelete`, `afterDelete`. The output may look like this: +

 {
   "results": [
@@ -210,12 +223,13 @@ To create a new function webhook post to 
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"functionName":"baz","url":"https://api.example.com/baz"}' \
   https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions
@@ -228,12 +242,14 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
+ The output may look like this:

@@ -244,11 +260,13 @@ It returns the function name and url of the created webhook.
 
 If you try to create a function webhook and a cloud code function with the same name already exists, upon successful creation the response json has an additional `warning` field informing about the name conflict. Note that, function webhooks takes precedence over cloud code functions.
 
-For example,
+For example:
+
+

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"functionName":"bar","url":"https://api.example.com/bar"}' \
   https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions
@@ -261,14 +279,16 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "functionName": "bar",
@@ -278,19 +298,24 @@ The output may look like this:
 
## Create trigger webhook -To create a new function webhook post to /parse/hooks/triggers with payload in the format +To create a new function webhook post to /parse/hooks/triggers with payload in the format: + +

 {"className": x, "triggerName": y, "url": z}
 

 {"className": x, "triggerName": y, "url": z}
 
+
-Post example, +Post example: + +

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"className": "Game", "triggerName": "beforeSave", "url": "https://api.example.com/Game/beforeSave"}' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers
@@ -303,14 +328,16 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "className": "Game",
@@ -323,11 +350,13 @@ It returns the class name, trigger name and url of the created trigger webhook.
 
 If you try to create a trigger webhook and a cloud code trigger with the same name already exists, upon successful creation the response json has an additional `warning` field informing about the name conflict. Note that, trigger webhooks takes precedence over cloud code triggers.
 
-For example,
+For example:
+
+

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"className": "Tournament", "triggerName": "beforeDelete", "url": "https://api.example.com/Scores/beforeDelete"}' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers
@@ -340,14 +369,16 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "className": "Tournament",
@@ -360,11 +391,13 @@ The output may look like this:
 ## Edit function webhook
 To edit the url of a function webhook that was already created use the put method.
 
-Put example,
+Put example:
+
+

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"url":"https://api.example.com/_baz"}' \
   https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions/baz
@@ -377,14 +410,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {"functionName": "baz", "url": "https://api.example.com/baz"}'
 
@@ -393,11 +428,13 @@ It returns the function name and url of the modified webhook. If you try to update a function webhook and a cloud code function with the same name already exists, upon successful update the response json has an additional `warning` field informing about the name conflict. Note that, function webhooks takes precedence over cloud code functions. -For example, +For example: + +

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"url":"https://api.example.com/_bar"}' \
   https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions/bar
@@ -410,14 +447,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "functionName": "bar",
@@ -429,10 +468,11 @@ The output may look like this:
 ## Edit trigger webhook
 To edit the url of a trigger webhook that was already crated use the put method.
 
+

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"url": "https://api.example.com/Game/_beforeSave"}' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers/Game/beforeSave
@@ -445,14 +485,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "className": "Game",
@@ -465,11 +507,13 @@ It returns the class name, trigger name and url of the modified trigger webhook.
 
 If you try to update a trigger webhook and a cloud code trigger with the same name already exists, upon successful update the response json has an additional `warning` field informing about the name conflict. Note that, trigger webhooks takes precedence over cloud code triggers.
 
-For example,
+For example:
+
+

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"url": "https://api.example.com/Scores/beforeDelete"}' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers/Tournament/beforeDelete
@@ -482,14 +526,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "className": "Tournament",
@@ -502,10 +548,11 @@ The output may look like this:
 ## Delete function webhook
 To delete a function webhook use the put method.
 
+

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"__op": "Delete"}' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions/foo
@@ -518,14 +565,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {}
 
@@ -533,10 +582,11 @@ The output may look like this: If a cloud code function with the same name already exists then it is returned as the result. Since the overriding webhook was just deleted, this cloud code function will be run the next time sendMessage is called. +

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{ "__op": "Delete" }' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/functions/sendMessage
@@ -549,14 +599,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 { "functionName": "sendMessage" }
 
@@ -564,10 +616,11 @@ The output may look like this: ## Delete trigger webhook To delete a trigger webhook use the put method. +

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{ "__op": "Delete" }' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers/Game/beforeSave
@@ -580,14 +633,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {}
 
@@ -595,10 +650,11 @@ The output may look like this: If a cloud code trigger with the same name already exists then the it is returned as the result. Since the overriding webhook was just deleted, this cloud code trigger will be run the next time a Tournament object is saved. +

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{ "__op": "Delete" }' \
 https://YOUR.PARSE-SERVER.HERE/parse/hooks/triggers/Tournament/beforeDelete
@@ -611,14 +667,16 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
The output may look like this: +

 {
   "className": "Tournament",
diff --git a/_includes/rest/objects.md b/_includes/rest/objects.md
index 6d32a8e6e..737d02344 100644
--- a/_includes/rest/objects.md
+++ b/_includes/rest/objects.md
@@ -60,7 +60,7 @@ To create a new object on Parse, send a POST request to the class URL containing
 

   curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -75,7 +75,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 results = json.loads(connection.getresponse().read())
@@ -107,7 +107,7 @@ Once you've created an object, you can retrieve its contents by sending a GET re
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
 

@@ -116,7 +116,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore/Ed1nuqPvcm', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -140,13 +140,13 @@ The response body is a JSON object containing all the user-provided fields, plus
 }
 ```
 
-When retrieving objects that have pointers to children, you can fetch child objects by using the `include` option. For instance, to fetch the object pointed to by the "game" key:
+When retrieving objects that have pointers to children, **you can fetch child objects** by using the `include` option. For instance, to fetch the object pointed to by the "game" key:
 
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'include=game' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -158,13 +158,42 @@ params = urllib.urlencode({"include":"game"})
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore/Ed1nuqPvcm?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+When using a MongoDB replica set, you can use the `readPreference` option to choose from which replica the object will be retrieved. You can also use the `includeReadPreference` option to choose from which replica the included pointers will be retrieved. The possible values for both options are `PRIMARY` (default), `PRIMARY_PREFERRED`, `SECONDARY`, `SECONDARY_PREFERRED`, or `NEAREST`. If the `includeReadPreference` option is not set, the same replica chosen for `readPreference` will be also used for the includes. + +
+

+curl -X GET \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -G \
+  --data-urlencode 'include=game' \
+  --data-urlencode 'readPreference=SECONDARY' \
+  --data-urlencode 'includeReadPreference=SECONDARY_PREFERRED' \
+  https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
+
+

+import json,httplib,urllib
+connection = httplib.HTTPSConnection('YOUR.PARSE-SERVER.HERE', 443)
+params = urllib.urlencode({"include":"game","readPreference":"SECONDARY","includeReadPreference":"SECONDARY_PREFERRED"})
+connection.connect()
+connection.request('GET', '/parse/classes/GameScore/Ed1nuqPvcm?%s' % params, '', {
+       "X-Parse-Application-Id": "${APPLICATION_ID}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+     })
+result = json.loads(connection.getresponse().read())
+print result
+
+
+ + + ## Updating Objects To change the data on an object that already exists, send a PUT request to the object URL. Any keys you don't specify will remain unchanged, so you can update just a subset of the object's data. For example, if we wanted to change the score field of our object: @@ -173,7 +202,7 @@ To change the data on an object that already exists, send a PUT request to the o

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"score":73453}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -186,7 +215,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -210,7 +239,7 @@ To help with storing counter-type data, Parse provides the ability to atomically
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"score":{"__op":"Increment","amount":1}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -226,7 +255,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -240,7 +269,7 @@ To decrement the counter, use the `Increment` operator with a negative number:
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"score":{"__op":"Increment","amount":-1}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -256,7 +285,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -278,7 +307,7 @@ Each method takes an array of objects to add or remove in the "objects" key. For
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"skills":{"__op":"AddUnique","objects":["flying","kungfu"]}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -297,7 +326,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -313,7 +342,7 @@ print result
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"opponents":{"__op":"AddRelation","objects":[{"__type":"Pointer","className":"Player","objectId":"Vx4nudeWn"}]}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -335,7 +364,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -349,7 +378,7 @@ To remove an object from a relation, you can do:
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"opponents":{"__op":"RemoveRelation","objects":[{"__type":"Pointer","className":"Player","objectId":"Vx4nudeWn"}]}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -371,7 +400,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -387,7 +416,7 @@ To delete an object from the Parse Cloud, send a DELETE request to its object UR
 

 curl -X DELETE \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
 

@@ -396,7 +425,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('DELETE', '/parse/classes/GameScore/Ed1nuqPvcm', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -409,7 +438,7 @@ You can delete a single field from an object by using the `Delete` operation:
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"opponents":{"__op":"Delete"}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
@@ -424,7 +453,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -442,7 +471,7 @@ Each command in a batch has `method`, `path`, and `body` parameters that specify
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "requests": [
@@ -491,7 +520,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -527,7 +556,7 @@ Other commands that work in a batch are `update` and `delete`.
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "requests": [
@@ -566,7 +595,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -606,7 +635,7 @@ Dates are useful in combination with the built-in `createdAt` and `updatedAt` fi
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2011-08-21T18:02:52.249Z"}}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -625,7 +654,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
diff --git a/_includes/rest/push-notifications.md b/_includes/rest/push-notifications.md
index 9be45c23d..22dc9c830 100644
--- a/_includes/rest/push-notifications.md
+++ b/_includes/rest/push-notifications.md
@@ -31,7 +31,7 @@ Creating an installation object is similar to creating a generic object, but the
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "deviceType": "ios",
@@ -54,7 +54,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -90,7 +90,7 @@ You could create and object with these fields using a command like this:
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "deviceType": "android",
@@ -115,7 +115,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -131,7 +131,7 @@ You can retrieve the contents of an installation object by sending a GET request
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/installations/mrmBZvsErB
 

@@ -140,7 +140,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/installations/mrmBZvsErB', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -170,7 +170,7 @@ Installation objects can be updated by sending a PUT request to the installation
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "deviceType": "ios",
@@ -195,7 +195,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -215,7 +215,7 @@ Without any URL parameters, a GET request simply lists installations:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/installations
 

@@ -224,7 +224,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/installations', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -271,7 +271,7 @@ To delete an installation from the Parse Cloud, send a DELETE request to its URL
 

 curl -X DELETE \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/installations/mrmBZvsErB
 

@@ -280,7 +280,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('DELETE', '/parse/installations/mrmBZvsErB', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -291,7 +291,7 @@ print result
 
 There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting](#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section.
 
-You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push.  For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs.
+You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push.  For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs.
 
 ### Using Channels
 
@@ -307,7 +307,7 @@ Subscribing to a channel via the REST API can be done by updating the `Installat
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "channels": [
@@ -326,7 +326,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -348,7 +348,7 @@ With the REST API, the following code can be used to alert all subscribers of th
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "channels": [
@@ -375,7 +375,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -397,7 +397,7 @@ Storing arbitrary data on an `Installation` object is done in the same way we st
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "scores": true,
@@ -416,7 +416,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -430,7 +430,7 @@ You can even create relationships between your `Installation` objects and other
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "user": {
@@ -453,7 +453,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -469,7 +469,7 @@ Once you have your data stored on your `Installation` objects, you can use a que
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -494,7 +494,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -508,7 +508,7 @@ We can even use channels with our query. To send a push to all subscribers of th
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -535,7 +535,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -549,7 +549,7 @@ If we store relationships to other objects in our `Installation` class, we can a
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -579,16 +579,19 @@ connection.connect()
 connection.request('POST', '/parse/push', json.dumps({
        "where": {
          "user": {
-           "$inQuery": {
-             "location": {
-               "$nearSphere": {
-                 "__type": "GeoPoint",
-                 "latitude": 30.0,
-                 "longitude": -20.0
-               },
-               "$maxDistanceInMiles": 1.0
-             }
-           }
+           "$inQuery":{
+              "where":{
+                "location":{
+                  "$nearSphere":{
+                    "__type":"GeoPoint",
+                      "latitude":51.252437591552734,
+                      "longitude":-1.6038470268249512
+                    },
+                  "$maxDistanceInMiles":1.0
+                }
+              },
+              "className":"_User"
+            }
          }
        },
        "data": {
@@ -596,7 +599,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -617,7 +620,9 @@ If you want to send more than just a message, you can set other fields in the `d
 *   **`alert`**: the notification's message.
 *   **`badge`**: _(iOS only)_ the value indicated in the top right corner of the app icon. This can be set to a value or to `Increment` in order to increment the current value by 1.
 *   **`sound`**: _(iOS only)_ the name of a sound file in the application bundle.
-*   **`content-available`**: _(iOS only)_ If you are a writing an app using the Remote Notification Background Mode [introduced in iOS7](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS7.html#//apple_ref/doc/uid/TP40013162-SW10) (a.k.a. "Background Push"), set this value to 1 to trigger a background download.
+*   **`content-available`**: _(iOS only)_ If you are a writing an app using the Remote Notification Background Mode [introduced in iOS7](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS7.html#//apple_ref/doc/uid/TP40013162-SW10) (a.k.a. "Background Push"), set this value to 1 to trigger a background download. You also have to set `push_type` starting iOS 13 and watchOS 6.
+*   **`push_type`**: _(iOS only)_ The type of the notification. The value is `alert` or `background`. Specify `alert` when the delivery of your notification displays an alert, plays a sound, or badges your app's icon. Specify `background` for silent notifications that do not interact with the user. Defaults to `alert` if no value is set. Required when delivering notifications to devices running iOS 13 and later, or watchOS 6 and later.
+*   **`priority`**: _(iOS only)_ The priority of the notification. Specify 10 to send the notification immediately. Specify 5 to send the notification based on power considerations on the user’s device. ([More detailed documentation](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns))
 *   **`category`**: _(iOS only)_ the identifier of the [`UNNotification​Category`](https://developer.apple.com/reference/usernotifications/unnotificationcategory) for this push notification.
 *   **`uri`**: _(Android only)_ an optional field that contains a URI. When the notification is opened, an `Activity` associated      with opening the URI is launched.
 *   **`title`**: _(Android only)_ the value displayed in the Android system tray notification.
@@ -628,7 +633,7 @@ For example, to send a notification that increases the current badge number by 1
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "channels": [
@@ -659,7 +664,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -688,7 +693,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -706,7 +711,7 @@ There are two parameters provided by Parse to allow setting an expiration date f
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "expiration_time": "2015-03-19T22:05:08Z",
@@ -727,7 +732,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -741,7 +746,7 @@ Alternatively, you can use the `expiration_interval` parameter to specify a dura
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "push_time": "2015-03-13T22:05:08Z",
@@ -764,7 +769,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -782,7 +787,7 @@ The following examples would send a different notification to Android, iOS, and
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -807,7 +812,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -819,7 +824,7 @@ print result
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -844,7 +849,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -856,7 +861,7 @@ print result
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -881,7 +886,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -893,7 +898,7 @@ print result
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -918,7 +923,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -934,7 +939,7 @@ You can schedule a push in advance by specifying a `push_time`. For example, if
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "where": {
@@ -961,7 +966,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -969,11 +974,11 @@ print result
 
-If you also specify an `expiration_interval`, it will be calculated from the scheduled push time, not from the time the push is submitted. +If you also specify an `expiration_interval`, it will be calculated from the scheduled push time, not from the time the push is submitted. This means a push scheduled to be sent in a week with an expiration interval of a day will expire 8 days after the request is sent. -The scheduled time cannot be in the past, and can be up to two weeks in the future. -It can be an ISO 8601 date with a date, time, and timezone, as in the example above, or it can be a numeric value representing a UNIX epoch time in seconds (UTC). +The scheduled time cannot be in the past, and can be up to two weeks in the future. +It can be an ISO 8601 date with a date, time, and timezone, as in the example above, or it can be a numeric value representing a UNIX epoch time in seconds (UTC). To schedule an alert for 08/22/2015 at noon UTC time, you can set the `push_time` to either `2015-08-022T12:00:00.000Z` or `1440226800000`. @@ -987,10 +992,11 @@ To schedule a push according to each device's local time, the `push_time` parame Starting parse-server version 2.6.1, it is possible to localize the push notifications messages according to the _Installation's `localeIdentifier`. +

 curl -X POST \
-  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "data": {
@@ -1010,13 +1016,14 @@ connection.request('POST', '/1/push', json.dumps({
          "alert-fr": "Une alerte en français"
        }
      }), {
-       "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-Application-Id": "${APPLICATION_ID}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
When setting the `alert-[lang|locale]` in the data, parse-server will find all installations that have that language or locale set. The language is usually the first part of the locale. This will have no impact on the query planning, as the localizations will be resolved just before the push is sent. diff --git a/_includes/rest/queries.md b/_includes/rest/queries.md index 885efd4bc..a2acbb84c 100644 --- a/_includes/rest/queries.md +++ b/_includes/rest/queries.md @@ -8,7 +8,7 @@ You can retrieve multiple objects at once by sending a GET request to the class

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
 

@@ -17,7 +17,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -57,7 +57,7 @@ There are several ways to put constraints on the objects found, using the `where
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"playerName":"Sean Plott","cheatMode":false}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -72,7 +72,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -103,7 +103,7 @@ For example, to retrieve scores between 1000 and 3000, including the endpoints,
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"score":{"$gte":1000,"$lte":3000}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -120,7 +120,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -133,7 +133,7 @@ To retrieve scores equal to an odd number below 10, we could issue:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"score":{"$in":[1,3,5,7,9]}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -155,7 +155,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -168,7 +168,7 @@ To retrieve scores not by a given list of players we could issue:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={
    "playerName": {
@@ -196,7 +196,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -209,7 +209,7 @@ To retrieve documents with the score set, we could issue:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"score":{"$exists":true}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -225,7 +225,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -238,7 +238,7 @@ To retrieve documents without the score set, we could issue:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"score":{"$exists":false}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -254,7 +254,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -267,7 +267,7 @@ If you have a class containing sports teams and you store a user's hometown in t
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"hometown":{"$select":{"query":{"className":"Team","where":{"winPct":{"$gt":0.5}}},"key":"city"}}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/_User
@@ -293,7 +293,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/_User?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -302,13 +302,14 @@ print result
 
 In addition to `where`, there are several parameters you can use to configure what types of results are returned by the query.
 
-| Parameter   | Use                                               |
-|-----------------------------------------------------------------|
-| order       | Specify a field to sort by                        |
-| limit       | Limit the number of objects returned by the query |
-| skip        | Use with limit to paginate through results        |
-| keys        | Restrict the fields returned by the query         |
-| include     | Use on Pointer columns to return the full object  |
+| Parameter     | Use                                               |
+|-------------------------------------------------------------------|
+| order         | Specify a field to sort by                        |
+| limit         | Limit the number of objects returned by the query |
+| skip          | Use with limit to paginate through results        |
+| keys          | Restrict the fields returned by the query         |
+| excludeKeys   | Exclude specific fields from the returned query   |
+| include       | Use on Pointer columns to return the full object  |
 
 You can use the `order` parameter to specify a field to sort by. Prefixing with a negative sign reverses the order. Thus, to retrieve scores in ascending order:
 
@@ -316,7 +317,7 @@ You can use the `order` parameter to specify a field to sort by. Prefixing with
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'order=score' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -328,7 +329,7 @@ params = urllib.urlencode({"order":"score"})
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -341,7 +342,7 @@ And to retrieve scores in descending order:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'order=-score' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -353,7 +354,7 @@ params = urllib.urlencode({"order":"-score"})
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -366,7 +367,7 @@ You can sort by multiple fields by passing `order` a comma-separated list. To re
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'order=score,-name' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -378,7 +379,7 @@ params = urllib.urlencode({"order":"score,-name"})
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -391,7 +392,7 @@ You can use the `limit` and `skip` parameters for pagination.`limit` defaults to
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'limit=200' \
   --data-urlencode 'skip=400' \
@@ -404,20 +405,20 @@ params = urllib.urlencode({"limit":200,"skip":400})
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
-You can restrict the fields returned by passing `keys` a comma-separated list. To retrieve documents that contain only the `score` and `playerName` fields (and also special built-in fields such as `objectId`, `createdAt`, and `updatedAt`): +You can restrict the fields returned by passing `keys` or `excludeKeys` a comma-separated list. To retrieve documents that contain only the `score` and `playerName` fields (and also special built-in fields such as `objectId`, `createdAt`, and `updatedAt`):

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'keys=score,playerName' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore
@@ -429,7 +430,32 @@ params = urllib.urlencode({"keys":"score,playerName"})
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+     })
+result = json.loads(connection.getresponse().read())
+print result
+
+
+ +Or you may use `excludeKeys` to fetch everything except `playerName`: + +
+

+curl -X GET \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -G \
+  --data-urlencode 'excludeKeys=playerName' \
+  https://YOUR.PARSE-SERVER.HERE/parse/classes/GameScore/Ed1nuqPvcm
+
+

+import json,httplib,urllib
+connection = httplib.HTTPSConnection('YOUR.PARSE-SERVER.HERE', 443)
+params = urllib.urlencode({"excludeKeys":"playerName"})
+connection.connect()
+connection.request('GET', '/parse/classes/GameScore/Ed1nuqPvcm?%s' % params, '', {
+       "X-Parse-Application-Id": "${APPLICATION_ID}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -442,7 +468,7 @@ All of these parameters can be used in combination with each other. For example:
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={
    "playerName": {
@@ -479,7 +505,7 @@ params = urllib.urlencode({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -495,7 +521,7 @@ For keys with an array type, you can find objects where the key's array value co
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"arrayKey":2}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/RandomObject
@@ -509,7 +535,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/RandomObject?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -522,7 +548,7 @@ You can also use the `$all` operator to find objects with an array field which c
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"arrayKey":{"$all":[2,3,4]}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/RandomObject
@@ -542,7 +568,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/RandomObject?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -551,10 +577,6 @@ print result
 
 ## Queries on String Values
 
-
- If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
- Use the `$regex` operator to restrict to string values that match a regular expression. Most regular expression queries in Parse are heavily throttled due to performance considerations. Use case sensitive, anchored queries where possible. Similar to a MySQL LIKE operator, anchored queries are indexed so they are efficient for large datasets. For example:
@@ -562,7 +584,7 @@ Use the `$regex` operator to restrict to string values that match a regular expr # Finds barbecue sauces that start with "Big Daddy" curl -X GET \ -H "X-Parse-Application-Id: ${APPLICATION_ID}" \ - -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \ + -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \ -G \ --data-urlencode 'where={"name":{"$regex":"^Big Daddy"}}' \ https://YOUR.PARSE-SERVER.HERE/parse/classes/BarbecueSauce @@ -579,7 +601,7 @@ params = urllib.urlencode({"where":json.dumps({ connection.connect() connection.request('GET', '/parse/classes/BarbecueSauce?%s' % params, '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}" + "X-Parse-REST-API-Key": "${REST_API_KEY}" }) result = json.loads(connection.getresponse().read()) print result @@ -617,8 +639,8 @@ Note: Postgres doesn't support `$caseSensitive` for Full Text Search, please use

 # Finds strings that contains "Daddy"
 curl -X GET \
-  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"name":{"$text":{"$search":{"$term":"Daddy"}}}}' \
   https://api.parse.com/1/classes/BarbecueSauce
@@ -638,8 +660,8 @@ params = urllib.urlencode({"where":json.dumps({
      })})
 connection.connect()
 connection.request('GET', '/1/classes/BarbecueSauce?%s' % params, '', {
-       "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Application-Id": "${APPLICATION_ID}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -652,8 +674,8 @@ print result
 

 # Finds strings that contains "Daddy" ordered by relevance
 curl -X GET \
-  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"name":{"$text":{"$search":{"$term":"Daddy"}}}}' \
   --data-urlencode 'order="$score"' \
@@ -678,8 +700,8 @@ params = urllib.urlencode({"where":json.dumps({
      })
 connection.connect()
 connection.request('GET', '/1/classes/BarbecueSauce?%s' % params, '', {
-       "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Application-Id": "${APPLICATION_ID}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -696,7 +718,7 @@ There are several ways to issue queries for relational data. If you want to retr
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"post":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/Comment
@@ -714,7 +736,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/Comment?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -727,7 +749,7 @@ If you want to retrieve objects where a field contains an object that matches an
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"post":{"$inQuery":{"where":{"image":{"$exists":true}},"className":"Post"}}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/Comment
@@ -750,7 +772,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/Comment?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -763,7 +785,7 @@ If you want to retrieve objects where a field contains an object that does not m
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"post":{"$notInQuery":{"where":{"image":{"$exists":true}},"className":"Post"}}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/Comment
@@ -786,7 +808,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/Comment?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -799,7 +821,7 @@ If you want to retrieve objects that are members of `Relation` field of a parent
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/users
@@ -820,7 +842,7 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/users?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -833,7 +855,7 @@ In some situations, you want to return multiple types of related objects in one
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'order=-createdAt' \
   --data-urlencode 'limit=10' \
@@ -847,7 +869,7 @@ params = urllib.urlencode({"order":"-createdAt","limit":10,"include":"post"})
 connection.connect()
 connection.request('GET', '/parse/classes/Comment?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -883,7 +905,7 @@ You can also do multi level includes using dot notation.  If you wanted to inclu
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'order=-createdAt' \
   --data-urlencode 'limit=10' \
@@ -897,15 +919,15 @@ params = urllib.urlencode({"order":"-createdAt","limit":10,"include":"post.autho
 connection.connect()
 connection.request('GET', '/parse/classes/Comment?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
You can issue a query with multiple fields included by passing a comma-separated list of keys as the `include` parameter. - ## Counting Objects Note: In the old Parse hosted backend, count queries were rate limited to a maximum of 160 requests per minute. They also returned inaccurate results for classes with more than 1,000 objects. But, Parse Server has removed both constraints and can count objects well above 1,000. @@ -916,7 +938,7 @@ If you are limiting your query, or if there are a very large number of results,

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"playerName":"Jonathan Walsh"}' \
   --data-urlencode 'count=1' \
@@ -932,11 +954,12 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
Since this requests a count as well as limiting to zero results, there will be a count but no results in the response. @@ -957,7 +980,7 @@ With a nonzero limit, that request would return results as well as the count.

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"$or":[{"wins":{"$gt":150}},{"wins":{"$lt":5}}]}' \
   https://YOUR.PARSE-SERVER.HERE/parse/classes/Player
@@ -982,11 +1005,12 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/classes/Player?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+ Any other constraints on the query are also applied to the object returned, so you can add other constraints to queries with `$or`. @@ -1002,8 +1026,8 @@ Finds unique values for a specified field.

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'distinct=score' \
   https://YOUR.PARSE-SERVER.HERE/parse/aggregate/GameScore
@@ -1015,12 +1039,13 @@ params = urllib.urlencode({"distinct":"score"})
 connection.connect()
 connection.request('GET', '/parse/aggregate/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+ Can be used with `where` parameter for constraining the value for keys. @@ -1028,8 +1053,8 @@ Can be used with `where` parameter for constraining the value for keys.

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'where={"playerName":"Sean Plott"},distinct=score' \
   https://YOUR.PARSE-SERVER.HERE/parse/aggregate/GameScore
@@ -1043,12 +1068,13 @@ params = urllib.urlencode({"where":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/aggregate/GameScore?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+ ## Aggregate Queries @@ -1070,8 +1096,8 @@ Note: `_id` does not exist in parse-server. Please replace with `objectId`.

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'group={"objectId":null,"total":{"$sum":"$score"}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/aggregate/Player
@@ -1088,12 +1114,13 @@ params = urllib.urlencode({"group":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/aggregate/Player?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+ You can add or remove existing fields with `project` parameter. `project` is similar to `keys`. @@ -1101,8 +1128,8 @@ You can add or remove existing fields with `project` parameter. `project` is sim

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'project={"score":1}' \
   https://YOUR.PARSE-SERVER.HERE/parse/aggregate/Player
@@ -1116,12 +1143,13 @@ params = urllib.urlencode({"project":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/aggregate/Player?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+ You can filter out objects with `match` parameter. `match` is similar to `$eq`. @@ -1129,8 +1157,8 @@ You can filter out objects with `match` parameter. `match` is similar to `$eq`.

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -G \
   --data-urlencode 'match={"score":{"$gt":15}}' \
   https://YOUR.PARSE-SERVER.HERE/parse/aggregate/Player
@@ -1146,11 +1174,60 @@ params = urllib.urlencode({"match":json.dumps({
 connection.connect()
 connection.request('GET', '/parse/aggregate/Player?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+ + +You can also constraint by `limit`, `skip`, `sort`. + +## Read Preference -You can also constraint by `limit`, `skip`, `sort` +When using a MongoDB replica set, you can use the `readPreference` option to choose from which replica the objects will be retrieved. You can also use the `includeReadPreference` option to choose from which replica the included pointers will be retrieved and the `subqueryReadPreference` option to choose in which replica the subqueries will run. The possible values these options are `PRIMARY` (default), `PRIMARY_PREFERRED`, `SECONDARY`, `SECONDARY_PREFERRED`, or `NEAREST`. If the `includeReadPreference` option is not set, the same replica chosen for `readPreference` will be also used for the includes. The same rule applies for the `subqueryReadPreference` option. + +
+

+curl -X GET \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -G \
+  --data-urlencode 'where={"post":{"$inQuery":{"where":{"image":{"$exists":true}},"className":"Post"}}}' \
+  --data-urlencode 'include=post' \
+  --data-urlencode 'readPreference=SECONDARY' \
+  --data-urlencode 'includeReadPreference=SECONDARY_PREFERRED' \
+  --data-urlencode 'subqueryReadPreference=NEAREST' \
+  https://YOUR.PARSE-SERVER.HERE/parse/classes/Comment
+
+

+import json,httplib,urllib
+connection = httplib.HTTPSConnection('YOUR.PARSE-SERVER.HERE', 443)
+params = urllib.urlencode({
+  "where":json.dumps({
+    "post": {
+      "$inQuery": {
+        "where": {
+          "image": {
+            "$exists": True
+          }
+        },
+        "className": "Post"
+      }
+    }
+  }),
+  "include":"post",
+  "readPreference":"SECONDARY",
+  "includeReadPreference":"SECONDARY_PREFERRED",
+  "subqueryReadPreference":"NEAREST"
+})
+connection.connect()
+connection.request('GET', '/parse/classes/Comment?%s' % params, '', {
+       "X-Parse-Application-Id": "${APPLICATION_ID}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+     })
+result = json.loads(connection.getresponse().read())
+print result
+
+
diff --git a/_includes/rest/quick-reference.md b/_includes/rest/quick-reference.md index 92b0dae8e..84ab29f83 100644 --- a/_includes/rest/quick-reference.md +++ b/_includes/rest/quick-reference.md @@ -163,6 +163,6 @@ Whether a request succeeded is indicated by the HTTP status code. A 2xx status c You should not use the REST API Key in client apps (i.e. code you distribute to your customers). If the Parse SDK is available for your client platform, we recommend using our SDK instead of the REST API. If you must call the REST API directly from the client, you should use the corresponding client-side Parse key for that plaform (e.g. Client Key for iOS/Android, or .NET Key for Windows/Xamarin/Unity). -If there is no Parse SDK for your client platform, please use your app's Client Key to call the REST API. Requests made with the Client Key, JavaScript Key, or Windows Key are restricted by client-side app settings that you configure in your Parse.com app dashboard. These settings make your app more secure. For example, we recommend that all production apps turn off the "Client Push Enabled" setting to prevent push notifications from being sent from any device using the Client Key, JavaScript Key, or .NET Key, but not the REST API Key. Therefore, if you plan on registering installations to enable Push Notifications for your app, you should not distribute any app code with the REST API key embedded in it. +If there is no Parse SDK for your client platform, please use your app's Client Key to call the REST API. Requests made with the Client Key, JavaScript Key, or Windows Key are restricted by client-side app settings that you configure in your Parse Dashboard app dashboard. These settings make your app more secure. For example, we recommend that all production apps turn off the "Client Push Enabled" setting to prevent push notifications from being sent from any device using the Client Key, JavaScript Key, or .NET Key, but not the REST API Key. Therefore, if you plan on registering installations to enable Push Notifications for your app, you should not distribute any app code with the REST API key embedded in it. The JavaScript Key cannot be used to make requests directly against the REST API from JavaScript. The JavaScript Key is meant to be used with the Parse JavaScript SDK, which makes its posts through a Cross Origin-friendly format without HTTP headers. diff --git a/_includes/rest/roles.md b/_includes/rest/roles.md index 93622e481..096d23504 100644 --- a/_includes/rest/roles.md +++ b/_includes/rest/roles.md @@ -22,7 +22,7 @@ To create a new role, send a POST request to the roles root:

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "name": "Moderators",
@@ -47,7 +47,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -61,7 +61,7 @@ You can create a role with child roles or users by adding existing objects to th
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "name": "Moderators",
@@ -126,7 +126,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -158,7 +158,7 @@ You can also retrieve the contents of a role object by sending a GET request to
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/roles/mrmBZvsErB
 

@@ -167,7 +167,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/roles/mrmBZvsErB', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -206,7 +206,7 @@ For example, we can add two users to the "Moderators" role created above like so
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "users": {
@@ -249,7 +249,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -263,7 +263,7 @@ Similarly, we can remove a child role from the "Moderators" role created above l
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "roles": {
@@ -296,7 +296,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -315,7 +315,7 @@ To delete a role from the Parse Cloud, send a DELETE request to its URL.  For ex
 

 curl -X DELETE \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/roles/mrmBZvsErB
 

@@ -324,7 +324,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('DELETE', '/parse/roles/mrmBZvsErB', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}"
+       "X-Parse-Master-Key": "${MASTER_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -337,7 +337,7 @@ Again, we pass the master key in order to bypass the ACL on the role itself.  Al
 

 curl -X DELETE \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/roles/mrmBZvsErB
 
@@ -347,7 +347,7 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('DELETE', '/parse/roles/mrmBZvsErB', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) @@ -389,7 +389,7 @@ These types of relationships are commonly found in applications with user-manage

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '{
         "roles": {
@@ -422,7 +422,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
diff --git a/_includes/rest/schemas.md b/_includes/rest/schemas.md
index 1164f47ec..d617c5234 100644
--- a/_includes/rest/schemas.md
+++ b/_includes/rest/schemas.md
@@ -21,7 +21,7 @@ as strings in object representation. This is a special case for the Parse API.
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   https://YOUR.PARSE-SERVER.HERE/parse/schemas
 
@@ -31,7 +31,7 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/schemas', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) @@ -78,7 +78,7 @@ To fetch schema of a single class, run:

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   https://YOUR.PARSE-SERVER.HERE/parse/schemas/Game
 
@@ -88,7 +88,7 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/schemas/Game', "", { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) @@ -105,7 +105,7 @@ fields and some default fields applicable to the class. To add the schema, run:

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '
     {
@@ -126,7 +126,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -140,7 +140,7 @@ You may also add indexes to your fields. You need to use the format you need to
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '
     {
@@ -166,7 +166,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -182,7 +182,7 @@ You can add or delete columns to a schema. To do so, run:
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '
     {
@@ -208,7 +208,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -222,7 +222,7 @@ To delete a particular field or index, you need to use `{"__op" : "Delete" }`
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   -d '
     {
@@ -248,7 +248,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-Master-Key": "${MASTER_KEY}",
+       "X-Parse-Master-Key": "${MASTER_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -265,7 +265,7 @@ To do that, run:
 

 curl -X DELETE\
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
+  -H "X-Parse-Master-Key: ${MASTER_KEY}" \
   -H "Content-Type: application/json" \
   https://YOUR.PARSE-SERVER.HERE/parse/schemas/City
 
@@ -275,7 +275,7 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('PUT', '/parse/schemas/City', "", { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-Master-Key": "${MASTER_KEY}", + "X-Parse-Master-Key": "${MASTER_KEY}", "Content-Type": "application/json" }) result = json.loads(connection.getresponse().read()) diff --git a/_includes/rest/sessions.md b/_includes/rest/sessions.md index abd17c1b2..4ffbb2001 100644 --- a/_includes/rest/sessions.md +++ b/_includes/rest/sessions.md @@ -1,6 +1,6 @@ # Sessions -Sessions represent an instance of a user logged into a device. Sessions are automatically created when users log in or sign up. They are automatically deleted when users log out. There is one distinct `Session` object for each user-installation pair; if a user issues a login request from a device they're already logged into, that user's previous `Session` object for that Installation is automatically deleted. `Session` objects are stored on Parse in the Session class, and you can view them on the Parse.com Data Browser. We provide a set of APIs to manage `Session` objects in your app. +Sessions represent an instance of a user logged into a device. Sessions are automatically created when users log in or sign up. They are automatically deleted when users log out. There is one distinct `Session` object for each user-installation pair; if a user issues a login request from a device they're already logged into, that user's previous `Session` object for that Installation is automatically deleted. `Session` objects are stored on Parse in the Session class, and you can view them on the Parse Dashboard Data Browser. We provide a set of APIs to manage `Session` objects in your app. A `Session` is a subclass of a Parse `Object`, so you can query, update, and delete sessions in the same way that you manipulate normal objects on Parse. Because the Parse Cloud automatically creates sessions when you log in or sign up users, you should not manually create `Session` objects unless you are building a "Parse for IoT" app (e.g. Arduino or Embedded C). Deleting a `Session` will log the user out of the device that is currently using this session's token. @@ -18,7 +18,7 @@ The `Session` object has these special fields: * `restricted` (readonly): Boolean for whether this session is restricted. * Restricted sessions do not have write permissions on `User`, `Session`, and `Role` classes on Parse. Restricted sessions also cannot read unrestricted sessions. * All sessions that the Parse Cloud automatically creates during user login/signup will be unrestricted. All sessions that the developer manually creates by saving a new `Session` object from the client (only needed for "Parse for IoT" apps) will be restricted. -* `expiresAt` (readonly): Approximate UTC date when this `Session` object will be automatically deleted. You can configure session expiration settings (either 1-year inactivity expiration or no expiration) in your app's Parse.com dashboard settings page. +* `expiresAt` (readonly): Approximate UTC date when this `Session` object will be automatically deleted. You can configure session expiration settings (either 1-year inactivity expiration or no expiration) in your app's Parse Dashboard settings page. * `installationId` (can be set only once): String referring to the `Installation` where the session is logged in from. For the REST API, you can set this by passing the `X-Parse-Installation-Id` header on login and signup requests. All special fields except `installationId` can only be set automatically by the Parse Cloud. You can add custom fields onto `Session` objects, but please keep in mind that any logged-in device (with session token) can read other sessions that belong to the same user (unless you disable Class-Level Permissions, see below). @@ -34,10 +34,11 @@ For mobile apps and websites, you should not create `Session` objects manually. In "Parse for IoT" apps (e.g. Arduino or Embedded C), you may want to programmatically create a restricted session that can be transferred to an IoT device. In order to do this, you must first log in normally to obtain an unrestricted session token. Then, you can create a restricted session by providing this unrestricted session token: +

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   -H "Content-Type: application/json" \
   -d '{"customField":"value"}' \
@@ -51,13 +52,14 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
In the above code, `r:pnktnjyb996sj4p156gjtp4im` is the unrestricted session token from the original user login. @@ -80,10 +82,12 @@ At this point, you can pass the session token `r:aVrtljyb7E8xKo9256gfvp4n2` to a ## Retrieving Sessions If you have the session's objectId, you fetch the `Session` object as long as it belongs to the same user as your current session: + +

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/sessions/Axy98kq1B09
 
@@ -93,19 +97,21 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/sessions/Axy98kq1B09', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) print result
+ If you only have the session's token (from previous login or session create), you can validate and fetch the corresponding session by: +

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/sessions/me
 
@@ -115,21 +121,23 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/sessions/me', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) print result
+ ## Updating Sessions Updating a session is analogous to updating a Parse object. +

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   -H "Content-Type: application/json" \
   -d '{"customField":"value"}' \
@@ -141,21 +149,23 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('POST', '/parse/logout', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
## Querying Sessions Querying for `Session` objects will only return objects belonging to the same user as your current session (due to the Session ACL). You can also add a where clause to your query, just like normal Parse objects. +

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/sessions
 
@@ -165,23 +175,23 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/sessions', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) print result
- - + ## Deleting Sessions Deleting the Session object will revoke its session token and cause the user to be logged out on the device that's currently using this session token. When you have the session token, then you can delete its `Session` object by calling the logout endpoint: +

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/logout
 
@@ -191,19 +201,21 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('POST', '/parse/logout', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) print result
+ If you want to delete another `Session` object for your user, and you have its `objectId`, you can delete it (but not log yourself out) by: +

 curl -X DELETE \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/sessions/Axy98kq1B09
 
@@ -213,12 +225,13 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('DELETE', '/parse/sessions/Axy98kq1B09', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) print result
+ `X-Parse-Session-Token` authenticates the request as the user that also owns session `Axy98kq1B09`, which may have a different session token. You can only delete other sessions that belong to the same user. @@ -233,6 +246,7 @@ The following API is most useful for "Parse for IoT" apps (e.g. Arduino or Embed 3. IoT device connects to Internet via Wi-Fi, saves its `Installation` object. 4. IoT device calls the following endpoint to associate the its `installationId` with its session. This endpoint only works with session tokens from restricted sessions. Please note that REST API calls from an IoT device should use the Client Key, not the REST API Key. +

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
@@ -250,13 +264,14 @@ connection.connect()
 connection.request('PUT', '/parse/sessions/me', json.dumps({
      }), {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Session-Token": "r:aVrtljyb7E8xKo9256gfvp4n2",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
 print result
 
+
## Session Security @@ -280,6 +295,7 @@ Parse.Cloud.beforeSave("MyClass", function(request, response) { }); });
+ You can configure Class-Level Permissions (CLPs) for the Session class just like other classes on Parse. CLPs restrict reading/writing of sessions via the /parse/sessions API, but do not restrict Parse Cloud's automatic session creation/deletion when users log in, sign up, and log out. We recommend that you disable all CLPs not needed by your app. Here are some common use cases for Session CLPs: * **Find**, **Delete** — Useful for building a UI screen that allows users to see their active session on all devices, and log out of sessions on other devices. If your app does not have this feature, you should disable these permissions. diff --git a/_includes/rest/users.md b/_includes/rest/users.md index d2b3e102c..025cb6470 100644 --- a/_includes/rest/users.md +++ b/_includes/rest/users.md @@ -16,7 +16,7 @@ To sign up a new user, send a POST request to the users root. You may add any ad

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Revocable-Session: 1" \
   -H "Content-Type: application/json" \
   -d '{"username":"cooldude6","password":"p_n7!-e8","phone":"415-392-0202"}' \
@@ -32,7 +32,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Revocable-Session": "1",
        "Content-Type": "application/json"
      })
@@ -66,7 +66,7 @@ After you allow users to sign up, you need to let them log in to their account w
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Revocable-Session: 1" \
   -G \
   --data-urlencode 'username=cooldude6' \
@@ -80,7 +80,7 @@ params = urllib.urlencode({"username":"cooldude6","password":"p_n7!-e8"})
 connection.connect()
 connection.request('GET', '/parse/login?%s' % params, '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Revocable-Session": "1"
      })
 result = json.loads(connection.getresponse().read())
@@ -117,7 +117,7 @@ You can request a verification email to be sent by sending a POST request to 
 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"email":"email@example.com"}' \
   https://YOUR.PARSE-SERVER.HERE/parse/verificationEmailRequest
@@ -130,7 +130,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -148,7 +148,7 @@ You can initiate password resets for users who have emails associated with their
 

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "Content-Type: application/json" \
   -d '{"email":"coolguy@iloveapps.com"}' \
   https://YOUR.PARSE-SERVER.HERE/parse/requestPasswordReset
@@ -161,7 +161,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "Content-Type": "application/json"
      })
 result = json.loads(connection.getresponse().read())
@@ -180,7 +180,7 @@ You can also retrieve the contents of a user object by sending a GET request to
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/users/g7y9tkhB7O
 

@@ -189,7 +189,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/users/g7y9tkhB7O', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -216,7 +216,7 @@ With a valid session token, you can send a GET request to the 
 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/users/me
 
@@ -226,7 +226,7 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('GET', '/parse/users/me', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) @@ -255,7 +255,7 @@ For example, if we wanted to change the phone number for `cooldude6`:

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   -H "Content-Type: application/json" \
   -d '{"phone":"415-369-6201"}' \
@@ -269,7 +269,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im",
        "Content-Type": "application/json"
      })
@@ -294,7 +294,7 @@ You can retrieve multiple users at once by sending a GET request to the root use
 

 curl -X GET \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   https://YOUR.PARSE-SERVER.HERE/parse/users
 

@@ -303,7 +303,7 @@ connection = httplib.HTTPSConnection('YOUR
 connection.connect()
 connection.request('GET', '/parse/users', '', {
        "X-Parse-Application-Id": "${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}"
+       "X-Parse-REST-API-Key": "${REST_API_KEY}"
      })
 result = json.loads(connection.getresponse().read())
 print result
@@ -344,7 +344,7 @@ To delete a user from the Parse Cloud, send a DELETE request to its URL. You mus
 

 curl -X DELETE \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
   https://YOUR.PARSE-SERVER.HERE/parse/users/g7y9tkhB7O
 
@@ -354,7 +354,7 @@ connection = httplib.HTTPSConnection('YOUR connection.connect() connection.request('DELETE', '/parse/users/g7y9tkhB7O', '', { "X-Parse-Application-Id": "${APPLICATION_ID}", - "X-Parse-REST-API-Key": "${REST_API_KEY}", + "X-Parse-REST-API-Key": "${REST_API_KEY}", "X-Parse-Session-Token": "r:pnktnjyb996sj4p156gjtp4im" }) result = json.loads(connection.getresponse().read()) @@ -417,7 +417,7 @@ Signing a user up with a linked service and logging them in with that service us

 curl -X POST \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Revocable-Session: 1" \
   -H "Content-Type: application/json" \
   -d '{
@@ -451,7 +451,7 @@ connection.request('POST', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Revocable-Session": "1",
        "Content-Type": "application/json"
      })
@@ -515,7 +515,7 @@ Linking an existing user with a service like Facebook or Twitter uses a PUT requ
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:samplei3l83eerhnln0ecxgy5" \
   -H "Content-Type: application/json" \
   -d '{
@@ -543,7 +543,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Session-Token": "r:samplei3l83eerhnln0ecxgy5",
        "Content-Type": "application/json"
      })
@@ -562,7 +562,7 @@ Unlinking an existing user with a service also uses a PUT request to clear `auth
 

 curl -X PUT \
   -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
-  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
+  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
   -H "X-Parse-Session-Token: r:samplei3l83eerhnln0ecxgy5" \
   -H "Content-Type: application/json" \
   -d '{
@@ -582,7 +582,7 @@ connection.request('PUT', '/parse/${APPLICATION_ID}",
-       "X-Parse-REST-API-Key": "${REST_API_KEY}",
+       "X-Parse-REST-API-Key": "${REST_API_KEY}",
        "X-Parse-Session-Token": "r:samplei3l83eerhnln0ecxgy5",
        "Content-Type": "application/json"
      })
diff --git a/_includes/unity/files.md b/_includes/unity/files.md
index 80f7832ff..ae38acc8e 100644
--- a/_includes/unity/files.md
+++ b/_includes/unity/files.md
@@ -2,7 +2,7 @@
 
 ## The ParseFile
 
-`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data (up to 10 megabytes).
+`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data.
 
 Getting started with `ParseFile` is easy. First, you'll need to have the data in `byte[]` or `Stream` form and then create a `ParseFile` with it. In this example, we'll just use a string:
 
diff --git a/_includes/unity/push-notifications.md b/_includes/unity/push-notifications.md
index 5aae1a58a..5ede4e6dd 100644
--- a/_includes/unity/push-notifications.md
+++ b/_includes/unity/push-notifications.md
@@ -41,13 +41,13 @@ While it is possible to modify a `ParseInstallation` just like you would a `Pars
 
 There are two ways to send push notifications using Parse: [channels](#using-channels) and [advanced targeting](#using-advanced-targeting). Channels offer a simple and easy to use model for sending pushes, while advanced targeting offers a more powerful and flexible model. Both are fully compatible with each other and will be covered in this section.
 
-Sending notifications is often done from the Parse.com push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app.
+Sending notifications is often done from the Parse Dashboard push console, the [REST API]({{ site.baseUrl }}/rest/guide/#sending-pushes) or from [Cloud Code]({{ site.baseUrl }}/js/guide/#sending-pushes). However, push notifications can also be triggered by the existing client SDKs. If you decide to send notifications from the client SDKs, you will need to set **Client Push Enabled** in the Push Notifications settings of your Parse app.
 
-However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app, as outlined [on our blog](http://blog.parse.com/2014/09/03/the-dangerous-world-of-client-push/). We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production.
+However, be sure you understand that enabling Client Push can lead to a security vulnerability in your app. We recommend that you enable Client Push for testing purposes only, and move your push notification logic into Cloud Code when your app is ready to go into production.
 
 
 
-You can view your past push notifications on the Parse.com push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs.
+You can view your past push notifications on the Parse Dashboard push console for up to 30 days after creating your push. For pushes scheduled in the future, you can delete the push on the push console as long as no sends have happened yet. After you send the push, the push console shows push analytics graphs.
 
 ### Using Channels
 
@@ -289,7 +289,7 @@ wpPush.SendAsync();
 
 ## Scheduling Pushes
 
-Sending scheduled push notifications is not currently supported by the .NET SDK. Take a look at the [REST API]({{ site.baseUrl }}/rest/guide/#scheduling-pushes), [JavaScript SDK]({{ site.baseUrl }}/js/guide/#scheduling-pushes) or the Parse.com push console.
+Sending scheduled push notifications is not currently supported by the .NET SDK. Take a look at the [REST API]({{ site.baseUrl }}/rest/guide/#scheduling-pushes), [JavaScript SDK]({{ site.baseUrl }}/js/guide/#scheduling-pushes) or the Parse Dashboard push console.
 
 ## Receiving Pushes
 
diff --git a/_includes/unity/queries.md b/_includes/unity/queries.md
index 35b1f5199..020b3eeab 100644
--- a/_includes/unity/queries.md
+++ b/_includes/unity/queries.md
@@ -140,10 +140,6 @@ var query = ParseObject.GetQuery("MyClass")
 
 ## Queries on String Values
 
-
- If you're trying to implement a generic search feature, we recommend taking a look at this blog post: Implementing Scalable Search on a NoSQL Backend. -
- Use `WhereStartsWith` to restrict to string values that start with a particular string. Similar to a MySQL LIKE operator, this is indexed so it is efficient for large datasets: ```cs diff --git a/arduino.md b/arduino.md index ed50b52c5..1e5993c08 100644 --- a/arduino.md +++ b/arduino.md @@ -16,7 +16,6 @@ sections: - "arduino/push-notifications.md" - "arduino/analytics.md" - "arduino/cloud-code.md" -- "arduino/sample-app.md" - "common/errors.md" --- diff --git a/assets/images/graphql/dashboard-graphql-playground.png b/assets/images/graphql/dashboard-graphql-playground.png new file mode 100644 index 000000000..be5eab1b9 Binary files /dev/null and b/assets/images/graphql/dashboard-graphql-playground.png differ diff --git a/assets/images/graphql/graphql-docs.png b/assets/images/graphql/graphql-docs.png new file mode 100644 index 000000000..99ca7195a Binary files /dev/null and b/assets/images/graphql/graphql-docs.png differ diff --git a/assets/images/graphql/graphql-playground.png b/assets/images/graphql/graphql-playground.png new file mode 100644 index 000000000..bafa125b3 Binary files /dev/null and b/assets/images/graphql/graphql-playground.png differ diff --git a/assets/images/graphql/parse-graphql-server.png b/assets/images/graphql/parse-graphql-server.png new file mode 100644 index 000000000..0c1378311 Binary files /dev/null and b/assets/images/graphql/parse-graphql-server.png differ diff --git a/assets/images/graphql/session-token.png b/assets/images/graphql/session-token.png new file mode 100644 index 000000000..81c4b1f1f Binary files /dev/null and b/assets/images/graphql/session-token.png differ diff --git a/assets/js/bundle.js b/assets/js/bundle.js index f1d16c896..43b7b23ab 100644 --- a/assets/js/bundle.js +++ b/assets/js/bundle.js @@ -1,22 +1,22 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=100)}([function(e,t,n){(function(r){var o,i;o=[n(13),n(1),n(98),n(29),n(58),n(57),n(28),n(27),n(97),n(26),n(56),n(96),n(7),n(55)],void 0===(i=function(e,t,n,r,o,i,a,s,u,c,l,f,p,d){"use strict";var h=function(e,t){return new h.fn.init(e,t)},v=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,g=/^-ms-/,m=/-([a-z])/g,y=function(e,t){return t.toUpperCase()};function x(e){var t=!!e&&"length"in e&&e.length,n=h.type(e);return"function"!==n&&!h.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}return h.fn=h.prototype={jquery:"3.0.0",constructor:h,length:0,toArray:function(){return r.call(this)},get:function(e){return null!=e?e<0?this[e+this.length]:this[e]:r.call(this)},pushStack:function(e){var t=h.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return h.each(this,e)},map:function(e){return this.pushStack(h.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(r.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n)[^>]*|#([\w-]+))$/,i=e.fn.init=function(i,a,s){var u,c;if(!i)return this;if(s=s||r,"string"==typeof i){if(!(u="<"===i[0]&&">"===i[i.length-1]&&i.length>=3?[null,i,null]:o.exec(i))||!u[1]&&a)return!a||a.jquery?(a||s).find(i):this.constructor(a).find(i);if(u[1]){if(a=a instanceof e?a[0]:a,e.merge(this,e.parseHTML(u[1],a&&a.nodeType?a.ownerDocument||a:t,!0)),n.test(u[1])&&e.isPlainObject(a))for(u in a)e.isFunction(this[u])?this[u](a[u]):this.attr(u,a[u]);return this}return(c=t.getElementById(u[2]))&&(this[0]=c,this.length=1),this}return i.nodeType?(this[0]=i,this.length=1,this):e.isFunction(i)?void 0!==s.ready?s.ready(i):i(e):e.makeArray(i,this)};return i.prototype=e.fn,r=e(t),i}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(50)],void 0===(o=function(e){"use strict";return new e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0)],void 0===(o=function(e){"use strict";var t=function(n,r,o,i,a,s,u){var c=0,l=n.length,f=null==o;if("object"===e.type(o))for(c in a=!0,o)t(n,r,c,o[c],!0,s,u);else if(void 0!==i&&(a=!0,e.isFunction(i)||(u=!0),f&&(u?(r.call(n,i),r=null):(f=r,r=function(t,n,r){return f.call(e(t),r)})),r))for(;c0&&(T=window.setTimeout(function(){M.abort("timeout")},A.timeout));try{k=!1,x.send(H,P)}catch(e){if(k)throw e;P(-1,e)}}else P(-1,"No Transport");function P(t,n,r,o){var i,a,s,u,c,l=n;k||(k=!0,T&&window.clearTimeout(T),x=void 0,w=o||"",M.readyState=t>0?4:0,i=t>=200&&t<300||304===t,r&&(u=function(e,t,n){for(var r,o,i,a,s=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(o in s)if(s[o]&&s[o].test(r)){u.unshift(o);break}if(u[0]in n)i=u[0];else{for(o in n){if(!u[0]||e.converters[o+" "+u[0]]){i=o;break}a||(a=o)}i=i||a}if(i)return i!==u[0]&&u.unshift(i),n[i]}(A,M,r)),u=function(e,t,n,r){var o,i,a,s,u,c={},l=e.dataTypes.slice();if(l[1])for(a in e.converters)c[a.toLowerCase()]=e.converters[a];for(i=l.shift();i;)if(e.responseFields[i]&&(n[e.responseFields[i]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=i,i=l.shift())if("*"===i)i=u;else if("*"!==u&&u!==i){if(!(a=c[u+" "+i]||c["* "+i]))for(o in c)if((s=o.split(" "))[1]===i&&(a=c[u+" "+s[0]]||c["* "+s[0]])){!0===a?a=c[o]:!0!==c[o]&&(i=s[0],l.unshift(s[1]));break}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+i}}}return{state:"success",data:t}}(A,u,M,i),i?(A.ifModified&&((c=M.getResponseHeader("Last-Modified"))&&(e.lastModified[b]=c),(c=M.getResponseHeader("etag"))&&(e.etag[b]=c)),204===t||"HEAD"===A.type?l="nocontent":304===t?l="notmodified":(l=u.state,a=u.data,i=!(s=u.error))):(s=l,!t&&l||(l="error",t<0&&(t=0))),M.status=t,M.statusText=(n||l)+"",i?L.resolveWith(D,[a,l,M]):L.rejectWith(D,[M,l,s]),M.statusCode(_),_=void 0,E&&O.trigger(i?"ajaxSuccess":"ajaxError",[M,A,i?a:s]),q.fireWith(D,[M,l]),E&&(O.trigger("ajaxComplete",[M,A]),--e.active||e.event.trigger("ajaxStop")))}return M},getJSON:function(t,n,r){return e.get(t,n,r,"json")},getScript:function(t,n){return e.get(t,void 0,n,"script")}}),e.each(["get","post"],function(t,n){e[n]=function(t,r,o,i){return e.isFunction(r)&&(i=i||o,o=r,r=void 0),e.ajax(e.extend({url:t,type:n,dataType:i,data:r,success:o},e.isPlainObject(t)&&t))}}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(1),n(19),n(6),n(29),n(4),n(3),n(2)],void 0===(o=function(e,t,n,r,o,i){"use strict";var a=/^key/,s=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,u=/^([^.]*)(?:\.(.+)|)/;function c(){return!0}function l(){return!1}function f(){try{return t.activeElement}catch(e){}}function p(t,n,r,o,i,a){var s,u;if("object"==typeof n){for(u in"string"!=typeof r&&(o=o||r,r=void 0),n)p(t,u,r,o,n[u],a);return t}if(null==o&&null==i?(i=r,o=r=void 0):null==i&&("string"==typeof r?(i=o,o=void 0):(i=o,o=r,r=void 0)),!1===i)i=l;else if(!i)return t;return 1===a&&(s=i,(i=function(t){return e().off(t),s.apply(this,arguments)}).guid=s.guid||(s.guid=e.guid++)),t.each(function(){e.event.add(this,n,i,o,r)})}return e.event={global:{},add:function(t,o,a,s,c){var l,f,p,d,h,v,g,m,y,x,b,w=i.get(t);if(w)for(a.handler&&(a=(l=a).handler,c=l.selector),c&&e.find.matchesSelector(n,c),a.guid||(a.guid=e.guid++),(d=w.events)||(d=w.events={}),(f=w.handle)||(f=w.handle=function(n){return void 0!==e&&e.event.triggered!==n.type?e.event.dispatch.apply(t,arguments):void 0}),h=(o=(o||"").match(r)||[""]).length;h--;)y=b=(p=u.exec(o[h])||[])[1],x=(p[2]||"").split(".").sort(),y&&(g=e.event.special[y]||{},y=(c?g.delegateType:g.bindType)||y,g=e.event.special[y]||{},v=e.extend({type:y,origType:b,data:s,handler:a,guid:a.guid,selector:c,needsContext:c&&e.expr.match.needsContext.test(c),namespace:x.join(".")},l),(m=d[y])||((m=d[y]=[]).delegateCount=0,g.setup&&!1!==g.setup.call(t,s,x,f)||t.addEventListener&&t.addEventListener(y,f)),g.add&&(g.add.call(t,v),v.handler.guid||(v.handler.guid=a.guid)),c?m.splice(m.delegateCount++,0,v):m.push(v),e.event.global[y]=!0)},remove:function(t,n,o,a,s){var c,l,f,p,d,h,v,g,m,y,x,b=i.hasData(t)&&i.get(t);if(b&&(p=b.events)){for(d=(n=(n||"").match(r)||[""]).length;d--;)if(m=x=(f=u.exec(n[d])||[])[1],y=(f[2]||"").split(".").sort(),m){for(v=e.event.special[m]||{},g=p[m=(a?v.delegateType:v.bindType)||m]||[],f=f[2]&&new RegExp("(^|\\.)"+y.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=c=g.length;c--;)h=g[c],!s&&x!==h.origType||o&&o.guid!==h.guid||f&&!f.test(h.namespace)||a&&a!==h.selector&&("**"!==a||!h.selector)||(g.splice(c,1),h.selector&&g.delegateCount--,v.remove&&v.remove.call(t,h));l&&!g.length&&(v.teardown&&!1!==v.teardown.call(t,y,b.handle)||e.removeEvent(t,m,b.handle),delete p[m])}else for(m in p)e.event.remove(t,m+n[d],o,a,!0);e.isEmptyObject(p)&&i.remove(t,"handle events")}},dispatch:function(t){var n,r,o,a,s,u,c=e.event.fix(t),l=new Array(arguments.length),f=(i.get(this,"events")||{})[c.type]||[],p=e.event.special[c.type]||{};for(l[0]=c,n=1;n-1:e.find(i,this,null,[c]).length),o[i]&&o.push(a);o.length&&s.push({elem:c,handlers:o})}return u=s&&(i!==r&&(c=void 0,l=[n]),o.rejectWith(c,l))}};t?p():(e.Deferred.getStackHook&&(p.stackTrace=e.Deferred.getStackHook()),window.setTimeout(p))}}return e.Deferred(function(s){o[0][3].add(u(0,s,e.isFunction(a)?a:n,s.notifyWith)),o[1][3].add(u(0,s,e.isFunction(t)?t:n)),o[2][3].add(u(0,s,e.isFunction(i)?i:r))}).promise()},promise:function(t){return null!=t?e.extend(t,a):a}},s={};return e.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[0][2].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),t&&t.call(s,s),s},when:function(n){var r=arguments.length,i=r,a=Array(i),s=t.call(arguments),u=e.Deferred(),c=function(e){return function(n){a[e]=this,s[e]=arguments.length>1?t.call(arguments):n,--r||u.resolveWith(a,s)}};if(r<=1&&(o(n,u.done(c(i)).resolve,u.reject),"pending"===u.state()||e.isFunction(s[i]&&s[i].then)))return u.then();for(;i--;)o(s[i],c(i),u.reject);return u.promise()}}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(28),n(93),n(92),n(54),n(3),n(52),n(2)],void 0===(o=function(e,t,n,r,o){"use strict";var i=/^(?:parents|prev(?:Until|All))/,a={children:!0,contents:!0,next:!0,prev:!0};function s(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}return e.fn.extend({has:function(t){var n=e(t,this),r=n.length;return this.filter(function(){for(var t=0;t-1:1===r.nodeType&&e.find.matchesSelector(r,t))){s.push(r);break}return this.pushStack(s.length>1?e.uniqueSort(s):s)},index:function(n){return n?"string"==typeof n?t.call(e(n),this[0]):t.call(this,n.jquery?n[0]:n):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,n){return this.pushStack(e.uniqueSort(e.merge(this.get(),e(t,n))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),e.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return n(e,"parentNode")},parentsUntil:function(e,t,r){return n(e,"parentNode",r)},next:function(e){return s(e,"nextSibling")},prev:function(e){return s(e,"previousSibling")},nextAll:function(e){return n(e,"nextSibling")},prevAll:function(e){return n(e,"previousSibling")},nextUntil:function(e,t,r){return n(e,"nextSibling",r)},prevUntil:function(e,t,r){return n(e,"previousSibling",r)},siblings:function(e){return r((e.parentNode||{}).firstChild,e)},children:function(e){return r(e.firstChild)},contents:function(t){return t.contentDocument||e.merge([],t.childNodes)}},function(t,n){e.fn[t]=function(r,o){var s=e.map(this,n,r);return"Until"!==t.slice(-5)&&(o=r),o&&"string"==typeof o&&(s=e.filter(o,s)),this.length>1&&(a[t]||e.uniqueSort(s),i.test(t)&&s.reverse()),this.pushStack(s)}}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(20),n(5),n(37),n(1),n(21),n(18),n(48),n(36),n(46),n(35),n(45),n(34),n(17),n(3),n(51),n(2)],void 0===(o=function(e,t,n,r,o,i,a,s,u,c,l,f,p,d){"use strict";var h=/^(none|table(?!-c[ea]).+)/,v={position:"absolute",visibility:"hidden",display:"block"},g={letterSpacing:"0",fontWeight:"400"},m=["Webkit","Moz","ms"],y=o.createElement("div").style;function x(e){if(e in y)return e;for(var t=e[0].toUpperCase()+e.slice(1),n=m.length;n--;)if((e=m[n]+t)in y)return e}function b(e,t,n){var r=i.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function w(t,n,r,o,i){for(var a=r===(o?"border":"content")?4:"width"===n?1:0,u=0;a<4;a+=2)"margin"===r&&(u+=e.css(t,r+s[a],!0,i)),o?("content"===r&&(u-=e.css(t,"padding"+s[a],!0,i)),"margin"!==r&&(u-=e.css(t,"border"+s[a]+"Width",!0,i))):(u+=e.css(t,"padding"+s[a],!0,i),"padding"!==r&&(u+=e.css(t,"border"+s[a]+"Width",!0,i)));return u}function C(t,n,r){var o,i=!0,s=u(t),c="border-box"===e.css(t,"boxSizing",!1,s);if(t.getClientRects().length&&(o=t.getBoundingClientRect()[n]),o<=0||null==o){if(((o=l(t,n,s))<0||null==o)&&(o=t.style[n]),a.test(o))return o;i=c&&(d.boxSizingReliable()||o===t.style[n]),o=parseFloat(o)||0}return o+w(t,n,r||(c?"border":"content"),i,s)+"px"}return e.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=l(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{float:"cssFloat"},style:function(t,n,r,o){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var a,s,u,c=e.camelCase(n),l=t.style;if(n=e.cssProps[c]||(e.cssProps[c]=x(c)||c),u=e.cssHooks[n]||e.cssHooks[c],void 0===r)return u&&"get"in u&&void 0!==(a=u.get(t,!1,o))?a:l[n];"string"===(s=typeof r)&&(a=i.exec(r))&&a[1]&&(r=f(t,n,a),s="number"),null!=r&&r==r&&("number"===s&&(r+=a&&a[3]||(e.cssNumber[c]?"":"px")),d.clearCloneStyle||""!==r||0!==n.indexOf("background")||(l[n]="inherit"),u&&"set"in u&&void 0===(r=u.set(t,r,o))||(l[n]=r))}},css:function(t,n,r,o){var i,a,s,u=e.camelCase(n);return n=e.cssProps[u]||(e.cssProps[u]=x(u)||u),(s=e.cssHooks[n]||e.cssHooks[u])&&"get"in s&&(i=s.get(t,!0,r)),void 0===i&&(i=l(t,n,o)),"normal"===i&&n in g&&(i=g[n]),""===r||r?(a=parseFloat(i),!0===r||isFinite(a)?a||0:i):i}}),e.each(["height","width"],function(t,n){e.cssHooks[n]={get:function(t,r,o){if(r)return!h.test(e.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?C(t,n,o):c(t,v,function(){return C(t,n,o)})},set:function(t,r,o){var a,s=o&&u(t),c=o&&w(t,n,o,"border-box"===e.css(t,"boxSizing",!1,s),s);return c&&(a=i.exec(r))&&"px"!==(a[3]||"px")&&(t.style[n]=r,r=e.css(t,n)),b(0,r,c)}}}),e.cssHooks.marginLeft=p(d.reliableMarginLeft,function(e,t){if(t)return(parseFloat(l(e,"marginLeft"))||e.getBoundingClientRect().left-c(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),e.each({margin:"",padding:"",border:"Width"},function(t,n){e.cssHooks[t+n]={expand:function(e){for(var r=0,o={},i="string"==typeof e?e.split(" "):[e];r<4;r++)o[t+s[r]+n]=i[r]||i[r-2]||i[0];return o}},r.test(t)||(e.cssHooks[t+n].set=b)}),e.fn.extend({css:function(t,r){return n(this,function(t,n,r){var o,i,a={},s=0;if(e.isArray(n)){for(o=u(t),i=n.length;s1)}}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return[]}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0),n(58),n(57),n(5),n(44),n(43),n(42),n(41),n(40),n(39),n(38),n(87),n(4),n(49),n(24),n(55),n(3),n(11),n(2),n(9)],void 0===(o=function(e,t,n,r,o,i,a,s,u,c,l,f,p,d,h,v){"use strict";var g=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,m=/\s*$/g;function w(t,n){return e.nodeName(t,"table")&&e.nodeName(11!==n.nodeType?n:n.firstChild,"tr")&&t.getElementsByTagName("tbody")[0]||t}function C(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function T(e){var t=x.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function S(t,n){var r,o,i,a,s,u,c,l;if(1===n.nodeType){if(p.hasData(t)&&(a=p.access(t),s=p.set(n,a),l=a.events))for(i in delete s.handle,s.events={},l)for(r=0,o=l[i].length;r1&&"string"==typeof k&&!f.checkClone&&y.test(k))return n.each(function(e){var t=n.eq(e);N&&(r[0]=k.call(this,e,t.html())),E(t,r,o,i)});if(w&&(c=(s=l(r,n[0].ownerDocument,!1,n,i)).firstChild,1===s.childNodes.length&&(s=c),c||i)){for(h=(d=e.map(u(s,"script"),C)).length;x")},clone:function(t,n,r){var o,i,a,s,l=t.cloneNode(!0),p=e.contains(t.ownerDocument,t);if(!(f.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||e.isXMLDoc(t)))for(s=u(l),o=0,i=(a=u(t)).length;o0&&c(s,!p&&u(t,"script")),l},cleanData:function(t){for(var n,r,o,i=e.event.special,a=0;void 0!==(r=t[a]);a++)if(h(r)){if(n=r[p.expando]){if(n.events)for(o in n.events)i[o]?e.event.remove(r,o):e.removeEvent(r,o,n.handle);r[p.expando]=void 0}r[d.expando]&&(r[d.expando]=void 0)}}}),e.fn.extend({detach:function(e){return N(this,e,!0)},remove:function(e){return N(this,e)},text:function(t){return r(this,function(t){return void 0===t?e.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return E(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||w(this,e).appendChild(e)})},prepend:function(){return E(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=w(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return E(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return E(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var t,n=0;null!=(t=this[n]);n++)1===t.nodeType&&(e.cleanData(u(t,!1)),t.textContent="");return this},clone:function(t,n){return t=null!=t&&t,n=null==n?t:n,this.map(function(){return e.clone(this,t,n)})},html:function(t){return r(this,function(t){var n=this[0]||{},r=0,o=this.length;if(void 0===t&&1===n.nodeType)return n.innerHTML;if("string"==typeof t&&!m.test(t)&&!s[(i.exec(t)||["",""])[1].toLowerCase()]){t=e.htmlPrefilter(t);try{for(;r-1&&(y=(x=y.split(".")).shift(),x.sort()),h=y.indexOf(":")<0&&"on"+y,(a=a[e.expando]?a:new e.Event(y,"object"==typeof a&&a)).isTrigger=c?2:3,a.namespace=x.join("."),a.rnamespace=a.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,a.result=void 0,a.target||(a.target=u),s=null==s?[a]:e.makeArray(s,[a]),g=e.event.special[y]||{},c||!g.trigger||!1!==g.trigger.apply(u,s))){if(!c&&!g.noBubble&&!e.isWindow(u)){for(d=g.delegateType||y,i.test(d+y)||(f=f.parentNode);f;f=f.parentNode)m.push(f),p=f;p===(u.ownerDocument||t)&&m.push(p.defaultView||p.parentWindow||window)}for(l=0;(f=m[l++])&&!a.isPropagationStopped();)a.type=l>1?d:g.bindType||y,(v=(n.get(f,"events")||{})[a.type]&&n.get(f,"handle"))&&v.apply(f,s),(v=h&&f[h])&&v.apply&&r(f)&&(a.result=v.apply(f,s),!1===a.result&&a.preventDefault());return a.type=y,c||a.isDefaultPrevented()||g._default&&!1!==g._default.apply(m.pop(),s)||!r(u)||h&&e.isFunction(u[y])&&!e.isWindow(u)&&((p=u[h])&&(u[h]=null),e.event.triggered=y,u[y](),e.event.triggered=void 0,p&&(u[h]=p)),a.result}},simulate:function(t,n,r){var o=e.extend(new e.Event,r,{type:t,isSimulated:!0});e.event.trigger(o,null,n)}}),e.fn.extend({trigger:function(t,n){return this.each(function(){e.event.trigger(t,n,this)})},triggerHandler:function(t,n){var r=this[0];if(r)return e.event.trigger(t,n,r,!0)}}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(1),n(7)],void 0===(o=function(e,t){"use strict";return function(){var n=e.createElement("input"),r=e.createElement("select").appendChild(e.createElement("option"));n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=r.selected,(n=e.createElement("input")).value="t",n.type="radio",t.radioValue="t"===n.value}(),t}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(1),n(19),n(7)],void 0===(o=function(e,t,n,r){"use strict";return function(){function o(){if(l){l.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",l.innerHTML="",n.appendChild(c);var e=window.getComputedStyle(l);i="1%"!==e.top,u="2px"===e.marginLeft,a="4px"===e.width,l.style.marginRight="50%",s="4px"===e.marginRight,n.removeChild(c),l=null}}var i,a,s,u,c=t.createElement("div"),l=t.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",r.clearCloneStyle="content-box"===l.style.backgroundClip,c.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",c.appendChild(l),e.extend(r,{pixelPosition:function(){return o(),i},boxSizingReliable:function(){return o(),a},pixelMarginRight:function(){return o(),s},reliableMarginLeft:function(){return o(),u}}))}(),r}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(20)],void 0===(o=function(e){"use strict";return new RegExp("^("+e+")(?!px)[a-z%]+$","i")}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(1)],void 0===(o=function(e){"use strict";return e.documentElement}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(20)],void 0===(o=function(e){"use strict";return new RegExp("^(?:([+-])=|)("+e+")([a-z%]*)$","i")}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(1),n(21),n(6),n(48),n(47),n(46),n(45),n(4),n(88),n(3),n(23),n(10),n(11),n(14),n(12),n(86)],void 0===(o=function(e,t,n,r,o,i,a,s,u,c){"use strict";var l,f,p=/^(?:toggle|show|hide)$/,d=/queueHooks$/;function h(){f&&(window.requestAnimationFrame(h),e.fx.tick())}function v(){return window.setTimeout(function(){l=void 0}),l=e.now()}function g(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=o[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function m(e,t,n){for(var r,o=(y.tweeners[t]||[]).concat(y.tweeners["*"]),i=0,a=o.length;i-1;)s.splice(r,1),r<=c&&c--}),this},has:function(t){return t?e.inArray(t,s)>-1:s.length>0},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=o="",this},disabled:function(){return!s},lock:function(){return a=u=[],o||r||(s=o=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),r||l()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!i}};return f},e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(27)],void 0===(o=function(e){"use strict";return e.hasOwnProperty}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return{}}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(13)],void 0===(o=function(e){"use strict";return e.indexOf}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(13)],void 0===(o=function(e){"use strict";return e.slice}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(44),n(3),n(11),n(33)],void 0===(o=function(e,t){"use strict";var n=/\[\]$/,r=/\r?\n/g,o=/^(?:submit|button|image|reset|file)$/i,i=/^(?:input|select|textarea|keygen)/i;function a(t,r,o,i){var s;if(e.isArray(r))e.each(r,function(e,r){o||n.test(t)?i(t,r):a(t+"["+("object"==typeof r&&null!=r?e:"")+"]",r,o,i)});else if(o||"object"!==e.type(r))i(t,r);else for(s in r)a(t+"["+s+"]",r[s],o,i)}return e.param=function(t,n){var r,o=[],i=function(t,n){var r=e.isFunction(n)?n():n;o[o.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==r?"":r)};if(e.isArray(t)||t.jquery&&!e.isPlainObject(t))e.each(t,function(){i(this.name,this.value)});else for(r in t)a(r,t[r],n,i);return o.join("&")},e.fn.extend({serialize:function(){return e.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=e.prop(this,"elements");return t?e.makeArray(t):this}).filter(function(){var n=this.type;return this.name&&!e(this).is(":disabled")&&i.test(this.nodeName)&&!o.test(n)&&(this.checked||!t.test(n))}).map(function(t,n){var o=e(this).val();return null==o?null:e.isArray(o)?e.map(o,function(e){return{name:n.name,value:e.replace(r,"\r\n")}}):{name:n.name,value:o.replace(r,"\r\n")}}).get()}}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/\?/}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0)],void 0===(o=function(e){"use strict";return e.now()}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(5),n(16),n(2)],void 0===(o=function(e,t,n){"use strict";var r=/^(?:input|select|textarea|button)$/i,o=/^(?:a|area)$/i;e.fn.extend({prop:function(n,r){return t(this,e.prop,n,r,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[e.propFix[t]||t]})}}),e.extend({prop:function(t,n,r){var o,i,a=t.nodeType;if(3!==a&&8!==a&&2!==a)return 1===a&&e.isXMLDoc(t)||(n=e.propFix[n]||n,i=e.propHooks[n]),void 0!==r?i&&"set"in i&&void 0!==(o=i.set(t,r,n))?o:t[n]=r:i&&"get"in i&&null!==(o=i.get(t,n))?o:t[n]},propHooks:{tabIndex:{get:function(t){var n=e.find.attr(t,"tabindex");return n?parseInt(n,10):r.test(t.nodeName)||o.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),n.optSelected||(e.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),e.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){e.propFix[this.toLowerCase()]=this})}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return function(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0),n(18),n(37),n(36),n(17),n(2)],void 0===(o=function(e,t,n,r,o){"use strict";return function(i,a,s){var u,c,l,f,p=i.style;return(s=s||r(i))&&(""!==(f=s.getPropertyValue(a)||s[a])||e.contains(i.ownerDocument,i)||(f=e.style(i,a)),!o.pixelMarginRight()&&t.test(f)&&n.test(a)&&(u=p.width,c=p.minWidth,l=p.maxWidth,p.minWidth=p.maxWidth=p.width=f,f=s.width,p.width=u,p.minWidth=c,p.maxWidth=l)),void 0!==f?f+"":f}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=window),t.getComputedStyle(e)}}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/^margin/}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0),n(43),n(42),n(41),n(40),n(39)],void 0===(o=function(e,t,n,r,o,i){"use strict";var a=/<|&#?\w+;/;return function(s,u,c,l,f){for(var p,d,h,v,g,m,y=u.createDocumentFragment(),x=[],b=0,w=s.length;b-1)f&&f.push(p);else if(g=e.contains(p.ownerDocument,p),d=o(y.appendChild(p),"script"),g&&i(d),c)for(m=0;p=d[m++];)n.test(p.type||"")&&c.push(p);return y}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(4)],void 0===(o=function(e){"use strict";return function(t,n){for(var r=0,o=t.length;r",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};return e.optgroup=e.option,e.tbody=e.tfoot=e.colgroup=e.caption=e.thead,e.th=e.td,e}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/^$|\/(?:java|ecma)script/i}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/<([a-z][^\/\0>\x20\t\r\n\f]+)/i}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/^(?:checkbox|radio)$/i}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0),n(21)],void 0===(o=function(e,t){"use strict";return function(n,r,o,i){var a,s=1,u=20,c=i?function(){return i.cur()}:function(){return e.css(n,r,"")},l=c(),f=o&&o[3]||(e.cssNumber[r]?"":"px"),p=(e.cssNumber[r]||"px"!==f&&+l)&&t.exec(e.css(n,r));if(p&&p[3]!==f){f=f||p[3],o=o||[],p=+l||1;do{p/=s=s||".5",e.style(n,r,p+f)}while(s!==(s=c()/l)&&1!==s&&--u)}return o&&(p=+p||+l||0,a=o[1]?p+(o[1]+1)*o[2]:+o[2],i&&(i.unit=f,i.start=p,i.end=a)),a}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return function(e,t,n,r){var o,i,a={};for(i in t)a[i]=e.style[i],e.style[i]=t[i];for(i in o=n.apply(e,r||[]),t)e.style[i]=a[i];return o}}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0),n(2)],void 0===(o=function(e){"use strict";return function(t,n){return"none"===(t=n||t).style.display||""===t.style.display&&e.contains(t.ownerDocument,t)&&"none"===e.css(t,"display")}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return["Top","Right","Bottom","Left"]}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(50)],void 0===(o=function(e){"use strict";return new e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(6),n(24)],void 0===(o=function(e,t,n){"use strict";function r(){this.expando=e.expando+r.uid++}return r.uid=1,r.prototype={cache:function(e){var t=e[this.expando];return t||(t={},n(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(t,n,r){var o,i=this.cache(t);if("string"==typeof n)i[e.camelCase(n)]=r;else for(o in n)i[e.camelCase(o)]=n[o];return i},get:function(t,n){return void 0===n?this.cache(t):t[this.expando]&&t[this.expando][e.camelCase(n)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(n,r){var o,i=n[this.expando];if(void 0!==i){if(void 0!==r){o=(r=e.isArray(r)?r.map(e.camelCase):(r=e.camelCase(r))in i?[r]:r.match(t)||[]).length;for(;o--;)delete i[r[o]]}(void 0===r||e.isEmptyObject(i))&&(n.nodeType?n[this.expando]=void 0:delete n[this.expando])}},hasData:function(t){var n=t[this.expando];return void 0!==n&&!e.isEmptyObject(n)}},r}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(1),n(10)],void 0===(o=function(e,t){"use strict";var n=e.Deferred();function r(){t.removeEventListener("DOMContentLoaded",r),window.removeEventListener("load",r),e.ready()}e.fn.ready=function(e){return n.then(e),this},e.extend({isReady:!1,readyWait:1,holdReady:function(t){t?e.readyWait++:e.ready(!0)},ready:function(r){(!0===r?--e.readyWait:e.isReady)||(e.isReady=!0,!0!==r&&--e.readyWait>0||n.resolveWith(t,[e]))}}),e.ready.then=n.then,"complete"===t.readyState||"loading"!==t.readyState&&!t.documentElement.doScroll?window.setTimeout(e.ready):(t.addEventListener("DOMContentLoaded",r),window.addEventListener("load",r))}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(28),n(54),n(2)],void 0===(o=function(e,t,n){"use strict";var r=/^.[^:#\[\.,]*$/;function o(n,o,i){if(e.isFunction(o))return e.grep(n,function(e,t){return!!o.call(e,t,e)!==i});if(o.nodeType)return e.grep(n,function(e){return e===o!==i});if("string"==typeof o){if(r.test(o))return e.filter(o,n,i);o=e.filter(o,n)}return e.grep(n,function(e){return t.call(o,e)>-1!==i&&1===e.nodeType})}e.filter=function(t,n,r){var o=n[0];return r&&(t=":not("+t+")"),1===n.length&&1===o.nodeType?e.find.matchesSelector(o,t)?[o]:[]:e.find.matches(t,e.grep(n,function(e){return 1===e.nodeType}))},e.fn.extend({find:function(t){var n,r,o=this.length,i=this;if("string"!=typeof t)return this.pushStack(e(t).filter(function(){for(n=0;n1?e.uniqueSort(r):r},filter:function(e){return this.pushStack(o(this,e||[],!1))},not:function(e){return this.pushStack(o(this,e||[],!0))},is:function(t){return!!o(this,"string"==typeof t&&n.test(t)?e(t):t||[],!1).length}})}.apply(t,r))||(e.exports=o)},function(e,t,n){var r;void 0===(r=function(){"use strict";return/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i}.call(t,n,t,e))||(e.exports=r)},function(e,t,n){var r,o;r=[n(0),n(2)],void 0===(o=function(e){"use strict";return e.expr.match.needsContext}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(1)],void 0===(o=function(e){"use strict";return function(t,n){var r=(n=n||e).createElement("script");r.text=t,n.head.appendChild(r).parentNode.removeChild(r)}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(26)],void 0===(o=function(e){"use strict";return e.toString}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(13)],void 0===(o=function(e){"use strict";return e.push}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(13)],void 0===(o=function(e){"use strict";return e.concat}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(2),n(11),n(25),n(10),n(91),n(51),n(90),n(23),n(89),n(85),n(9),n(81),n(80),n(14),n(78),n(75),n(12),n(74),n(30),n(8),n(73),n(72),n(71),n(70),n(67),n(22),n(66),n(65),n(64),n(63),n(62)],void 0===(o=function(e){"use strict";return window.jQuery=window.$=e}.apply(t,r))||(e.exports=o)},function(e,t,n){"use strict";var r=function(e,t,n,r){var o;if(o=r?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e),n){Array.isArray(n)||(n=[n]);for(var i=0;i6&&n<10)return(0|(t=e/1e6))===t||0===Math.round(e%1e6/1e5)?(0|t)+"M":(0|t)+"."+Math.round(e%1e6/1e5)+"M";if(n>5)return(0|(t=e/1e3))===t||0===Math.round(e%1e3/100)?(0|t)+"K":(0|t)+"."+Math.round(e%1e3/100)+"K";if(n>3){var r=e%1e3|0;return r<10?r="00"+r:r<100&&(r="0"+r),(e/1e3|0)+","+r}return(0|e)+""},Animate:{show:function(e,t){"undefined"===t&&(t=0),e.style.display="block",e.style.opacity=0,setTimeout(function(){e.style.opacity=1},t)},hide:function(e,t){window.getComputedStyle(e).opacity>0&&(void 0===t&&(t=500),e.style.opacity=0,setTimeout(function(){e.style.display="none"},t))}},ComponentProto:{attach:function(e){return this.node?(e.appendChild(this.node),this):null},remove:function(){return this.node&&this.node.parentNode?(this.node.parentNode.removeChild(this.node),this):null}}};e.exports=o},function(e,t,n){"use strict";var r,o,i,a,s,u=window,c={},l=function(){},f=function(e,t){if(function(e){return null===e.offsetParent}(e))return!1;var n=e.getBoundingClientRect();return n.right>=t.l&&n.bottom>=t.t&&n.left<=t.r&&n.top<=t.b},p=function(){!a&&o||(clearTimeout(o),o=setTimeout(function(){c.render(),o=null},i))};c.init=function(e){var t=(e=e||{}).offset||0,n=e.offsetVertical||t,o=e.offsetHorizontal||t,f=function(e,t){return parseInt(e||t,10)};r={t:f(e.offsetTop,n),b:f(e.offsetBottom,n),l:f(e.offsetLeft,o),r:f(e.offsetRight,o)},i=f(e.throttle,250),a=!1!==e.debounce,s=!!e.unload,l=e.callback||l,c.render(),document.addEventListener?(u.addEventListener("scroll",p,!1),u.addEventListener("load",p,!1)):(u.attachEvent("onscroll",p),u.attachEvent("onload",p))},c.render=function(e){for(var t,n,o=(e||document).querySelectorAll("[data-echo], [data-echo-background]"),i=o.length,a={l:0-r.l,t:0-r.t,b:(u.innerHeight||document.documentElement.clientHeight)+r.b,r:(u.innerWidth||document.documentElement.clientWidth)+r.r},p=0;p-1?(s=(o=f.position()).top,i=o.left):(s=parseFloat(a)||0,i=parseFloat(c)||0),e.isFunction(n)&&(n=n.call(t,r,e.extend({},u))),null!=n.top&&(p.top=n.top-u.top+s),null!=n.left&&(p.left=n.left-u.left+i),"using"in n?n.using.call(t,p):f.css(p)}},e.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(n){e.offset.setOffset(this,t,n)});var n,r,o,i,a=this[0];return a?a.getClientRects().length?(o=a.getBoundingClientRect()).width||o.height?(r=u(i=a.ownerDocument),n=i.documentElement,{top:o.top+r.pageYOffset-n.clientTop,left:o.left+r.pageXOffset-n.clientLeft}):o:{top:0,left:0}:void 0},position:function(){if(this[0]){var t,n,r=this[0],o={top:0,left:0};return"fixed"===e.css(r,"position")?n=r.getBoundingClientRect():(t=this.offsetParent(),n=this.offset(),e.nodeName(t[0],"html")||(o=t.offset()),o={top:o.top+e.css(t[0],"borderTopWidth",!0),left:o.left+e.css(t[0],"borderLeftWidth",!0)}),{top:n.top-o.top-e.css(r,"marginTop",!0),left:n.left-o.left-e.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent;t&&"static"===e.css(t,"position");)t=t.offsetParent;return t||r})}}),e.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(n,r){var o="pageYOffset"===r;e.fn[n]=function(e){return t(this,function(e,t,n){var i=u(e);if(void 0===n)return i?i[r]:e[t];i?i.scrollTo(o?i.pageXOffset:n,o?n:i.pageYOffset):e[t]=n},n,e,arguments.length)}}),e.each(["top","left"],function(t,n){e.cssHooks[n]=a(s.pixelPosition,function(t,r){if(r)return r=i(t,n),o.test(r)?e(t).position()[n]+"px":r})}),e}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(2),n(22)],void 0===(o=function(e){"use strict";e.expr.pseudos.animated=function(t){return e.grep(e.timers,function(e){return t===e.elem}).length}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(9)],void 0===(o=function(e){"use strict";e.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(t,n){e.fn[n]=function(e){return this.on(n,e)}})}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(1),n(7)],void 0===(o=function(e,t){"use strict";return t.createHTMLDocument=function(){var t=e.implementation.createHTMLDocument("").body;return t.innerHTML="
",2===t.childNodes.length}(),t}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(1),n(53),n(38),n(68)],void 0===(o=function(e,t,n,r,o){"use strict";return e.parseHTML=function(i,a,s){return"string"!=typeof i?[]:("boolean"==typeof a&&(s=a,a=!1),a||(o.createHTMLDocument?((u=(a=t.implementation.createHTMLDocument("")).createElement("base")).href=t.location.href,a.head.appendChild(u)):a=t),c=n.exec(i),l=!s&&[],c?[a.createElement(c[1])]:(c=r([i],a,l),l&&l.length&&e(l).remove(),e.merge([],c.childNodes)));var u,c,l},e.parseHTML}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(69),n(8),n(11),n(14),n(2)],void 0===(o=function(e){"use strict";e.fn.load=function(t,n,r){var o,i,a,s=this,u=t.indexOf(" ");return u>-1&&(o=e.trim(t.slice(u)),t=t.slice(0,u)),e.isFunction(n)?(r=n,n=void 0):n&&"object"==typeof n&&(i="POST"),s.length>0&&e.ajax({url:t,type:i||"GET",dataType:"html",data:n}).done(function(t){a=arguments,s.html(o?e("
").append(e.parseHTML(t)).find(o):t)}).always(r&&function(e,t){s.each(function(){r.apply(this,a||[e.responseText,t,e])})}),this}}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(32),n(31),n(8)],void 0===(o=function(e,t,n){"use strict";var r=[],o=/(=)\?(?=&|$)|\?\?/;e.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var n=r.pop()||e.expando+"_"+t++;return this[n]=!0,n}}),e.ajaxPrefilter("json jsonp",function(t,i,a){var s,u,c,l=!1!==t.jsonp&&(o.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&o.test(t.data)&&"data");if(l||"jsonp"===t.dataTypes[0])return s=t.jsonpCallback=e.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,l?t[l]=t[l].replace(o,"$1"+s):!1!==t.jsonp&&(t.url+=(n.test(t.url)?"&":"?")+t.jsonp+"="+s),t.converters["script json"]=function(){return c||e.error(s+" was not called"),c[0]},t.dataTypes[0]="json",u=window[s],window[s]=function(){c=arguments},a.always(function(){void 0===u?e(window).removeProp(s):window[s]=u,t[s]&&(t.jsonpCallback=i.jsonpCallback,r.push(s)),c&&e.isFunction(u)&&u(c[0]),c=u=void 0}),"script"})}.apply(t,r))||(e.exports=o)},function(e,t,n){var r,o;r=[n(0),n(1),n(8)],void 0===(o=function(e,t){"use strict";e.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),e.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return e.globalEval(t),t}}}),e.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),e.ajaxTransport("script",function(n){var r,o;if(n.crossDomain)return{send:function(i,a){r=e("