diff --git a/_includes/flutter/analytics.md b/_includes/flutter/analytics.md new file mode 100644 index 00000000..3bd9a7ac --- /dev/null +++ b/_includes/flutter/analytics.md @@ -0,0 +1,10 @@ +# Analytics + +Parse provides a number of hooks for you to get a glimpse into the ticking heart of your app. We understand that it's important to understand what your app is doing, how frequently, and when. + +While this section will cover different ways to instrument your app to best take advantage of Parse's analytics backend, developers using Parse to store and retrieve data can already take advantage of metrics on Parse. + +Without having to implement any client-side logic, you can view real-time graphs and breakdowns (by device type, Parse class name, or REST verb) of your API Requests in your app's dashboard and save these graph filters to quickly access just the data you're interested in. + + +## Custom Analytics diff --git a/_includes/flutter/config.md b/_includes/flutter/config.md new file mode 100644 index 00000000..fe908fe2 --- /dev/null +++ b/_includes/flutter/config.md @@ -0,0 +1,19 @@ +# Config + + +## Save & Update Config + + +## Retrieving Config + + + +## Internal Config + + + +## Current Config + + + +## Parameters diff --git a/_includes/flutter/files.md b/_includes/flutter/files.md new file mode 100644 index 00000000..f5769eeb --- /dev/null +++ b/_includes/flutter/files.md @@ -0,0 +1,29 @@ +# Files + +## Creating a Parse.File + + + +### Client Side + + +### Server Side + + +### Embedding files in other objects + + +## Retrieving File Contents + + + +## Deleting Files + + + +#### Parse Server <4.2.0 + + + +## Adding Metadata and Tags + diff --git a/_includes/flutter/futures.md b/_includes/flutter/futures.md new file mode 100644 index 00000000..81f9c9a9 --- /dev/null +++ b/_includes/flutter/futures.md @@ -0,0 +1 @@ +# Futures diff --git a/_includes/flutter/geopoints.md b/_includes/flutter/geopoints.md new file mode 100644 index 00000000..9c564c92 --- /dev/null +++ b/_includes/flutter/geopoints.md @@ -0,0 +1,13 @@ +# GeoPoints + + + +## ParseGeoPoint + + +## Geo Queries + + + +## Caveats + diff --git a/_includes/flutter/getting-started.md b/_includes/flutter/getting-started.md new file mode 100644 index 00000000..625408d9 --- /dev/null +++ b/_includes/flutter/getting-started.md @@ -0,0 +1,88 @@ +# Getting Started + +To use the parse in your project flutter, start adding the dependency in your project: + +You can run flutter command directly in your terminal: + +```bash +flutter pub add parse_server_sdk_flutter +``` + +Then this will add a line like this to your package's pubspec.yaml (and run an implicit `flutter pub get`): + +```yaml +dependencies: + parse_server_sdk_flutter: ^3.1.0 + +``` +Alternatively, your editor might support `flutter pub get`. Check the docs for your editor to learn more. + + +## Initializing the Parse SDK + +```dart +import 'package:parse_server_sdk_flutter/generated/i18n.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'dart:async'; +import 'package:flutter/material.dart'; + +void main() async { + final keyParseApplicationId = 'YOUR_APP_ID_HERE'; + final keyParseClientKey = 'YOUR_CLIENT_KEY_HERE'; + final keyParseServerUrl = 'http://YOUR_PARSE_SERVER:1337/parse'; + await Parse().initialize(keyParseApplicationId, keyParseServerUrl, + clientKey: keyParseClientKey); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Welcome to Flutter using Parse SDK', + home: Scaffold( + appBar: AppBar( + title: const Text('Welcome to Flutter using Parse SDK'), + ), + body: const Center( + child: Text('Hello World'), + ), + ), + ); + } +} +``` +Note that you must import the library `dart:async` to use o Parse. + +⚠️ If you need to use the master key, note please note that the master key should only be used in safe environments and never on the client-side: + +```dart + await Parse().initialize( + //... + masterKey: 'YOUR_MASTERKEY'); +``` + +## Web support + +Due to Cross-origin resource sharing (CORS) restrictions, this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. +When running directly via docker, set the env var `PARSE_SERVER_ALLOW_HEADERS=X-Parse-Installation-Id`. +When running via express, set [ParseServerOptions](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `allowHeaders: ['X-Parse-Installation-Id']`. + +## Network client + +By default, this SDK uses the `ParseHTTPClient`. +Another option is use `ParseDioClient`. This client supports the most features (for example a progress callback at the file upload), but a benchmark has shown, that dio is slower than http on web. + +If you want to use the `ParseDioClient`, which uses the dio network library, +you can provide a custom `ParseClientCreator` at the initialization of the SDK. +```dart +await Parse().initialize( + //... + clientCreator: ({bool? sendSessionId, SecurityContext? securityContext}) => ParseDioClient(sendSessionId: sendSessionId, securityContext: securityContext), +); +``` + + + diff --git a/_includes/flutter/handling-errors.md b/_includes/flutter/handling-errors.md new file mode 100644 index 00000000..aa143831 --- /dev/null +++ b/_includes/flutter/handling-errors.md @@ -0,0 +1 @@ +# Error Handling \ No newline at end of file diff --git a/_includes/flutter/live-queries.md b/_includes/flutter/live-queries.md new file mode 100644 index 00000000..1b3657ae --- /dev/null +++ b/_includes/flutter/live-queries.md @@ -0,0 +1,114 @@ +# Live Queries + +## Standard API +As we discussed in our [LiveQuery protocol](https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification), we maintain a WebSocket connection to communicate with the Parse LiveQuery server. When used server side we use the [`ws`](https://www.npmjs.com/package/ws) package and in the browser we use [`window.WebSocket`](https://developer.mozilla.org/en-US{{ site.baseUrl }}/Web/API/WebSockets_API). We think in most cases it isn't necessary to deal with the WebSocket connection directly. Thus, we developed a simple API to let you focus on your own business logic. + +## Create a subscription + +```dart +final query = QueryBuilder(ParseObject('Game')); +final LiveQuery liveQuery = LiveQuery(); +Subscription subscription = await liveQuery.client.subscribe(query); +``` + +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 +We define several types of events you'll get through a subscription object: + +### Open event + +Event not available on Flutter SDK. + +### Create event + +```dart +subscription.on(LiveQueryEvent.create, (value) { + print('object created'); +}); +``` + +When a new `ParseObject` is created and it fulfills the `QueryBuilder` you subscribe, you'll get this event. The `object` is the `ParseObject` which was created. + +### Update event + +```dart +subscription.on(LiveQueryEvent.update, (value) { + print('object updated'); +}); +``` + +When an existing `ParseObject` which fulfills the `QueryBuilder` you subscribe is updated (The `ParseObject` fulfills the `QueryBuilder` before and after changes), you'll get this event. The `object` is the `ParseObject` which was updated. Its content is the latest value of the `ParseObject`. + +### Enter event + +```dart +subscription.on(LiveQueryEvent.enter, (value) { + print('object entered'); +}); +``` + +When an existing `ParseObject`'s old value does not fulfill the `QueryBuilder` but its new value fulfills the `QueryBuilder`, you'll get this event. The `object` is the `ParseObject` which enters the `QueryBuilder`. Its content is the latest value of the `ParseObject`. + +### Leave event + +```dart +subscription.on(LiveQueryEvent.leave, (value) { + print('object left'); +}); +``` + +When an existing `ParseObject`'s old value fulfills the `QueryBuilder` but its new value doesn't fulfill the `QueryBuilder`, you'll get this event. The `object` is the `ParseObject` which leaves the `QueryBuilder`. Its content is the latest value of the `ParseObject`. + +### Delete event + +```dart +subscription.on(LiveQueryEvent.delete, (value) { + print('object deleted'); +}); +``` + +When an existing `ParseObject` which fulfills the `QueryBuilder` is deleted, you'll get this event. The `object` is the `ParseObject` which is deleted. + +### Close event + +Function not available on Flutter SDK. + +## Unsubscribe + +```dart +liveQuery.client.unSubscribe(subscription); +``` + +If you would like to stop receiving events from a `QueryBuilder`, you can just unsubscribe the `subscription`. After that, you won't get any events from the `subscription` object. + + +## Close + +Function not available on Flutter SDK. + + +## Setup server URL + +``` +Parse().initialize( + keyApplicationId, + keyParseServerUrl, + clientKey: keyParseClientKey, + debug: true, + liveQueryUrl: keyLiveQueryUrl, + autoSendSessionId: true); +``` + + +### Error event + +```dart +subscription.on(LiveQueryEvent.delete, (value) { + print('object deleted'); +}); +``` + +When some network error or LiveQuery server error happens, you'll get this event. + +*** diff --git a/_includes/flutter/local-datastore.md b/_includes/flutter/local-datastore.md new file mode 100644 index 00000000..6f5b7071 --- /dev/null +++ b/_includes/flutter/local-datastore.md @@ -0,0 +1,34 @@ +# Local Datastore + + + +## Pinning + + +## Retrieving + + + +## Querying the Local Datastore + + + +## Unpinning + + + +## Pinning with Labels + + + +## Caching Query Results + + + +## Dumping Contents + + + +## LiveQuery + + diff --git a/_includes/flutter/objects.md b/_includes/flutter/objects.md new file mode 100644 index 00000000..8dae729d --- /dev/null +++ b/_includes/flutter/objects.md @@ -0,0 +1,299 @@ +# Objects + +## ParseObject + +Storing data on Parse is built around the ``ParseObject``. Each `ParseObject` contains key-value pairs of JSON-compatible data. This data is schemaless, which means that you don’t need to specify ahead of time what keys exist on each `ParseObject`. You simply set whatever key-value pairs you want, and our backend will store it. + +For example, let’s say you’re tracking high scores for a game. A single `ParseObject` could contain: + +```dart +score: 1337, playerName: 'Sean Plott', cheatMode: false +``` + +Keys must be alphanumeric strings. Values can be strings, numbers, booleans, or even arrays and dictionaries - anything that can be JSON-encoded. + +Each `ParseObject` is an instance of a specific subclass with a class name that you can use to distinguish different sorts of data. For example, we could call the high score object a `GameScore`. We recommend that you NameYourClassesLikeThis and nameYourKeysLikeThis, just to keep your code looking pretty. + + +## Saving Objects + +Let’s say you want to save the `GameScore` described above to your Parse Server. The interface is similar to a common class calling the method generic, passing the key and the value, including the `save` method: + +```dart +var gameScore = ParseObject('GameScore') + ..set('score', 1337) + ..set('playerName', 'Sean Plott') + ..set('cheatMode', false); +var response = await gameScore.save(); +if (response.success) { + // The object was saved successfully. +} else { + // The save failed. +} +``` + +After this code runs, you will probably be wondering if anything 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: + +```json +objectId: "xWMyZ4YEGZ", score: 1337, playerName: "Sean Plott", cheatMode: false, +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 cloud. Each of these fields is filled in by Parse, so they don't exist on a `ParseObject` until a save operation has been completed. + + +## Retrieving Objects + +Saving data to the cloud is fun, but it's even more fun to get that data out again. If the `ParseObject` has been uploaded to the server, you can use the `objectId` to get it using a `ParseObject`: + +```dart +var gameScoreResponse = await ParseObject('GameScore').getObject('xWMyZ4YEGZ'); +if (gameScoreResponse.success) { + // The object was retrieved successfully. + var gameScore = gameScoreResponse.result; +} else { + // The object was not retrieved successfully. + // The error is in a property called exception of response with an error code and message. +} +``` + +To get the values out of the `ParseObject`, use the `get` method. + +```dart +final score = gameScore.get('score'); +final playerName = gameScore.get('playerName'); +final cheatMode = gameScore.get('cheatMode'); +``` + +The four special reserved values are provided as properties and cannot be retrieved using the 'get' method nor modified with the 'set' method: + +```dart +final objectId = gameScore.objectId; +final updatedAt = gameScore.updatedAt; +final createdAt = gameScore.createdAt; +final acl = gameScore.getACL(); +``` + +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: + +```dart +await myObject.fetch(); +``` + +## Updating Objects + +Updating an object is simple. Just set some new data on it and call the save method. For example: + +```dart +// Create the object. +var gameScore = ParseObject('DietPlan') + ..set('score', 1337) + ..set('playerName', 'Sean Plott') + ..set('cheatMode', false) + ..setAdd('skills', ['pwnage', 'flying']); +final gameScoreResponse = await gameScore.save(); +gameScore = gameScoreResponse.result; + +// Now let's update it with some new data. In this case, only cheatMode and score +// will get sent to the cloud. playerName hasn't changed. +gameScore.set('cheatMode', true); +gameScore.set('score', 1338); +await gameScore.save(); +``` + +Parse automatically figures out which data has changed so only "dirty" fields will be sent to the Parse Cloud. You don't need to worry about squashing data that you didn't intend to update. + +### Counters + +The above example contains a common use case. The "score" field is a counter that we'll need to continually update with the player's latest score. Using the above method works but it's cumbersome and can lead to problems if you have multiple clients trying to update the same counter. + +To help with storing counter-type data, Parse provides methods that atomically increment (or decrement) any number field. So, the same update can be rewritten as: + +Retrieve it, call: +```dart +await gameScore.increment('score', 1); +``` +or using with save function: + +```dart +gameScore.setIncrement('score', 1); +var response = await gameScore.save() +``` + +### Arrays + +To help with storing array data, three operations can be used to atomically change an array associated with a given key: + +* `setAdd` or `add` append the given object to the end of an array field. +* `setAddUnique` or `addUnique` add the given object only if it isn't already contained in an array field. The position of the insert is not guaranteed. +* `setRemove` or `remove` remove all instances of the given object from an array field. + +For example, we can add items to the set-like "skills" field like so: + +```dart +gameScore.setAddUnique('skills', ['flying', 'kungfu']); +await gameScore.save(); +``` +You can use directly without the method `save`: + +```dart +await gameScore.addUnique('skills', ['flying', 'kungfu']); +``` + +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. + +## Destroying Objects + +To delete an object from the cloud: + +```dart +await myObject.delete(); +``` + +You can delete a single field from an object with the `unset` method: + +```dart +// After this, the playerName field will be empty +await myObject.unset('playerName'); + +// If object is not saved remotely, set offlineOnly to true to avoid api calls. +myObject.unset('playerName', offlineOnly: true); +``` + +## Relational Data + +Objects may have relationships with other objects. For example, in a blogging application, a `Post` object may have many `Comment` objects. Parse supports all kinds of relationships, including one-to-one, one-to-many, and many-to-many. + +### One-to-One and One-to-Many Relationships + +One-to-one and one-to-many relationships are modeled by saving a `ParseObject` as a value in the other object. For example, each `Comment` in a blogging app might correspond to one `Post`. + +To create a new `Post` with a single `Comment`, you could write: + +```dart +// Create the post +var myPost = ParseObject('Post') + ..set('title', 'I\'m Hungry') + ..set('content', 'Where should we go for lunch?'); + +// Create the comment +var myComment = ParseObject('Comment') + ..set('content', 'Let\'s do Sushirrito.'); + +// Add the post as a value in the comment +myComment.set('parent', myPost); + +// This will save both myPost and myComment +await myComment.save(); +``` + +Internally, the Parse framework will store the referred-to object in just one place, to maintain consistency. You can also link objects using just their `objectId`s like so: + +```dart +var post = ParseObject('Post') + ..objectId = '1zEcyElZ80'; +myComment.set('parent', post); +``` + +By default, when fetching an object, related `ParseObject`s are not fetched. These objects' values cannot be retrieved until they have been fetched like so: + +```dart +final post = fetchedComment.get('parent'); +await post.fetch(); +final title = post.get('title'); +``` + +### Many-to-Many Relationships + +Many-to-many relationships are modeled using `ParseRelation`. This works similar to storing an array of `ParseObject`s in a key, except that you don't need to fetch all of the objects in a relation at once. In addition, this allows `ParseRelation` to scale to many more objects than the array of `ParseObject` approach. For example, a `User` may have many `Posts` that she might like. In this case, you can store the set of `Posts` that a `User` likes using `relation`. To add a `Post` to the "likes" list of the `User`, you can do: + +```dart +final user = ParseUser.currentUser(); +final relation = user.getRelation('likes'); +relation.add(post); +await user.save(); +``` + +You can remove a post from a `ParseRelation`: + +```dart +relation.remove(post); +user.save(); +``` + +You can call `add` and `remove` multiple times before calling save: + +```dart +relation.remove(post1); +relation.remove(post2); +await user.save(); +``` + +You can also pass in an array of `ParseObject` to `add` and `remove`: + +```dart +user.addRelation('likes', [post1, post2, post3]); +user.save(); +``` + +By default, the list of objects in this relation is not downloaded. You can get a list of the posts that a user likes by using the `QueryBuilder` returned by `query`. The code looks like this: + +```dart +await relation.getQuery().query(); +``` + +If you want only a subset of the Posts, you can add extra constraints to the `QueryBuilder` returned by query like this: + +```dart +final queryBuilder = relation.getQuery(); +queryBuilder.whereEqualTo('title', 'I\'m Hungry'); +var response = await queryBuilder.query(); +``` + +For more details on `QueryBuilder`, please look at the query portion of this guide. A `ParseRelation` behaves similar to an array of `ParseObject` for querying purposes, so any query you can do on an array of objects, you can do on a `ParseRelation`. + +## Data Types + +So far we've used values with type `String`, `int`, and `ParseObject`. Parse also supports `DateTime` and `null`. You can nest `Map`s and `List` to store more structured data within a single `ParseObject`. Overall, the following types are allowed for each field in your object: + +* String => `String` +* Number => `int, double` +* Bool => `bool` +* Array => `List` +* Object => `Map` +* Date => `DateTime` +* File => `ParseFile` +* Pointer => other `ParseObject` +* GeoPoint => `ParseGeoPoint` +* Relation => `ParseRelation` +* Null => `null` + +Some examples: + +```dart +final number = 42; +final boolean = false; +final stringExample = 'the number is ' + number; +final date = DateTime.now(); +final array = [stringExample, number]; +final object = { number: number, string: stringExample }; +final pointer = ParseObject('MyClassName') + ..objectId = objectId; +final bigObject = ParseObject('BigObject') + ..set('myNumber', number); + ..set('myBool', boolean); + ..set('myString', stringExample); + ..set('myDate', date); + ..set('myArray', array); + ..set('myObject', object); + ..set('anyKey', null); // this value can only be saved to an existing key + ..set('myPointerKey', pointer); // shows up as Pointer <MyClassName> in the Data Browser +await bigObject.save(); +``` + +We do not recommend storing large pieces of binary data like images or documents on `ParseObject`. We recommend you use `ParseFile`s to store images, documents, and other types of files. You can do so by instantiating a `ParseFile` 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). diff --git a/_includes/flutter/push-notifications.md b/_includes/flutter/push-notifications.md new file mode 100644 index 00000000..88f638d8 --- /dev/null +++ b/_includes/flutter/push-notifications.md @@ -0,0 +1,2 @@ +# Push Notifications + diff --git a/_includes/flutter/queries.md b/_includes/flutter/queries.md new file mode 100644 index 00000000..41cd90dd --- /dev/null +++ b/_includes/flutter/queries.md @@ -0,0 +1,352 @@ +# Queries + +We've already seen how a `ParseObject` with `getObject` can retrieve a single `ParseObject` from Parse. There are many other ways to retrieve data with `QueryBuilder` - you can retrieve many objects at once, put conditions on the objects you wish to retrieve, and more. + +## Basic Queries + +In many cases, `getObject` isn't powerful enough to specify which objects you want to retrieve. `QueryBuilder` offers different ways to retrieve a list of objects rather than just a single object. + +The general pattern is to create a `QueryBuilder`, put conditions on it, and then retrieve an `List` of matching `ParseObject`s using `query`. For example, to retrieve the scores that have a particular `playerName`, use the `whereEqualTo` method to constrain the value for a key. + +```dart +final gameScore = ParseObject('GameScore'); +final query = QueryBuilder(gameScore) + ..whereEqualTo("playerName", "Dan Stemkoski"); +final resultsFuture = await query.query(); +final results = resultsFuture.results ?? []; +print("${"Successfully retrieved " + results.length} scores."); +// Do something with the returned Parse.Object values +for (int i = 0; i < results.length; i++) { + ParseObject object = results[i]; + print('${object.objectId} - ${object.get('playerName')}'); +} +``` + +## Query Constraints +There are several ways to put constraints on the objects found by a `QueryBuilder`. You can filter out objects with a particular key-value pair with `whereNotEqualTo`: + +```dart +query.whereNotEqualTo("playerName", "Michael Yabuti"); +``` + +You can give multiple constraints, and objects will only be in the results if they match all of the constraints. In other words, it's like an AND of constraints. + +```dart +query.whereNotEqualTo("playerName", "Michael Yabuti"); +query.whereGreaterThan("playerAge", 18); +``` + +You can limit the number of results by setting `setLimit`. By default, results are limited to 100. In the old Parse hosted backend, the maximum limit was 1,000, but Parse Server removed that constraint: + +```dart +query.setLimit(10); // limit to at most 10 results +``` + +If you want exactly one result, a more convenient alternative may be to use `first` instead of using `find`. + +```dart +final gameScore = ParseObject('GameScore'); +final query = QueryBuilder(gameScore); +query.whereEqualTo("playerEmail", "dstemkoski@example.com"); +final resultObject = await query.first(); +``` + +You can skip the first results by setting `setAmountToSkip`. In the old Parse hosted backend, the maximum skip value was 10,000, but Parse Server removed that constraint. This can be useful for pagination: + +```dart +query.setAmountToSkip(10); // skip the first 10 results +``` + +For sortable types like numbers and strings, you can control the order in which results are returned: + +```dart +// Sorts the results in ascending order by the score field +query.orderByAscending("score"); + +// Sorts the results in descending order by the score field +query.orderByDescending("score"); +``` + +For sortable types, you can also use comparisons in queries: + +```dart +// Restricts to wins < 50 +query.whereLessThan("wins", 50); + +// Restricts to wins <= 50 +query.whereLessThanOrEqualTo("wins", 50); + +// Restricts to wins > 50 +query.whereGreaterThan("wins", 50); + +// Restricts to wins >= 50 +query.whereGreaterThanOrEqualsTo("wins", 50); +``` + +If you want to retrieve objects matching any of the values in a list of values, you can use `whereContainedIn`, providing an array of acceptable values. This is often useful to replace multiple queries with a single query. For example, if you want to retrieve scores made by any player in a particular list: + +```dart +// Finds scores from any of Jonathan, Dario, or Shawn +query.whereContainedIn("playerName", + ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]); +``` + +If you want to retrieve objects that do not match any of several values you can use `whereNotContainedIn`, providing an array of acceptable values. For example if you want to retrieve scores from players besides those in a list: + +```dart +// Finds scores from anyone who is neither Jonathan, Dario, nor Shawn +query.whereNotContainedIn("playerName", + ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]); +``` + +If you want to retrieve objects that have a particular key set, you can use `whereValueExists` with parameter boolean as true. Conversely, if you want to retrieve objects without a particular key set, you can use `whereValueExists` with parameter boolean as false. + +```dart +// Finds objects that have the score set +query.whereValueExists("score", true); + +// Finds objects that don't have the score set +query.whereValueExists("score", false); +``` + +You can use the `whereMatchesKeyInQuery` method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user's hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like: + +```dart +final team = ParseObject('Team'); +final teamQuery = QueryBuilder(team); +teamQuery.whereGreaterThan("winPct", 0.5); +final userQuery = QueryBuilder(ParseUser.forQuery()); +userQuery.whereMatchesKeyInQuery("hometown", "city", teamQuery); +// results has the list of users with a hometown team with a winning record +final resultsFuture = await userQuery.find(); +``` + +Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use `whereDoesNotMatchKeyInQuery`. For example, to find users whose hometown teams have losing records: + +```dart +final losingUserQuery = QueryBuilder(ParseUser.forQuery()); +losingUserQuery.whereDoesNotMatchKeyInQuery("hometown", "city", teamQuery); +// results has the list of users with a hometown team with a losing record +final resultsFuture = await losingUserQuery.find(); +``` + +To filter rows based on `objectId`'s from pointers in a second table, you can use dot notation: + +```dart +final rolesOfTypeX = QueryBuilder(ParseObject('Role')); +rolesOfTypeX.whereEqualTo('type', 'x'); + +final groupsWithRoleX = QueryBuilder(ParseObject('Group')); +groupsWithRoleX.whereMatchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); +groupsWithRoleX.find().then((results) { + // results has the list of groups with role x +}); +``` + +You can restrict the fields returned by calling `keysToReturn` 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`): + +```dart +final query = QueryBuilder(ParseObject("GameScore")); +query.keysToReturn(["score", "playerName"]); +query.find().then((results) { + // each of results will only have the selected fields available. +}); +``` + +Similarly, use `excludeKeys` to remove undesired fields while retrieving the rest: + +```dart +final query = QueryBuilder(ParseObject("GameScore")); +query.excludeKeys(["playerName"]); +query.find().then((results) { + // Now each result will have all fields except `playerName` +}); +``` + + +The remaining fields can be fetched later by calling `fetch` on the returned objects: + +```dart +query.first().then((result) { + // only the selected fields of the object will now be available here. + return result?.fetch(); +}).then((result) { + // all fields of the object will now be available here. +}); +``` +## Queries on Array Values + +For keys with an array type, you can find objects where the key's array value contains 2 by: + +```dart +// Find objects where the array in arrayKey contains 2. +query.whereEqualTo("arrayKey", 2); +``` + +You can also find objects where the key's array value contains each of the elements 2, 3, and 4 with the following: + +```dart +// Find objects where the array in arrayKey contains all of the elements 2, 3, and 4. +query.whereArrayContainsAll("arrayKey", [2, 3, 4]); +``` + +## Queries on String Values + +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: + +```dart +// Finds barbecue sauces that start with "Big Daddy's". +final query = QueryBuilder(ParseObject("BarbecueSauce")); +query.whereStartsWith("name", "Big Daddy's"); +``` +To case-sensitive, pass a boolean in the third parameter. + +```dart +// Finds barbecue sauces that start with "Big Daddy's". +var query = QueryBuilder(ParseObject("BarbecueSauce")); +// Third parameter for case-sensitive +query.whereStartsWith("name", "Big Daddy's", caseSensitive: true); +``` + +The above example will match any `BarbecueSauce` objects where the value in the "name" String key starts with "Big Daddy's". For example, both "Big Daddy's" and "Big Daddy's BBQ" will match, but "big daddy's" or "BBQ Sauce: Big Daddy's" will not. + +Queries that have regular expression constraints are very expensive, especially for classes with over 100,000 records. Parse restricts how many such operations can be run on a particular app at any given time. + +## Relational Queries + +There are several ways to issue queries for relational data. If you want to retrieve objects where a field matches a particular `ParseObject`, you can use `whereEqualTo` just like for other data types. For example, if each `Comment` has a `Post` object in its `post` field, you can fetch comments for a particular `Post`: + +```dart +// Assume Parse.Object myPost was previously created. +final query = QueryBuilder(ParseObject("Comment")) + ..whereEqualTo("post", myPost); +// comments now contains the comments for myPost +var comments = await query.find(); +``` + +If you want to retrieve objects where a field contains a `ParseObject` that matches a different query, you can use `whereMatchesQuery`. In order to find comments for posts containing images, you can do: + +```dart +final innerQuery = QueryBuilder(ParseObject("Post")); +innerQuery.whereValueExists("image", true); +final query = QueryBuilder(ParseObject("Comment")); +query.whereMatchesQuery("post", innerQuery); +// comments now contains the comments for posts with images. +var comments = await query.find(); +``` + +If you want to retrieve objects where a field contains a `Parse.Object` that does not match a different query, you can use `doesNotMatchQuery`. In order to find comments for posts without images, you can do: + +```dart +final innerQuery = QueryBuilder(ParseObject("Post")); +innerQuery.whereValueExists("image", true); +final query = QueryBuilder(ParseObject("Comment")); +query.whereDoesNotMatchQuery("post", innerQuery); +// comments now contains the comments for posts with images. +var comments = await query.find(); +``` + +You can also do relational queries by `objectId`: + +```dart +var post = ParseObject('Post'); +post.objectId = '1zEcyElZ80'; +query.whereEqualTo("post", post); +``` + +In some situations, you want to return multiple types of related objects in one query. You can do this with the `include` method. For example, let's say you are retrieving the last ten comments, and you want to retrieve their related posts at the same time: + +```dart +final query = QueryBuilder(ParseObject("Comment")); + +// Retrieve the most recent ones +query.orderByDescending("createdAt"); + +// Only retrieve the last ten +query.setLimit(10); + +// Include the post data with each comment +query.includeObject(["post"]); + +// Comments now contains the last ten comments, and the "post" field +var comments = await query.find(); + +// has been populated. For example: +for (var i = 0; i < comments.length; i++) { + // This does not require a network access. + var post = comments[i].getObject("post"); +} +``` + +You can also do multi level includes using dot notation. If you wanted to include the post for a comment and the post's author as well you can do: + +```dart +query.includeObject(['post.author']); +``` + +You can issue a query with multiple fields included by calling `includeObject` multiple times. This functionality also works with QueryBuilder helper like `first`. + +## 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. + +If you just need to count how many objects match a query, but you do not need to retrieve all the objects that match, you can use `count` instead of `find`. For example, to count how many games have been played by a particular player: + +```dart +final query = QueryBuilder(ParseObject("GameScore")); +query.whereEqualTo("playerName", "Sean Plott"); +final countResponse = await query.count(); +print("${"Sean has played " + countResponse.count.toString()} games"); +``` + +## Compound Queries + +For more complex queries you might need compound queries. A compound query is a logical combination (e. g. "and" or "or") of sub queries. + +Note that we do not support GeoPoint or non-filtering constraints (e.g. `whereNear`, `whereWithinGeoBox`, `setLimit`, `setAmountToSkip`, `orderByAscending`/`orderByDescending`, `includeObject`) in the subqueries of the compound query. + + +### OR-ed query constraints + +If you want to find objects that match one of several queries, you can use `Parse.Query.or` method to construct a query that is an OR of the queries passed in. For instance if you want to find players who either have a lot of wins or a few wins, you can do: + +```dart +final lotsOfWins = QueryBuilder(ParseObject("Player")); +lotsOfWins.whereGreaterThan("wins", 150); + +final fewWins = QueryBuilder(ParseObject("Player")); +fewWins.whereLessThan("wins", 5); + +final mainQuery = QueryBuilder.or(ParseObject("Player"), [lotsOfWins, fewWins]); +mainQuery.find().then((results) => { + // results contains a list of players that either have won a lot of games or won only a few games. +}).catchError((error) { + // There was an error. +}); +``` + + +### AND-ed query constraints +Function not available on Flutter SDK. + +> Alternative is use Cloud Code + +## Aggregate + +Function not available on Flutter SDK. + +> Alternative is use Cloud Code + +## Distinct + +Function not available on Flutter SDK. + +> Alternative is use Cloud Code + + + +## Read Preference + +Function not available on Flutter SDK. + +> Alternative is use Cloud Code diff --git a/_includes/flutter/roles.md b/_includes/flutter/roles.md new file mode 100644 index 00000000..373e1525 --- /dev/null +++ b/_includes/flutter/roles.md @@ -0,0 +1,2 @@ +# Roles + diff --git a/_includes/flutter/schema.md b/_includes/flutter/schema.md new file mode 100644 index 00000000..cc470005 --- /dev/null +++ b/_includes/flutter/schema.md @@ -0,0 +1,2 @@ +# Schema + diff --git a/_includes/flutter/users.md b/_includes/flutter/users.md new file mode 100644 index 00000000..9d621466 --- /dev/null +++ b/_includes/flutter/users.md @@ -0,0 +1,162 @@ +# Users + +At the core of many apps, there is a notion of user accounts that lets users access their information in a secure manner. We provide a specialized user class called `ParseUser` that automatically handles much of the functionality required for user account management. + +With this class, you'll be able to add user account functionality in your app. + +`ParseUser` is a subclass of `ParseObject`, and has all the same features, such as flexible schema, automatic persistence, and a key value interface. All the methods that are on `ParseObject` also exist in `ParseUser`. The difference is that `ParseUser` has some special additions specific to user accounts. + +## `ParseUser` Properties + +`ParseUser` has several values that set it apart from `ParseObject`: + +* username: The username for the user (required). +* password: The password for the user (required on signup). +* emailAddress: The email address for the user (optional). + +We'll go through each of these in detail as we run through the various use cases for users. + +## Signing Up + +The first thing your app will do is probably ask the user to sign up. The following code illustrates a typical sign up: + +```dart +var user = ParseUser("my name", "my pass", "email@example.com"); +// other fields can be set just like with Parse.Object +user.set('phone', '415-392-0202'); +var result = await user.signUp(); +if (result.success) { + // success signup +} else { + var code = result.error?.code.toString() ?? ""; + var message = result.error?.message ?? ""; + print("${"Error: $code"} $message"); +} +``` + +This call will asynchronously create a new user in your Parse App. Before it does this, it also checks to make sure that both the username and email are unique. Also, it securely hashes the password in the cloud using bcrypt. We never store passwords in plaintext, nor will we ever transmit passwords back to the client in plaintext. + +Note that we used the `signUp` method, not the `save` method. New `ParseUser`s should always be created using the `signUp` method. Subsequent updates to a user can be done by calling `save`. + +If a signup isn't successful, you should read the error object that is returned. The most likely case is that the username or email has already been taken by another user. You should clearly communicate this to your users, and ask them try a different username. + +You are free to use an email address as the username. Simply ask your users to enter their email, but fill it in the username property — `ParseUser` will work as normal. We'll go over how this is handled in the reset password section. + +## Logging In + +Of course, after you allow users to sign up, you need to let them log in to their account in the future. To do this, you can use the class method `login`. + +```dart +final ParseUser user = ParseUser("myname", "mypass", "email@example.com"); +var resultLogin = await user.login(doNotSendInstallationID: false); +if(resultLogin.success) { + // Do stuff after successful login. +} +``` + +Set `doNotSendInstallationID` to `true` in order to prevent the SDK from sending the installationID to the Server. +This option is especially useful if you are running you application on web and you don't have permission to set `X-Parse-Installation-Id` as an allowed header on your parse-server. + + +## Verifying Emails + +Enabling email verification in an application's settings allows the application to reserve part of its experience for users with confirmed email addresses. Email verification adds the `emailVerified` key to the `Parse.User` object. When a `Parse.User`'s `email` is set or modified, `emailVerified` is set to `false`. Parse then emails the user a link which will set `emailVerified` to `true`. + +There are three `emailVerified` states to consider: + +1. `true` - the user confirmed his or her email address by clicking on the link Parse emailed them. `Parse.Users` can never have a `true` value when the user account is first created. +2. `false` - at the time the `Parse.User` object was last refreshed, the user had not confirmed his or her email address. If `emailVerified` is `false`, consider calling `fetch` on the `ParseUser`. +3. _missing_ - the `ParseUser` was created when email verification was off or the `ParseUser` does not have an `email`. + +## Current User + +It would be bothersome if the user had to log in every time they open your app. You can avoid this by using the cached current `ParseUser` object. + +Whenever you use any signup or login methods, the user is cached in localStorage, or in any storage you configured via the `Parse.setAsyncStorage` method. You can treat this cache as a session, and automatically assume the user is logged in: + +```dart +final ParseUser currentUser = await ParseUser.currentUser(); +if (currentUser) { + // do stuff with the user +} else { + // show the signup or login page +} +``` + +You can clear the current user by logging them out: + +```dart +final ParseUser user = await ParseUser.currentUser(); +user.logout(); +``` + +## Setting the Current User + +If you’ve created your own authentication routines, or otherwise logged in a user on the server side, you can now pass the session token to the client and use the `getCurrentUserFromServer` method. This method will ensure the session token is valid before setting the current user. + +```dart +ParseUser.getCurrentUserFromServer("session-token-here").then((response) { +// The current user is now set to user. + final user = response?.result; +}).catchError((error) { + // The token could not be validated. +}); +``` + +## Security For User Objects + + + +## Encrypting Current User + + + +## Security For Other Objects + + + +## Resetting Passwords + + + +## Querying + + + +## Associations + + + +## Facebook Users + + + +### Setting up Facebook + + + +### Login & Signup + + + + +### Linking + + + +### Facebook SDK and Parse + + + +## Linking Users + + +### Signing Up and Logging In + + + +#### Linking un-authenticated users + + +### Custom Authentication Module + diff --git a/flutter.md b/flutter.md new file mode 100644 index 00000000..8262e352 --- /dev/null +++ b/flutter.md @@ -0,0 +1,33 @@ +--- +title: Flutter Developers Guide | Parse +permalink: /flutter/guide/ +layout: guide +platform: flutter +language: dart +display_platform: Flutter +api_reference: https://github.com/parse-community/Parse-SDK-Flutter + +sections: +- "flutter/getting-started.md" +- "flutter/objects.md" +- "flutter/queries.md" +- "flutter/live-queries.md" +- "flutter/users.md" +- "common/sessions.md" +- "flutter/roles.md" +- "flutter/files.md" +- "flutter/futures.md" +- "flutter/geopoints.md" +- "flutter/local-datastore.md" +- "flutter/push-notifications.md" +- "flutter/config.md" +- "flutter/analytics.md" +- "common/data.md" +- "common/relations.md" +- "flutter/schema.md" +- "flutter/handling-errors.md" +- "common/security.md" +- "common/performance.md" +- "common/errors.md" + +--- diff --git a/index.md b/index.md index 48d94d53..353e1e53 100644 --- a/index.md +++ b/index.md @@ -79,6 +79,19 @@ layout: docs Latest Downloads +
+
+ Flutter + +
+ + +