diff --git a/CHANGELOG.md b/CHANGELOG.md index e7aa829a3..14398f151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ +## 1.0.27 +User login / signUp / loginAnonymous delete SessionId stored in device before calling server + ## 1.0.26 +LiveList +Bug fixes +Sembast update ## 1.0.25 Update dependencies diff --git a/README.md b/README.md index 9ed4ae52c..e30959fd3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Parse For Flutter! Hi, this is a Flutter plugin that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). -This is a work in project and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project (Even if it is just to improve our documentation. +This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) ## Join in! Want to get involved? Join our Slack channel and help out! (http://flutter-parse-sdk.slack.com) @@ -13,7 +13,7 @@ Want to get involved? Join our Slack channel and help out! (http://flutter-parse To install, either add to your pubspec.yaml ```yml dependencies: - parse_server_sdk: ^1.0.26 + parse_server_sdk: ^1.0.27 ``` or clone this repository and add to your project. As this is an early development with multiple contributors, it is probably best to download/clone and keep updating as an when a new feature is added. @@ -49,6 +49,15 @@ It's possible to add other parameters to work with your instance of Parse Server coreStore: await CoreStoreSharedPrefsImp.getInstance()); // Local data storage method. Will use SharedPreferences instead of Sembast as an internal DB ``` + +#### Early Web support +Currently 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']`. + +Be aware that for web ParseInstallation does include app name, version or package identifier. + + ## Objects You can create custom objects by calling: ```dart @@ -98,7 +107,7 @@ The features available are:- * Array Operators ## Custom Objects -You can create your own ParseObjects or convert your existing objects into Parse Objects by doing the following: +You can create your own `ParseObjects` or convert your existing objects into Parse Objects by doing the following: ```dart class DietPlan extends ParseObject implements ParseCloneable { @@ -119,6 +128,26 @@ class DietPlan extends ParseObject implements ParseCloneable { ``` +When receiving an `ParseObject` from the SDK, you can often provide an instance of your custom object as an copy object. +To always use your custom object class, you can register your subclass at the initialization of the SDK. +```dart +Parse().initialize( + ..., + registeredSubClassMap: { + 'Diet_Plans': () => DietPlan(), + }, + parseUserConstructor: (username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress), +); +``` +Additionally you can register `SubClasses` after the initialization of the SDK. +```dart +ParseCoreData().registerSubClass('Diet_Plans', () => DietPlan()); +ParseCoreData().registerUserSubClass((username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress)); +``` +Providing a `ParseObject` as described above should still work, even if you have registered a different `SubClass`. + +For custom file classes have a lock at [here](#File). + ## Add new values to objects To add a variable to an object call and retrieve it, call @@ -128,7 +157,7 @@ var randomInt = dietPlan.get('RandomInt'); ``` ## Save objects using pins -You can now save an object by calling .pin() on an instance of an object +You can now save an object by calling `.pin()` on an instance of an object ```dart dietPlan.pin(); @@ -229,6 +258,26 @@ if (response.success) { } ``` +if you want to find objects that match one of several queries, you can use __QueryBuilder.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 +ParseObject playerObject = ParseObject("Player"); + +QueryBuilder lotsOfWins = + QueryBuilder(playerObject)) + ..whereGreaterThan('wins', 50); + +QueryBuilder fewWins = + QueryBuilder(playerObject) + ..whereLessThan('wins', 5); + +QueryBuilder mainQuery = QueryBuilder.or( + playerObject, + [lotsOfWins, fewWins], + ); + +var apiResponse = await mainQuery.query(); +``` + The features available are:- * Equals * Contains @@ -245,6 +294,10 @@ The features available are:- * WithinKilometers * WithinRadians * WithinGeoBox + * MatchesQuery + * DoesNotMatchQuery + * MatchesKeyInQuery + * DoesNotMatchKeyInQuery * Regex * Order * Limit @@ -288,6 +341,47 @@ QueryBuilder queryComment = var apiResponse = await queryComment.query(); ``` +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 +QueryBuilder teamQuery = + QueryBuilder(ParseObject('Team')) + ..whereGreaterThan('winPct', 0.5); + +QueryBuilder userQuery = + QueryBuilderParseUser.forQuery()) + ..whereMatchesKeyInQuery('hometown', 'city', teamQuery); + +var apiResponse = await userQuery.query(); +``` + +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 +QueryBuilder teamQuery = + QueryBuilder(ParseObject('Team')) + ..whereGreaterThan('winPct', 0.5); + +QueryBuilder losingUserQuery = + QueryBuilderParseUser.forQuery()) + ..whereDoesNotMatchKeyInQuery('hometown', 'city', teamQuery); + +var apiResponse = await losingUserQuery.query(); +``` + +To filter rows based on objectId’s from pointers in a second table, you can use dot notation: +```dart +QueryBuilder rolesOfTypeX = + QueryBuilder(ParseObject('Role')) + ..whereEqualTo('type', 'x'); + +QueryBuilder groupsWithRoleX = + QueryBuilder(ParseObject('Group'))) + ..whereMatchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); + +var apiResponse = await groupsWithRoleX.query(); +``` + ## Counting Objects If you only care about the number of games played by a particular player: @@ -430,10 +524,17 @@ LiveQuery server. liveQuery.client.unSubscribe(subscription); ``` +__Disconnection__ +In case the client's connection to the server breaks, +LiveQuery will automatically try to reconnect. +LiveQuery will wait at increasing intervals between reconnection attempts. +By default, these intervals are set to `[0, 500, 1000, 2000, 5000, 10000]` for mobile and `[0, 500, 1000, 2000, 5000]` for web. +You can change these by providing a custom list using the `liveListRetryIntervals` parameter at `Parse.initialize()` ("-1" means "do not try to reconnect"). + ## ParseLiveList ParseLiveList makes implementing a dynamic List as simple as possible. - +#### General Use It ships with the ParseLiveList class itself, this class manages all elements of the list, sorts them, keeps itself up to date and Notifies you on changes. @@ -451,13 +552,13 @@ ParseLiveListWidget( query: query, reverse: false, childBuilder: - (BuildContext context, bool failed, ParseObject loadedData) { - if (failed) { + (BuildContext context, ParseLiveListElementSnapshot snapshot) { + if (snapshot.failed) { return const Text('something went wrong!'); - } else if (loadedData != null) { + } else if (snapshot.hasData) { return ListTile( title: Text( - loadedData.get("text"), + snapshot.loadedData.get("text"), ), ); } else { @@ -487,8 +588,17 @@ ParseLiveListWidget( duration: Duration(seconds: 1), ); ``` +### included Sub-Objects +By default, ParseLiveQuery will provide you with all the objects you included in your Query like this: +```dart +queryBuilder.includeObject(/*List of all the included sub-objects*/); +``` +ParseLiveList will not listen for updates on this objects by default. +To activate listening for updates on all included objects, add `listenOnAllSubItems: true` to your ParseLiveListWidgets constructor. +If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const [/*all the included sub-objects*/]` instead. +Just as QueryBuilder, ParseLiveList supports nested sub-objects too. -Note: To use this features you have to enable [Live Queries](#live-queries) first. +**NOTE:** To use this features you have to enable [Live Queries](#live-queries) first. ## Users You can create and control users just as normal using this SDK. @@ -566,6 +676,24 @@ Other user features are:- } ``` +For Google and the example below, we used the library provided at https://pub.dev/packages/google_sign_in + +``` +class OAuthLogin { + final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: ['email', 'https://www.googleapis.com/auth/contacts.readonly'] ); + + sigInGoogle() async { + GoogleSignInAccount account = await _googleSignIn.signIn(); + GoogleSignInAuthentication authentication = await account.authentication; + await ParseUser.loginWith( + 'google', + google(_googleSignIn.currentUser.id, + authentication.accessToken, + authentication.idToken)); + } +} +``` + ## Security for Objects - ParseACL For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. To support this type of security, each object has an access control list, implemented by the __ParseACL__ class. @@ -690,11 +818,61 @@ QueryBuilder query = ..whereRelatedTo('fruits', 'DietPlan', DietPlan.objectId); ``` +## File +There are three different file classes in this SDK: +- `ParseFileBase` is and abstract class and is the foundation of every file class that can be handled by this SDK. +- `ParseFile` (former the only file class in the SDK) extends ParseFileBase and is by default used as the file class on every platform but web. +This class uses a `File` from `dart:io` for storing the raw file. +- `ParseWebFile` is the equivalent to ParseFile used at Flutter Web. +This class uses an `Uint8List` for storing the raw file. + +These classes are used by default to represent files, but you can also build your own class extending ParseFileBase and provide a custom `ParseFileConstructor` similar to the `SubClasses`. + +Have a look at the example application for a small (non web) example. + + +```dart +//A short example for showing an image from a ParseFileBase +Widget buildImage(ParseFileBase image){ + return FutureBuilder( + future: image.download(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (kIsWeb) { + return Image.memory((snapshot.data as ParseWebFile).file); + } else { + return Image.file((snapshot.data as ParseFile).file); + } + } else { + return CircularProgressIndicator(); + } + }, + ); +} +``` +```dart +//A short example for storing a selected picture +//libraries: image_picker (https://pub.dev/packages/image_picker), image_picker_for_web (https://pub.dev/packages/image_picker_for_web) +PickedFile pickedFile = await ImagePicker().getImage(source: ImageSource.gallery); +ParseFileBase parseFile; +if (kIsWeb) { + //Seems weird, but this lets you get the data from the selected file as an Uint8List very easily. + ParseWebFile file = ParseWebFile(null, name: null, url: pickedFile.path); + await file.download(); + parseFile = ParseWebFile(file.file, name: file.name); +} else { + parseFile = ParseFile(File(pickedFile.path)); +} +someParseObject.set("image", parseFile); +//This saves the ParseObject as well as all of its children, and the ParseFileBase is such a child. +await someParseObject.save(); +``` + ## Other Features of this library Main: * Installation (View the example application) * GeoPoints (View the example application) -* Files (View the example application) * Persistent storage * Debug Mode - Logging API calls * Manage Session ID's tokens diff --git a/example/lib/data/base/api_error.dart b/example/lib/data/base/api_error.dart index ccb11e256..02f72f2b4 100644 --- a/example/lib/data/base/api_error.dart +++ b/example/lib/data/base/api_error.dart @@ -1,8 +1,8 @@ class ApiError { - ApiError(this.code, this.message, this.isTypeOfException, this.type); + ApiError(this.code, this.message, this.exception, this.type); final int code; final String message; - final bool isTypeOfException; + final Exception exception; final String type; } diff --git a/example/lib/data/base/api_response.dart b/example/lib/data/base/api_response.dart index 41e4067ab..5bb328dca 100644 --- a/example/lib/data/base/api_response.dart +++ b/example/lib/data/base/api_response.dart @@ -25,6 +25,6 @@ ApiError getApiError(ParseError response) { return null; } - return ApiError(response.code, response.message, response.isTypeOfException, + return ApiError(response.code, response.message, response.exception, response.type); } diff --git a/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart b/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart index 6915520c0..4528ead5f 100644 --- a/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart +++ b/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart @@ -135,15 +135,13 @@ class DietPlanProviderDB implements DietPlanProviderContract { final Map values = convertItemToStorageMap(item); final Finder finder = Finder(filter: Filter.equals('objectId', item.objectId)); - final int returnedCount = - await _store.update(_db, values, finder: finder); + final int returnedCount = await _store.update(_db, values, finder: finder); if (returnedCount == 0) { return add(item); } - return ApiResponse( - true, 200, [item], null); + return ApiResponse(true, 200, [item], null); } Map convertItemToStorageMap(DietPlan item) { @@ -171,6 +169,6 @@ class DietPlanProviderDB implements DietPlanProviderContract { } } - static ApiError error = ApiError(1, 'No records found', false, ''); + static ApiError error = ApiError(1, 'No records found', null, ''); ApiResponse errorResponse = ApiResponse(false, 1, null, error); } diff --git a/example/lib/data/repositories/user/provider_db_user.dart b/example/lib/data/repositories/user/provider_db_user.dart index dff056796..be2a615bb 100644 --- a/example/lib/data/repositories/user/provider_db_user.dart +++ b/example/lib/data/repositories/user/provider_db_user.dart @@ -102,6 +102,6 @@ class UserProviderDB implements UserProviderContract { } } - static ApiError error = ApiError(1, 'No records found', false, ''); + static ApiError error = ApiError(1, 'No records found', null, ''); ApiResponse errorResponse = ApiResponse(false, 1, null, error); } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 7bc9ea95d..4515e9f7f 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -41,7 +41,7 @@ class _HomePageState extends State { title: const Text('Parse Server demo'), actions: [ FlatButton( - child: Text('Logout', + child: const Text('Logout', style: TextStyle(fontSize: 17.0, color: Colors.white)), onPressed: () async { final ParseUser user = await ParseUser.currentUser(); diff --git a/example/lib/pages/login_page.dart b/example/lib/pages/login_page.dart index c027dd575..54d3fc84e 100644 --- a/example/lib/pages/login_page.dart +++ b/example/lib/pages/login_page.dart @@ -145,7 +145,7 @@ class _LoginPageState extends State { if (_errorMessage.isNotEmpty && _errorMessage != null) { return Text( _errorMessage, - style: TextStyle( + style: const TextStyle( fontSize: 13.0, color: Colors.red, height: 1.0, @@ -215,9 +215,9 @@ class _LoginPageState extends State { Widget _showSecondaryButton() { return FlatButton( child: _formMode == FormMode.LOGIN - ? Text('Create an account', + ? const Text('Create an account', style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)) - : Text('Have an account? Sign in', + : const Text('Have an account? Sign in', style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)), onPressed: _formMode == FormMode.LOGIN ? _changeFormToSignUp @@ -236,9 +236,9 @@ class _LoginPageState extends State { borderRadius: BorderRadius.circular(30.0)), color: Colors.blue, child: _formMode == FormMode.LOGIN - ? Text('Login', + ? const Text('Login', style: TextStyle(fontSize: 20.0, color: Colors.white)) - : Text('Create account', + : const Text('Create account', style: TextStyle(fontSize: 20.0, color: Colors.white)), onPressed: _validateAndSubmit, ), diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart b/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart index e12877cbf..a0f95f909 100644 --- a/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart +++ b/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart @@ -101,9 +101,8 @@ void main() { final ApiResponse baseResponse = await repository.add(dummy); final ApiResponse responseWithResult = await repository .getNewerThan(DateTime.now().subtract(const Duration(days: 1))); - final ApiResponse responseWithoutResult = - await repository.getNewerThan( - DateTime.now().add(const Duration(days: 1))); + final ApiResponse responseWithoutResult = await repository + .getNewerThan(DateTime.now().add(const Duration(days: 1))); // CLEAR FROM DB await deleteFromApi(baseResponse.results); diff --git a/example_livelist/lib/main.dart b/example_livelist/lib/main.dart index bc537c2fc..14b569689 100644 --- a/example_livelist/lib/main.dart +++ b/example_livelist/lib/main.dart @@ -172,7 +172,7 @@ class _ObjectFormState extends State { ], ), trailing: IconButton( - icon: Icon(Icons.save), + icon: const Icon(Icons.save), onPressed: () { setState(() { _formKey.currentState.save(); diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index c8950d096..606fd131c 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:devicelocale/devicelocale.dart'; import 'package:http/http.dart'; @@ -20,80 +21,48 @@ import 'package:uuid/uuid.dart'; import 'package:xxtea/xxtea.dart'; export 'src/network/parse_live_query.dart' -if (dart.library.js) 'src/network/parse_live_query_web.dart'; + if (dart.library.js) 'src/network/parse_live_query_web.dart'; export 'src/utils/parse_live_list.dart'; +part 'package:parse_server_sdk/src/data/core_store.dart'; +part 'package:parse_server_sdk/src/data/parse_subclass_handler.dart'; part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; - part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; - part 'package:parse_server_sdk/src/objects/response/parse_response_builder.dart'; - part 'package:parse_server_sdk/src/objects/response/parse_response_utils.dart'; - part 'package:parse_server_sdk/src/objects/response/parse_success_no_results.dart'; - -part 'package:parse_server_sdk/src/data/core_store.dart'; - part 'package:parse_server_sdk/src/storage/core_store_sem_impl.dart'; - part 'package:parse_server_sdk/src/storage/core_store_sp_impl.dart'; - part 'package:parse_server_sdk/src/storage/xxtea_codec.dart'; - part 'src/base/parse_constants.dart'; - part 'src/data/parse_core_data.dart'; - part 'src/enums/parse_enum_api_rq.dart'; - part 'src/network/parse_http_client.dart'; - part 'src/network/parse_query.dart'; - part 'src/objects/parse_acl.dart'; - part 'src/objects/parse_base.dart'; - part 'src/objects/parse_cloneable.dart'; - part 'src/objects/parse_config.dart'; - part 'src/objects/parse_error.dart'; - part 'src/objects/parse_file.dart'; - +part 'src/objects/parse_file_base.dart'; +part 'src/objects/parse_file_web.dart'; part 'src/objects/parse_function.dart'; - part 'src/objects/parse_geo_point.dart'; - part 'src/objects/parse_installation.dart'; - +part 'src/objects/parse_merge.dart'; part 'src/objects/parse_object.dart'; - part 'src/objects/parse_relation.dart'; - part 'src/objects/parse_response.dart'; - part 'src/objects/parse_session.dart'; - part 'src/objects/parse_user.dart'; - part 'src/utils/parse_date_format.dart'; - part 'src/utils/parse_decoder.dart'; - part 'src/utils/parse_encoder.dart'; - part 'src/utils/parse_file_extensions.dart'; - part 'src/utils/parse_logger.dart'; - -part 'src/utils/parse_utils.dart'; - part 'src/utils/parse_login_helpers.dart'; - -part 'src/objects/parse_merge.dart'; +part 'src/utils/parse_utils.dart'; class Parse { ParseCoreData data; @@ -111,28 +80,42 @@ class Parse { // debug: true, // liveQuery: true); // ``` - Future initialize(String appId, String serverUrl, - {bool debug = false, - String appName = '', - String liveQueryUrl, - String clientKey, - String masterKey, - String sessionId, - bool autoSendSessionId, - SecurityContext securityContext, - CoreStore coreStore}) async { + Future initialize( + String appId, + String serverUrl, { + bool debug = false, + String appName = '', + String liveQueryUrl, + String clientKey, + String masterKey, + String sessionId, + bool autoSendSessionId, + SecurityContext securityContext, + CoreStore coreStore, + Map registeredSubClassMap, + ParseUserConstructor parseUserConstructor, + ParseFileConstructor parseFileConstructor, + List liveListRetryIntervals, + }) async { final String url = removeTrailingSlash(serverUrl); - await ParseCoreData.init(appId, url, - debug: debug, - appName: appName, - liveQueryUrl: liveQueryUrl, - masterKey: masterKey, - clientKey: clientKey, - sessionId: sessionId, - autoSendSessionId: autoSendSessionId, - securityContext: securityContext, - store: coreStore); + await ParseCoreData.init( + appId, + url, + debug: debug, + appName: appName, + liveQueryUrl: liveQueryUrl, + masterKey: masterKey, + clientKey: clientKey, + sessionId: sessionId, + autoSendSessionId: autoSendSessionId, + securityContext: securityContext, + store: coreStore, + registeredSubClassMap: registeredSubClassMap, + parseUserConstructor: parseUserConstructor, + parseFileConstructor: parseFileConstructor, + liveListRetryIntervals: liveListRetryIntervals, + ); _hasBeenInitialized = true; @@ -150,7 +133,7 @@ class Parse { final ParseHTTPClient _client = client ?? ParseHTTPClient( sendSessionId: - sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, + sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, securityContext: ParseCoreData().securityContext); const String className = 'parseBase'; diff --git a/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index 0105ee5e0..56a3d9962 100644 --- a/lib/src/base/parse_constants.dart +++ b/lib/src/base/parse_constants.dart @@ -1,7 +1,7 @@ part of flutter_parse_sdk; // Library -const String keySdkVersion = '1.0.26'; +const String keySdkVersion = '1.0.27'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points @@ -39,6 +39,7 @@ const String keyClassInstallation = '_Installation'; const String keyGeoPoint = 'GeoPoint'; const String keyFile = 'File'; const String keyRelation = 'Relation'; +const String keyFileClassname = 'ParseFile'; // Headers const String keyHeaderSessionToken = 'X-Parse-Session-Token'; @@ -80,3 +81,5 @@ const String keyVarInstallationId = 'installationId'; // Error const String keyError = 'error'; const String keyCode = 'code'; + +const bool parseIsWeb = identical(0, 0.0); diff --git a/lib/src/data/parse_core_data.dart b/lib/src/data/parse_core_data.dart index 7ffdaa7ce..02b37c2af 100644 --- a/lib/src/data/parse_core_data.dart +++ b/lib/src/data/parse_core_data.dart @@ -14,16 +14,23 @@ class ParseCoreData { /// /// This class should not be user unless switching servers during the app, /// which is odd. Should only be user by Parse.init - static Future init(String appId, String serverUrl, - {bool debug, - String appName, - String liveQueryUrl, - String masterKey, - String clientKey, - String sessionId, - bool autoSendSessionId, - SecurityContext securityContext, - CoreStore store}) async { + static Future init( + String appId, + String serverUrl, { + bool debug, + String appName, + String liveQueryUrl, + String masterKey, + String clientKey, + String sessionId, + bool autoSendSessionId, + SecurityContext securityContext, + CoreStore store, + Map registeredSubClassMap, + ParseUserConstructor parseUserConstructor, + ParseFileConstructor parseFileConstructor, + List liveListRetryIntervals, + }) async { _instance = ParseCoreData._init(appId, serverUrl); _instance.storage ??= @@ -53,6 +60,19 @@ class ParseCoreData { if (securityContext != null) { _instance.securityContext = securityContext; } + if (liveListRetryIntervals != null) { + _instance.liveListRetryIntervals = liveListRetryIntervals; + } else { + _instance.liveListRetryIntervals = parseIsWeb + ? [0, 500, 1000, 2000, 5000] + : [0, 500, 1000, 2000, 5000, 10000]; + } + + _instance._subClassHandler = ParseSubClassHandler( + registeredSubClassMap: registeredSubClassMap, + parseUserConstructor: parseUserConstructor, + parseFileConstructor: parseFileConstructor, + ); } String appName; @@ -66,6 +86,35 @@ class ParseCoreData { SecurityContext securityContext; bool debug; CoreStore storage; + ParseSubClassHandler _subClassHandler; + List liveListRetryIntervals; + + void registerSubClass( + String className, ParseObjectConstructor objectConstructor) { + _subClassHandler.registerSubClass(className, objectConstructor); + } + + void registerUserSubClass(ParseUserConstructor parseUserConstructor) { + _subClassHandler.registerUserSubClass(parseUserConstructor); + } + + void registerFileSubClass(ParseFileConstructor parseFileConstructor) { + _subClassHandler.registerFileSubClass(parseFileConstructor); + } + + ParseObject createObject(String classname) { + return _subClassHandler.createObject(classname); + } + + ParseUser createParseUser( + String username, String password, String emailAddress, + {String sessionToken, bool debug, ParseHTTPClient client}) { + return _subClassHandler.createParseUser(username, password, emailAddress, + sessionToken: sessionToken, debug: debug, client: client); + } + + ParseFileBase createFile({String url, String name}) => + _subClassHandler.createFile(name: name, url: url); /// Sets the current sessionId. /// diff --git a/lib/src/data/parse_subclass_handler.dart b/lib/src/data/parse_subclass_handler.dart new file mode 100644 index 000000000..c1e4abf27 --- /dev/null +++ b/lib/src/data/parse_subclass_handler.dart @@ -0,0 +1,71 @@ +part of flutter_parse_sdk; + +typedef ParseObjectConstructor = ParseObject Function(); +typedef ParseUserConstructor = ParseUser Function( + String username, String password, String emailAddress, + {String sessionToken, bool debug, ParseHTTPClient client}); +typedef ParseFileConstructor = ParseFileBase Function( + {String name, String url}); + +class ParseSubClassHandler { + ParseSubClassHandler( + {Map registeredSubClassMap, + ParseUserConstructor parseUserConstructor, + ParseFileConstructor parseFileConstructor}) { + _subClassMap = + registeredSubClassMap ?? Map(); + _parseUserConstructor = parseUserConstructor; + if (parseFileConstructor != null) + _parseFileConstructor = parseFileConstructor; + } + + Map _subClassMap; + ParseUserConstructor _parseUserConstructor; + ParseFileConstructor _parseFileConstructor = ({String name, String url}) { + if (parseIsWeb) { + return ParseWebFile(null, name: name, url: url); + } else { + return ParseFile(null, name: name, url: url); + } + }; + + void registerSubClass( + String className, ParseObjectConstructor objectConstructor) { + if (className != keyClassUser && + className != keyClassInstallation && + className != keyClassSession && + className != keyFileClassname) + _subClassMap[className] = objectConstructor; + } + + void registerUserSubClass(ParseUserConstructor parseUserConstructor) { + _parseUserConstructor = parseUserConstructor; + } + + void registerFileSubClass(ParseFileConstructor parseFileConstructor) { + _parseFileConstructor = parseFileConstructor; + } + + ParseObject createObject(String classname) { + if (classname == keyClassUser) { + return createParseUser(null, null, null); + } + if (_subClassMap.containsKey(classname)) { + return _subClassMap[classname](); + } + return ParseObject(classname); + } + + ParseUser createParseUser( + String username, String password, String emailAddress, + {String sessionToken, bool debug, ParseHTTPClient client}) { + return _parseUserConstructor != null + ? _parseUserConstructor(username, password, emailAddress, + sessionToken: sessionToken, debug: debug, client: client) + : ParseUser(username, password, emailAddress, + sessionToken: sessionToken, debug: debug, client: client); + } + + ParseFileBase createFile({String name, String url}) => + _parseFileConstructor(name: name, url: url); +} diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 6a34af0c0..ca90110c0 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -76,8 +76,7 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); } - // -1 means "do not try to reconnect", - static const List retryInterval = [0, 500, 1000, 2000, 5000, 10000]; + static List get retryInterval => ParseCoreData().liveListRetryIntervals; static const String DEBUG_TAG = 'LiveQueryReconnectingController'; final Function _reconnect; @@ -408,13 +407,15 @@ class Client { if (actionData.containsKey('object')) { final Map map = actionData['object']; final String className = map['className']; - if (className == '_User') { + if (className == keyClassUser) { subscription.eventCallbacks[actionData['op']]( - (subscription.copyObject ?? ParseUser(null, null, null)) + (subscription.copyObject ?? + ParseCoreData.instance.createParseUser(null, null, null)) .fromJson(map)); } else { subscription.eventCallbacks[actionData['op']]( - (subscription.copyObject ?? ParseObject(className)) + (subscription.copyObject ?? + ParseCoreData.instance.createObject(className)) .fromJson(map)); } } else { diff --git a/lib/src/network/parse_live_query_web.dart b/lib/src/network/parse_live_query_web.dart index 580d75670..6d9012785 100644 --- a/lib/src/network/parse_live_query_web.dart +++ b/lib/src/network/parse_live_query_web.dart @@ -41,7 +41,6 @@ class Subscription { enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } class LiveQueryReconnectingController with WidgetsBindingObserver { - LiveQueryReconnectingController( this._reconnect, this._eventStream, this.debug) { _connectivityChanged(ConnectivityResult.wifi); @@ -72,8 +71,7 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); } - // -1 means "do not try to reconnect", - static const List retryInterval = [0, 500, 1000, 2000, 5000]; + static List get retryInterval => ParseCoreData().liveListRetryIntervals; static const String DEBUG_TAG = 'LiveQueryReconnectingController'; final Function _reconnect; @@ -180,7 +178,6 @@ class Client { Stream _clientEventStream; LiveQueryReconnectingController reconnectingController; - final Map _requestSubScription = {}; Future reconnect({bool userInitialized = false}) async { @@ -342,7 +339,6 @@ class Client { // _channel.sink.add(jsonEncode(connectMessage)); } - void _subscribeLiveQuery(Subscription subscription) { if (subscription._enabled) { return; @@ -409,13 +405,15 @@ class Client { if (actionData.containsKey('object')) { final Map map = actionData['object']; final String className = map['className']; - if (className == '_User') { + if (className == keyClassUser) { subscription.eventCallbacks[actionData['op']]( - (subscription.copyObject ?? ParseUser(null, null, null)) + (subscription.copyObject ?? + ParseCoreData.instance.createParseUser(null, null, null)) .fromJson(map)); } else { subscription.eventCallbacks[actionData['op']]( - (subscription.copyObject ?? ParseObject(className)) + (subscription.copyObject ?? + ParseCoreData.instance.createObject(className)) .fromJson(map)); } } else { diff --git a/lib/src/network/parse_query.dart b/lib/src/network/parse_query.dart index 83cb866ad..9aca4aa3e 100644 --- a/lib/src/network/parse_query.dart +++ b/lib/src/network/parse_query.dart @@ -5,6 +5,9 @@ class QueryBuilder { /// Class to create complex queries QueryBuilder(this.object) : super(); + QueryBuilder.name(String classname) + : this(ParseCoreData.instance.createObject(classname)); + QueryBuilder.or(this.object, List> list) { if (list != null) { String query = '"\$or":['; @@ -295,6 +298,48 @@ class QueryBuilder { _SINGLE_QUERY, '\"$column\":{\"\$notInQuery\":$inQuery}')); } + // Add a constraint to the query that requires a particular key's value matches a value for a key in the results of another ParseQuery. + // ignore: always_specify_types + void whereMatchesKeyInQuery( + String column, String keyInQuery, QueryBuilder query) { + if (query.queries.isEmpty) { + throw ArgumentError('query conditions is required'); + } + if (limiters.containsKey('order')) { + throw ArgumentError('order is not allowed'); + } + if (limiters.containsKey('include')) { + throw ArgumentError('include is not allowed'); + } + + final String inQuery = + query._buildQueryRelationalKey(query.object.parseClassName, keyInQuery); + + queries.add(MapEntry( + _SINGLE_QUERY, '\"$column\":{\"\$select\":$inQuery}')); + } + + // Add a constraint to the query that requires a particular key's value does not match any value for a key in the results of another ParseQuery + // ignore: always_specify_types + void whereDoesNotMatchKeyInQuery( + String column, String keyInQuery, QueryBuilder query) { + if (query.queries.isEmpty) { + throw ArgumentError('query conditions is required'); + } + if (limiters.containsKey('order')) { + throw ArgumentError('order is not allowed'); + } + if (limiters.containsKey('include')) { + throw ArgumentError('include is not allowed'); + } + + final String inQuery = + query._buildQueryRelationalKey(query.object.parseClassName, keyInQuery); + + queries.add(MapEntry( + _SINGLE_QUERY, '\"$column\":{\"\$dontSelect\":$inQuery}')); + } + /// Finishes the query and calls the server /// /// Make sure to call this after defining your queries @@ -325,6 +370,12 @@ class QueryBuilder { return '{\"where\":{${buildQueries(queries)}},\"className\":\"$className\"${getLimitersRelational(limiters)}}'; } + /// Builds the query relational with Key for Parse + String _buildQueryRelationalKey(String className, String keyInQuery) { + queries = _checkForMultipleColumnInstances(queries); + return '{\"query\":{\"className\":\"$className\",\"where\":{${buildQueries(queries)}}},\"key\":\"$keyInQuery\"}'; + } + /// Builds the query for Parse String _buildQueryCount() { queries = _checkForMultipleColumnInstances(queries); diff --git a/lib/src/objects/parse_error.dart b/lib/src/objects/parse_error.dart index 39fd9ad19..c8b63dc4f 100644 --- a/lib/src/objects/parse_error.dart +++ b/lib/src/objects/parse_error.dart @@ -5,7 +5,7 @@ class ParseError { ParseError( {this.code = -1, this.message = 'Unknown error', - this.isTypeOfException = false, + this.exception, bool debug = false}) { type = exceptions[code]; if (debug) { @@ -70,7 +70,7 @@ class ParseError { final int code; final String message; - final bool isTypeOfException; + final Exception exception; String type; @override diff --git a/lib/src/objects/parse_file.dart b/lib/src/objects/parse_file.dart index 4a0058de9..c44d63ab3 100644 --- a/lib/src/objects/parse_file.dart +++ b/lib/src/objects/parse_file.dart @@ -1,6 +1,6 @@ part of flutter_parse_sdk; -class ParseFile extends ParseObject { +class ParseFile extends ParseFileBase { /// Creates a new file /// /// {https://docs.parseplatform.org/rest/guide/#files/} @@ -9,41 +9,17 @@ class ParseFile extends ParseObject { String url, bool debug, ParseHTTPClient client, - bool autoSendSessionId}) - : super('ParseFile', debug: debug, autoSendSessionId: autoSendSessionId) { - _debug = isDebugEnabled(objectLevelDebug: debug); - _client = client ?? - ParseHTTPClient( - sendSessionId: - autoSendSessionId ?? ParseCoreData().autoSendSessionId, - securityContext: ParseCoreData().securityContext); - - if (file != null) { - name = path.basename(file.path); - _path = '/files/$name'; - } else { - name = name; - url = url; - } - } + bool autoSendSessionId}) + : super( + name: file != null ? path.basename(file.path) : name, + url: url, + debug: debug, + client: client, + autoSendSessionId: autoSendSessionId, + ); File file; - String get name => super.get(keyVarName); - set name(String name) => set(keyVarName, name); - - String get url => super.get(keyVarURL); - set url(String url) => set(keyVarURL, url); - - bool get saved => url != null; - - @override - Map toJson({bool full = false, bool forApiRQ = false}) => - {'__type': keyFile, 'name': name, 'url': url}; - - @override - String toString() => json.encode(toJson(full: true)); - Future loadStorage() async { final Directory tempPath = await getTemporaryDirectory(); @@ -65,6 +41,7 @@ class ParseFile extends ParseObject { return this; } + @override Future download() async { if (url == null) { return this; @@ -81,11 +58,6 @@ class ParseFile extends ParseObject { /// Uploads a file to Parse Server @override - Future save() async { - return upload(); - } - - /// Uploads a file to Parse Server Future upload() async { if (saved) { //Creates a Fake Response to return the correct result diff --git a/lib/src/objects/parse_file_base.dart b/lib/src/objects/parse_file_base.dart new file mode 100644 index 000000000..e565b92a7 --- /dev/null +++ b/lib/src/objects/parse_file_base.dart @@ -0,0 +1,47 @@ +part of flutter_parse_sdk; + +abstract class ParseFileBase extends ParseObject { + /// Creates a new file + /// + /// {https://docs.parseplatform.org/rest/guide/#files/} + ParseFileBase( + {@required String name, + String url, + bool debug, + ParseHTTPClient client, + bool autoSendSessionId}) + : super(keyFileClassname, + debug: debug, + autoSendSessionId: autoSendSessionId, + client: client) { + _path = '/files/$name'; + this.name = name; + this.url = url; + } + + String get name => super.get(keyVarName); + set name(String name) => set(keyVarName, name); + + String get url => super.get(keyVarURL); + set url(String url) => set(keyVarURL, url); + + bool get saved => url != null; + + @override + Map toJson({bool full = false, bool forApiRQ = false}) => + {'__type': keyFile, 'name': name, 'url': url}; + + @override + String toString() => json.encode(toJson(full: true)); + + /// Uploads a file to Parse Server + @override + Future save() async { + return upload(); + } + + /// Uploads a file to Parse Server + Future upload(); + + Future download(); +} diff --git a/lib/src/objects/parse_file_web.dart b/lib/src/objects/parse_file_web.dart new file mode 100644 index 000000000..f17369a96 --- /dev/null +++ b/lib/src/objects/parse_file_web.dart @@ -0,0 +1,68 @@ +part of flutter_parse_sdk; + +class ParseWebFile extends ParseFileBase { + ParseWebFile(this.file, + {@required String name, + String url, + bool debug, + ParseHTTPClient client, + bool autoSendSessionId}) + : super( + name: name, + url: url, + debug: debug, + client: client, + autoSendSessionId: autoSendSessionId, + ); + + Uint8List file; + + @override + Future download() async { + if (url == null) { + return this; + } + + final Response response = await _client.get(url); + file = response.bodyBytes; + + return this; + } + + @override + Future upload() async { + if (saved) { + //Creates a Fake Response to return the correct result + final Map response = { + 'url': url, + 'name': name + }; + return handleResponse( + this, + Response(json.encode(response), 201), + ParseApiRQ.upload, + _debug, + parseClassName); + } + + final Map headers = { + HttpHeaders.contentTypeHeader: url ?? name != null + ? getContentType(path.extension(url ?? name)) + : 'text/plain' + }; + try { + final String uri = _client.data.serverUrl + '$_path'; + final Response response = + await _client.post(uri, headers: headers, body: file); + if (response.statusCode == 201) { + final Map map = json.decode(response.body); + url = map['url'].toString(); + name = map['name'].toString(); + } + return handleResponse( + this, response, ParseApiRQ.upload, _debug, parseClassName); + } on Exception catch (e) { + return handleException(e, ParseApiRQ.upload, _debug, parseClassName); + } + } +} diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index 264b81c5a..34e9eaa45 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -68,7 +68,9 @@ class ParseInstallation extends ParseObject { /// Updates the installation with current device data Future _updateInstallation() async { //Device type - if (Platform.isAndroid) { + if (parseIsWeb) { + set(keyDeviceType, 'web'); + } else if (Platform.isAndroid) { set(keyDeviceType, 'android'); } else if (Platform.isIOS) { set(keyDeviceType, 'ios'); @@ -81,7 +83,9 @@ class ParseInstallation extends ParseObject { } //Locale - final String locale = await Devicelocale.currentLocale; + final String locale = parseIsWeb + ? ui.window.locale.toString() + : await Devicelocale.currentLocale; if (locale != null && locale.isNotEmpty) { set(keyLocaleIdentifier, locale); } @@ -89,10 +93,12 @@ class ParseInstallation extends ParseObject { //Timezone //App info - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - set(keyAppName, packageInfo.appName); - set(keyAppVersion, packageInfo.version); - set(keyAppIdentifier, packageInfo.packageName); + if (!parseIsWeb) { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + set(keyAppName, packageInfo.appName); + set(keyAppVersion, packageInfo.version); + set(keyAppIdentifier, packageInfo.packageName); + } set(keyParseVersion, keySdkVersion); } @@ -153,6 +159,8 @@ class ParseInstallation extends ParseObject { final ParseInstallation installation = ParseInstallation(); installation._installationId = _currentInstallationId; await installation._updateInstallation(); + await ParseCoreData().getStore().setString(keyParseStoreInstallation, + json.encode(installation.toJson(full: true))); return installation; } diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index b89368736..94d9a1576 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -121,7 +121,7 @@ class ParseObject extends ParseBase implements ParseCloneable { Future _saveChildren(dynamic object) async { final Set uniqueObjects = Set(); - final Set uniqueFiles = Set(); + final Set uniqueFiles = Set(); if (!_collectionDirtyChildren(object, uniqueObjects, uniqueFiles, Set(), Set())) { final ParseResponse response = ParseResponse(); @@ -130,7 +130,7 @@ class ParseObject extends ParseBase implements ParseCloneable { if (object is ParseObject) { uniqueObjects.remove(object); } - for (ParseFile file in uniqueFiles) { + for (ParseFileBase file in uniqueFiles) { final ParseResponse response = await file.save(); if (!response.success) { return response; @@ -224,7 +224,11 @@ class ParseObject extends ParseBase implements ParseCloneable { bool _canbeSerialized(List aftersaving, {dynamic value}) { if (value != null) { if (value is ParseObject) { - if (value.objectId == null && !aftersaving.contains(value)) { + if (value is ParseFileBase) { + if (!value.saved && !aftersaving.contains(value)) { + return false; + } + } else if (value.objectId == null && !aftersaving.contains(value)) { return false; } } else if (value is Map) { @@ -250,7 +254,7 @@ class ParseObject extends ParseBase implements ParseCloneable { bool _collectionDirtyChildren( dynamic object, Set uniqueObjects, - Set uniqueFiles, + Set uniqueFiles, Set seen, Set seenNew) { if (object is List) { @@ -269,8 +273,8 @@ class ParseObject extends ParseBase implements ParseCloneable { } } else if (object is ParseACL) { // TODO(yulingtianxia): handle ACL - } else if (object is ParseFile) { - if (object.url == null) { + } else if (object is ParseFileBase) { + if (!object.saved) { uniqueFiles.add(object); } } else if (object is ParseObject) { diff --git a/lib/src/objects/parse_session.dart b/lib/src/objects/parse_session.dart index d4a1947fc..c02e1fe04 100644 --- a/lib/src/objects/parse_session.dart +++ b/lib/src/objects/parse_session.dart @@ -30,10 +30,8 @@ class ParseSession extends ParseObject implements ParseCloneable { set installationId(String installationId) => set(keyVarInstallationId, installationId); - Future getCurrentSessionFromServer() async { try { - const String path = '$keyEndPointSessions/me'; final Uri url = getSanitisedUri(_client, path); diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index 5c43a17e7..55c822e71 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -76,7 +76,8 @@ class ParseUser extends ParseObject implements ParseCloneable { static ParseUser createUser( [String username, String password, String emailAddress]) { - return ParseUser(username, password, emailAddress); + return ParseCoreData.instance + .createParseUser(username, password, emailAddress); } /// Gets the current user from the server @@ -118,11 +119,10 @@ class ParseUser extends ParseObject implements ParseCloneable { try { final Uri url = getSanitisedUri(_client, '$keyEndPointUserName'); final Response response = await _client.get(url, headers: headers); - return await _handleResponse(this, response, - ParseApiRQ.currentUser, _debug, parseClassName); + return await _handleResponse( + this, response, ParseApiRQ.currentUser, _debug, parseClassName); } on Exception catch (e) { - return handleException( - e, ParseApiRQ.currentUser, _debug, parseClassName); + return handleException(e, ParseApiRQ.currentUser, _debug, parseClassName); } } @@ -144,6 +144,8 @@ class ParseUser extends ParseObject implements ParseCloneable { /// After creating a new user via [Parse.create] call this method to register /// that user on Parse Future signUp() async { + forgetLocalSession(); + try { if (emailAddress == null) { return null; @@ -173,18 +175,21 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Once a user is created using [Parse.create] and a username and password is /// provided, call this method to login. Future login() async { + forgetLocalSession(); + try { final Map queryParams = { keyVarUsername: username, keyVarPassword: password }; - + final String installationId = await _getInstallationId(); final Uri url = getSanitisedUri(_client, '$keyEndPointLogin', queryParams: queryParams); _saveChanges(); final Response response = await _client.get(url, headers: { keyHeaderRevocableSession: '1', + if (installationId != null) keyHeaderInstallationId: installationId, }); return await _handleResponse( @@ -196,6 +201,7 @@ class ParseUser extends ParseObject implements ParseCloneable { // Logs in a user anonymously Future loginAnonymous() async { + forgetLocalSession(); try { final Uri url = getSanitisedUri(_client, '$keyEndPointUsers'); final Uuid uuid = Uuid(); @@ -328,6 +334,19 @@ class ParseUser extends ParseObject implements ParseCloneable { } } + @override + Future update() async { + if (objectId == null) { + return await signUp(); + } else { + final ParseResponse response = await super.update(); + if (response.success) { + await _onResponseSuccess(); + } + return response; + } + } + Future _onResponseSuccess() async { await saveInStorage(keyParseStoreUser); } @@ -413,7 +432,8 @@ class ParseUser extends ParseObject implements ParseCloneable { } } - static ParseUser _getEmptyUser() => ParseUser(null, null, null); + static ParseUser _getEmptyUser() => + ParseCoreData.instance.createParseUser(null, null, null); static Future _getInstallationId() async { final ParseInstallation parseInstallation = diff --git a/lib/src/objects/response/parse_exception_response.dart b/lib/src/objects/response/parse_exception_response.dart index 9a798c8b0..89f12e772 100644 --- a/lib/src/objects/response/parse_exception_response.dart +++ b/lib/src/objects/response/parse_exception_response.dart @@ -4,6 +4,6 @@ part of flutter_parse_sdk; ParseResponse buildParseResponseWithException(Exception exception) { final ParseResponse response = ParseResponse(); response.error = - ParseError(message: exception.toString(), isTypeOfException: true); + ParseError(message: exception.toString(), exception: exception); return response; } diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index 893010503..cf6ff8f14 100644 --- a/lib/src/objects/response/parse_response_builder.dart +++ b/lib/src/objects/response/parse_response_builder.dart @@ -151,6 +151,6 @@ class _ParseResponseBuilder { } bool isHealthCheck(Response apiResponse) { - return apiResponse.body == '{\"status\":\"ok\"}'; + return ['{\"status\":\"ok\"}', 'OK'].contains(apiResponse.body); } } diff --git a/lib/src/storage/core_store_sem_impl.dart b/lib/src/storage/core_store_sem_impl.dart index b2c67d35f..f8e2c317d 100644 --- a/lib/src/storage/core_store_sem_impl.dart +++ b/lib/src/storage/core_store_sem_impl.dart @@ -12,7 +12,8 @@ class CoreStoreSembastImp implements CoreStore { factory ??= databaseFactoryIo; final SembastCodec codec = getXXTeaSembastCodec(password: password); String dbDirectory = ''; - if (Platform.isIOS || Platform.isAndroid) + if (!parseIsWeb && + (Platform.isIOS || Platform.isAndroid || Platform.isMacOS)) dbDirectory = (await getApplicationDocumentsDirectory()).path; final String dbPath = path.join('$dbDirectory/parse', 'parse.db'); final Database db = await factory.openDatabase(dbPath, codec: codec); diff --git a/lib/src/utils/parse_decoder.dart b/lib/src/utils/parse_decoder.dart index 73a01d557..f2d0d929e 100644 --- a/lib/src/utils/parse_decoder.dart +++ b/lib/src/utils/parse_decoder.dart @@ -58,19 +58,12 @@ dynamic parseDecode(dynamic value) { final String val = map['base64']; return base64.decode(val); case 'Pointer': - final String className = map['className']; - if (className == '_User') { - return ParseUser._getEmptyUser().fromJson(map); - } - return ParseObject(className).fromJson(map); case 'Object': final String className = map['className']; - if (className == '_User') { - return ParseUser._getEmptyUser().fromJson(map); - } - return ParseObject(className).fromJson(map); + return ParseCoreData.instance.createObject(className).fromJson(map); case 'File': - return ParseFile(null, url: map['url'], name: map['name']) + return ParseCoreData.instance + .createFile(url: map['url'], name: map['name']) .fromJson(map); case 'GeoPoint': final num latitude = map['latitude'] ?? 0.0; @@ -78,7 +71,7 @@ dynamic parseDecode(dynamic value) { return ParseGeoPoint( latitude: latitude.toDouble(), longitude: longitude.toDouble()); case 'Relation': - // ignore: always_specify_types + // ignore: always_specify_types return ParseRelation().fromJson(map); } } @@ -86,15 +79,15 @@ dynamic parseDecode(dynamic value) { /// Decoding from locally cached JSON if (map.containsKey('className')) { switch (map['className']) { - case '_User': - return ParseUser._getEmptyUser().fromJson(map); case 'GeoPoint': final num latitude = map['latitude'] ?? 0.0; final num longitude = map['longitude'] ?? 0.0; return ParseGeoPoint( latitude: latitude.toDouble(), longitude: longitude.toDouble()); default: - return ParseObject(map['className']).fromJson(map); + return ParseCoreData.instance + .createObject(map['className']) + .fromJson(map); } } diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index ffd12e417..50e8e7f82 100644 --- a/lib/src/utils/parse_encoder.dart +++ b/lib/src/utils/parse_encoder.dart @@ -36,7 +36,7 @@ dynamic parseEncode(dynamic value, {bool full}) { return value; } - if (value is ParseFile) { + if (value is ParseFileBase) { return value; } diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 6c063ba6a..0528e676d 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -6,11 +6,23 @@ import '../../parse_server_sdk.dart'; // ignore_for_file: invalid_use_of_protected_member class ParseLiveList { - ParseLiveList._(this._query); + ParseLiveList._(this._query, this._listeningIncludes, this._lazyLoading) { + _debug = isDebugEnabled(); + } static Future> create( - QueryBuilder _query) { - final ParseLiveList parseLiveList = ParseLiveList._(_query); + QueryBuilder _query, + {bool listenOnAllSubItems, + List listeningIncludes, + bool lazyLoading = true}) { + final ParseLiveList parseLiveList = ParseLiveList._( + _query, + listenOnAllSubItems == true + ? _toIncludeMap( + _query.limiters['include']?.toString()?.split(',') ?? + []) + : _toIncludeMap(listeningIncludes ?? []), + lazyLoading); return parseLiveList._init().then((_) { return parseLiveList; @@ -20,6 +32,7 @@ class ParseLiveList { List> _list = List>(); StreamController> _eventStreamController; int _nextID = 0; + bool _debug; /// is object1 listed after object2? /// can return null @@ -78,29 +91,59 @@ class ParseLiveList { int get nextID => _nextID++; final QueryBuilder _query; + //The included Items, where LiveList should look for updates. + final Map _listeningIncludes; + + final bool _lazyLoading; int get size { return _list.length; } + List get includes => + _query.limiters['include']?.toString()?.split(',') ?? []; + + Map get _includePaths { + return _toIncludeMap(includes); + } + + static Map _toIncludeMap(List includes) { + final Map includesMap = {}; + + for (String includeString in includes) { + final List pathParts = includeString.split('.'); + Map root = includesMap; + for (String pathPart in pathParts) { + root.putIfAbsent(pathPart, () => {}); + root = root[pathPart]; + } + } + + return includesMap; + } + Stream> get stream => _eventStreamController.stream; Subscription _liveQuerySubscription; StreamSubscription _liveQueryClientEventSubscription; + final Future _updateQueue = Future.value(); Future _runQuery() async { final QueryBuilder query = QueryBuilder.copy(_query); - if (query.limiters.containsKey('order')) { - query.keysToReturn( - query.limiters['order'].toString().split(',').map((String string) { - if (string.startsWith('-')) { - return string.substring(1); - } - return string; - }).toList()); - } else { - query.keysToReturn(List()); + if (_debug) + print('ParseLiveList: lazyLoading is ${_lazyLoading ? 'on' : 'off'}'); + if (_lazyLoading) { + if (query.limiters.containsKey('order')) { + query.keysToReturn( + query.limiters['order'].toString().split(',').map((String string) { + if (string.startsWith('-')) { + return string.substring(1); + } + return string; + }).toList()); + } else { + query.keysToReturn(List()); + } } - return await query.query(); } @@ -110,8 +153,10 @@ class ParseLiveList { final ParseResponse parseResponse = await _runQuery(); if (parseResponse.success) { _list = parseResponse.results - ?.map>( - (dynamic element) => ParseLiveListElement(element)) + ?.map>((dynamic element) => + ParseLiveListElement(element, + updatedSubItems: _listeningIncludes, + loaded: !_lazyLoading)) ?.toList() ?? List>(); } @@ -122,11 +167,29 @@ class ParseLiveList { copyObject: _query.object.clone(_query.object.toJson())) .then((Subscription subscription) { _liveQuerySubscription = subscription; - subscription.on(LiveQueryEvent.create, _objectAdded); - subscription.on(LiveQueryEvent.update, _objectUpdated); - subscription.on(LiveQueryEvent.enter, _objectAdded); - subscription.on(LiveQueryEvent.leave, _objectDeleted); - subscription.on(LiveQueryEvent.delete, _objectDeleted); + + //This should synchronize the events. Not sure if it is necessary, but it should help preventing unexpected results. + subscription.on(LiveQueryEvent.create, + (T object) => _updateQueue.whenComplete(() => _objectAdded(object))); + subscription.on( + LiveQueryEvent.update, + (T object) => + _updateQueue.whenComplete(() => _objectUpdated(object))); + subscription.on(LiveQueryEvent.enter, + (T object) => _updateQueue.whenComplete(() => _objectAdded(object))); + subscription.on( + LiveQueryEvent.leave, + (T object) => + _updateQueue.whenComplete(() => _objectDeleted(object))); + subscription.on( + LiveQueryEvent.delete, + (T object) => + _updateQueue.whenComplete(() => _objectDeleted(object))); +// subscription.on(LiveQueryEvent.create, _objectAdded); +// subscription.on(LiveQueryEvent.update, _objectUpdated); +// subscription.on(LiveQueryEvent.enter, _objectAdded); +// subscription.on(LiveQueryEvent.leave, _objectDeleted); +// subscription.on(LiveQueryEvent.delete, _objectDeleted); }); _liveQueryClientEventSubscription = LiveQuery() @@ -134,90 +197,203 @@ class ParseLiveList { .getClientEventStream .listen((LiveQueryClientEvent event) async { if (event == LiveQueryClientEvent.CONNECTED) { - final ParseResponse parseResponse = await _runQuery(); - if (parseResponse.success) { - final List newList = parseResponse.results ?? List(); - - //update List - for (int i = 0; i < _list.length; i++) { - final ParseObject currentObject = _list[i].object; - final String currentObjectId = - currentObject.get(keyVarObjectId); - - bool stillInList = false; - - for (int j = 0; j < newList.length; j++) { - if (newList[j].get(keyVarObjectId) == currentObjectId) { - stillInList = true; - if (newList[j] - .get(keyVarUpdatedAt) - .isAfter(currentObject.get(keyVarUpdatedAt))) { - final QueryBuilder queryBuilder = - QueryBuilder.copy(_query) - ..whereEqualTo(keyVarObjectId, currentObjectId); - queryBuilder.query().then((ParseResponse result) { - if (result.success && result.results != null) { - _objectUpdated(result.results.first); - } - }); + _updateQueue.whenComplete(() async { + List> tasks = >[]; + final ParseResponse parseResponse = await _runQuery(); + if (parseResponse.success) { + final List newList = parseResponse.results ?? List(); + + //update List + for (int i = 0; i < _list.length; i++) { + final ParseObject currentObject = _list[i].object; + final String currentObjectId = + currentObject.get(keyVarObjectId); + + bool stillInList = false; + + for (int j = 0; j < newList.length; j++) { + if (newList[j].get(keyVarObjectId) == currentObjectId) { + stillInList = true; + if (newList[j] + .get(keyVarUpdatedAt) + .isAfter(currentObject.get(keyVarUpdatedAt))) { + final QueryBuilder queryBuilder = + QueryBuilder.copy(_query) + ..whereEqualTo(keyVarObjectId, currentObjectId); + tasks.add(queryBuilder + .query() + .then((ParseResponse result) async { + if (result.success && result.results != null) { + await _objectUpdated(result.results.first); + } + })); + } + newList.removeAt(j); + j--; + break; } - newList.removeAt(j); - j--; - break; + } + if (!stillInList) { + _objectDeleted(currentObject); + i--; } } - if (!stillInList) { - _objectDeleted(currentObject); - i--; + + for (int i = 0; i < newList.length; i++) { + tasks.add(_objectAdded(newList[i], loaded: false)); } } + await Future.wait(tasks); + tasks = >[]; + for (ParseLiveListElement element in _list) { + tasks.add(element.reconnected()); + } + await Future.wait(tasks); + }); + } + }); + } - for (int i = 0; i < newList.length; i++) { - _objectAdded(newList[i], loaded: false); + static Future _loadIncludes(ParseObject object, + {ParseObject oldObject, Map paths}) async { + if (object == null || paths == null || paths.isEmpty) { + return; + } + + final List> loadingNodes = >[]; + + for (String key in paths.keys) { + if (object.containsKey(key)) { + ParseObject includedObject = object.get(key); + //If the object is not fetched + if (!includedObject.containsKey(keyVarUpdatedAt)) { + //See if oldObject contains key + if (oldObject != null && oldObject.containsKey(key)) { + includedObject = oldObject.get(key); + //If the object is not fetched || the ids don't match / the pointer changed + if (!includedObject.containsKey(keyVarUpdatedAt) || + includedObject.objectId != + object.get(key).objectId) { + includedObject = object.get(key); + //fetch from web including sub objects + //same as down there + final QueryBuilder queryBuilder = QueryBuilder< + ParseObject>(ParseObject(includedObject.parseClassName)) + ..whereEqualTo(keyVarObjectId, includedObject.objectId) + ..includeObject(_toIncludeStringList(paths[key])); + loadingNodes.add(queryBuilder + .query() + .then((ParseResponse parseResponse) { + if (parseResponse.success && + parseResponse.results.length == 1) { + // ignore: deprecated_member_use_from_same_package + object.getObjectData()[key] = parseResponse.results[0]; + } + })); + continue; + } else { + // ignore: deprecated_member_use_from_same_package + object.getObjectData()[key] = includedObject; + //recursion + loadingNodes + .add(_loadIncludes(includedObject, paths: paths[key])); + continue; + } + } else { + //fetch from web including sub objects + //same as up there + final QueryBuilder queryBuilder = QueryBuilder< + ParseObject>(ParseObject(includedObject.parseClassName)) + ..whereEqualTo(keyVarObjectId, includedObject.objectId) + ..includeObject(_toIncludeStringList(paths[key])); + loadingNodes.add( + queryBuilder.query().then((ParseResponse parseResponse) { + if (parseResponse.success && parseResponse.results.length == 1) { + // ignore: deprecated_member_use_from_same_package + object.getObjectData()[key] = parseResponse.results[0]; + } + })); + continue; } + } else { + //recursion + loadingNodes.add(_loadIncludes(includedObject, + oldObject: oldObject?.get(key), paths: paths[key])); + continue; } + } else { + //All fine for this key + continue; } - }); + } + await Future.wait(loadingNodes); + } + + static List _toIncludeStringList(Map includes) { + final List includeList = []; + for (String key in includes.keys) { + includeList.add(key); + // ignore: avoid_as + if ((includes[key] as Map).isNotEmpty) { + includeList + .addAll(_toIncludeStringList(includes[key]).map((String e) => '$key.$e')); + } + } + return includeList; } - void _objectAdded(T object, {bool loaded = true}) { + Future _objectAdded(T object, + {bool loaded = true, bool fetchedIncludes = false}) async { + //This line seems unnecessary, but without this, weird things happen. + //(Hide first element, hide second, view first, view second => second is displayed twice) + object = object?.clone(object?.toJson(full: true)); + + if (!fetchedIncludes) { + await _loadIncludes(object, paths: _includePaths); + } for (int i = 0; i < _list.length; i++) { if (after(object, _list[i].object) != true) { - _list.insert(i, ParseLiveListElement(object, loaded: loaded)); + _list.insert( + i, + ParseLiveListElement(object, + loaded: loaded, updatedSubItems: _listeningIncludes)); _eventStreamController.sink.add(ParseLiveListAddEvent( i, object?.clone(object?.toJson(full: true)))); return; } } - _list.add(ParseLiveListElement(object, loaded: loaded)); + _list.add(ParseLiveListElement(object, + loaded: loaded, updatedSubItems: _listeningIncludes)); _eventStreamController.sink.add(ParseLiveListAddEvent( _list.length - 1, object?.clone(object?.toJson(full: true)))); } - void _objectUpdated(T object) { + Future _objectUpdated(T object) async { for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { + await _loadIncludes(object, + oldObject: _list[i].object, paths: _includePaths); if (after(_list[i].object, object) == null) { - _list[i].object = object; + _list[i].object = object?.clone(object?.toJson(full: true)); } else { _list.removeAt(i).dispose(); _eventStreamController.sink.add(ParseLiveListDeleteEvent( - // ignore: invalid_use_of_protected_member - i, - object?.clone(object?.toJson(full: true)))); - // ignore: invalid_use_of_protected_member - _objectAdded(object?.clone(object?.toJson(full: true))); + i, object?.clone(object?.toJson(full: true)))); + await _objectAdded(object?.clone(object?.toJson(full: true)), + fetchedIncludes: true); } break; } } } - void _objectDeleted(T object) { + Future _objectDeleted(T object) async { for (int i = 0; i < _list.length; i++) { if (_list[i].object.get(keyVarObjectId) == object.get(keyVarObjectId)) { + await _loadIncludes(object, + oldObject: _list[i].object, paths: _includePaths); _list.removeAt(i).dispose(); _eventStreamController.sink.add(ParseLiveListDeleteEvent( i, object?.clone(object?.toJson(full: true)))); @@ -255,6 +431,15 @@ class ParseLiveList { return 'NotFound'; } + String getIdentifier(int index) { + if (index < _list.length) { + return _list[index].object.get(keyVarObjectId) + + _list[index].object.get(keyVarUpdatedAt)?.toString() ?? + ''; + } + return 'NotFound'; + } + T getLoadedAt(int index) { if (index < _list.length && _list[index].loaded) { return _list[index].object; @@ -277,25 +462,181 @@ class ParseLiveList { } } +class ParseLiveElement extends ParseLiveListElement { + ParseLiveElement(T object, {bool loaded = false, List includeObject}) + : super(object, + loaded: loaded, + updatedSubItems: + ParseLiveList._toIncludeMap(includeObject ?? [])) { + _includes = ParseLiveList._toIncludeMap(includeObject ?? []); + queryBuilder = QueryBuilder(object.clone(null)) + ..includeObject(includeObject) + ..whereEqualTo(keyVarObjectId, object.objectId); + _init(object, loaded: loaded, includeObject: includeObject); + } + + Subscription _subscription; + Map _includes; + QueryBuilder queryBuilder; + + Future _init(T object, + {bool loaded = false, List includeObject}) async { + if (!loaded) { + final ParseResponse parseResponse = await queryBuilder.query(); + if (parseResponse.success) { + super.object = parseResponse.result.first; + } + } + + _subscription = await LiveQuery().client.subscribe( + QueryBuilder.copy(queryBuilder), + copyObject: object.clone(null)); + + _subscription.on(LiveQueryEvent.update, (T newObject) async { + await ParseLiveList._loadIncludes(newObject, + oldObject: super.object, paths: _includes); + super.object = newObject; + }); + + LiveQuery() + .client + .getClientEventStream + .listen((LiveQueryClientEvent event) { + _subscriptionQueue.whenComplete(() async { + // ignore: missing_enum_constant_in_switch + switch (event) { + case LiveQueryClientEvent.CONNECTED: + final ParseResponse parseResponse = await queryBuilder.query(); + if (parseResponse.success) { + super.object = parseResponse.result.first; + } + break; + } + }); + }); + } + + @override + void dispose() { + if (_subscription != null) { + LiveQuery().client.unSubscribe(_subscription); + _subscription = null; + } + super.dispose(); + } +} + class ParseLiveListElement { - ParseLiveListElement(this._object, {bool loaded = false}) { + ParseLiveListElement(this._object, + {bool loaded = false, Map updatedSubItems}) { if (_object != null) { _loaded = loaded; } + _updatedSubItems = + _toSubscriptionMap(updatedSubItems ?? Map()); + if (_updatedSubItems.isNotEmpty) { + _liveQuery = LiveQuery(); + _subscribe(); + } } final StreamController _streamController = StreamController.broadcast(); T _object; bool _loaded = false; + Map _updatedSubItems; + LiveQuery _liveQuery; + final Future _subscriptionQueue = Future.value(); Stream get stream => _streamController?.stream; // ignore: invalid_use_of_protected_member T get object => _object?.clone(_object?.toJson(full: true)); + Map _toSubscriptionMap(Map map) { + final Map result = Map(); + for (String key in map.keys) { + result.putIfAbsent(PathKey(key), () => _toSubscriptionMap(map[key])); + } + return result; + } + + Map _toKeyMap(Map map) { + final Map result = Map(); + for (PathKey key in map.keys) { + result.putIfAbsent(key.key, () => _toKeyMap(map[key])); + } + return result; + } + + void _subscribe() { + _subscriptionQueue.whenComplete(() async { + if (_updatedSubItems.isNotEmpty && _object != null) { + final List> tasks = >[]; + for (PathKey key in _updatedSubItems.keys) { + tasks.add(_subscribeSubItem(_object, key, + _object.get(key.key), _updatedSubItems[key])); + } + await Future.wait(tasks); + } + }); + } + + void _unsubscribe(Map subscriptions) { + for (PathKey key in subscriptions.keys) { + if (_liveQuery != null && key.subscription != null) { + _liveQuery.client.unSubscribe(key.subscription); + key.subscription = null; + } + _unsubscribe(subscriptions[key]); + } + } + + Future _subscribeSubItem(ParseObject parentObject, PathKey currentKey, + ParseObject subObject, Map path) async { + if (_liveQuery != null && subObject != null) { + final List> tasks = >[]; + for (PathKey key in path.keys) { + tasks.add(_subscribeSubItem( + subObject, key, subObject.get(key.key), path[key])); + } + final QueryBuilder queryBuilder = + QueryBuilder(subObject) + ..whereEqualTo(keyVarObjectId, subObject.objectId); + + tasks.add(_liveQuery.client + .subscribe(queryBuilder) + .then((Subscription subscription) { + currentKey.subscription = subscription; + subscription.on(LiveQueryEvent.update, (ParseObject newObject) async { + _subscriptionQueue.whenComplete(() async { + await ParseLiveList._loadIncludes(newObject, + oldObject: subObject, paths: _toKeyMap(path)); + // ignore: deprecated_member_use_from_same_package + parentObject.getObjectData()[currentKey.key] = newObject; + if (!_streamController.isClosed) { + _streamController + ?.add(_object?.clone(_object?.toJson(full: true))); + //Resubscribe subitems + // TODO(any): only resubscribe on changed pointers + _unsubscribe(path); + for (PathKey key in path.keys) { + tasks.add(_subscribeSubItem(newObject, key, + newObject.get(key.key), path[key])); + } + } + await Future.wait(tasks); + }); + }); + })); + await Future.wait(tasks); + } + } + set object(T value) { _loaded = true; _object = value; + _unsubscribe(_updatedSubItems); + _subscribe(); // ignore: invalid_use_of_protected_member _streamController?.add(_object?.clone(_object?.toJson(full: true))); } @@ -303,12 +644,76 @@ class ParseLiveListElement { bool get loaded => _loaded; void dispose() { + _unsubscribe(_updatedSubItems); _streamController.close(); } + + Future reconnected() async { + if (loaded) { + _subscriptionQueue.whenComplete(() async { + await _updateSubItems(_object, _updatedSubItems); +// _streamController?.add(_object?.clone(_object?.toJson(full: true))); + }); + } + } + + List _getIncludeList(Map path) { + final List includes = []; + for (PathKey key in path.keys) { + includes.add(key.key); + includes.addAll( + _getIncludeList(path[key]).map((String e) => '${key.key}.$e')); + } + return includes; + } + + Future _updateSubItems( + ParseObject root, Map path) async { + final List> tasks = >[]; + for (PathKey key in path.keys) { + ParseObject subObject = root.get(key.key); + if (subObject?.containsKey(keyVarUpdatedAt) == true) { + final QueryBuilder queryBuilder = + QueryBuilder(subObject) + ..keysToReturn([keyVarUpdatedAt]) + ..whereEqualTo(keyVarObjectId, subObject.objectId); + final ParseResponse parseResponse = await queryBuilder.query(); + if (parseResponse.success && parseResponse.results.first.updatedAt != + subObject.updatedAt) { + queryBuilder.limiters.remove('keys'); + queryBuilder.includeObject(_getIncludeList(path[key])); + final ParseResponse parseResponse = await queryBuilder.query(); + if (parseResponse.success) { + subObject = parseResponse.result.first; +// root.getObjectData()[key.key] = subObject; + if (key.subscription?.eventCallbacks?.containsKey('update') == + true) { + key.subscription.eventCallbacks['update'](subObject); + } +// key.subscription.eventCallbacks["update"](subObject); + break; + } + } + } + tasks.add(_updateSubItems(subObject, path[key])); + } + await Future.wait(tasks); + } +} + +class PathKey { + PathKey(this.key, {this.subscription}); + + final String key; + Subscription subscription; + @override + String toString() { + return 'PathKey(key: $key, subscription: ${subscription?.requestId})'; + } } abstract class ParseLiveListEvent { - ParseLiveListEvent(this._index, this._object); //, this._object); + ParseLiveListEvent(this._index, this._object); final int _index; final T _object; @@ -345,21 +750,24 @@ class ParseLiveListElementSnapshot { } class ParseLiveListWidget extends StatefulWidget { - const ParseLiveListWidget( - {Key key, - @required this.query, - this.listLoadingElement, - this.duration = const Duration(milliseconds: 300), - this.scrollPhysics, - this.scrollController, - this.scrollDirection = Axis.vertical, - this.padding, - this.primary, - this.reverse = false, - this.childBuilder, - this.shrinkWrap = false, - this.removedItemBuilder}) - : super(key: key); + const ParseLiveListWidget({ + Key key, + @required this.query, + this.listLoadingElement, + this.duration = const Duration(milliseconds: 300), + this.scrollPhysics, + this.scrollController, + this.scrollDirection = Axis.vertical, + this.padding, + this.primary, + this.reverse = false, + this.childBuilder, + this.shrinkWrap = false, + this.removedItemBuilder, + this.listenOnAllSubItems, + this.listeningIncludes, + this.lazyLoading = true, + }) : super(key: key); final QueryBuilder query; final Widget listLoadingElement; @@ -376,9 +784,19 @@ class ParseLiveListWidget extends StatefulWidget { final ChildBuilder childBuilder; final ChildBuilder removedItemBuilder; + final bool listenOnAllSubItems; + final List listeningIncludes; + + final bool lazyLoading; + @override - _ParseLiveListWidgetState createState() => - _ParseLiveListWidgetState(query, removedItemBuilder); + _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( + query: query, + removedItemBuilder: removedItemBuilder, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + ); static Widget defaultChildBuilder( BuildContext context, ParseLiveListElementSnapshot snapshot) { @@ -402,8 +820,18 @@ class ParseLiveListWidget extends StatefulWidget { class _ParseLiveListWidgetState extends State> { - _ParseLiveListWidgetState(this._query, this.removedItemBuilder) { - ParseLiveList.create(_query).then((ParseLiveList value) { + _ParseLiveListWidgetState( + {@required this.query, + @required this.removedItemBuilder, + bool listenOnAllSubItems, + List listeningIncludes, + bool lazyLoading = true}) { + ParseLiveList.create( + query, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + ).then((ParseLiveList value) { setState(() { _liveList = value; _liveList.stream.listen((ParseLiveListEvent event) { @@ -432,7 +860,7 @@ class _ParseLiveListWidgetState }); } - final QueryBuilder _query; + final QueryBuilder query; ParseLiveList _liveList; final GlobalKey _animatedListKey = GlobalKey(); @@ -459,7 +887,8 @@ class _ParseLiveListWidgetState itemBuilder: (BuildContext context, int index, Animation animation) { return ParseLiveListElementWidget( - key: ValueKey(_liveList?.idOf(index) ?? '_NotFound'), + key: ValueKey( + _liveList?.getIdentifier(index) ?? '_NotFound'), stream: () => _liveList?.getAt(index), loadedData: () => _liveList?.getLoadedAt(index), sizeFactor: animation, @@ -472,7 +901,7 @@ class _ParseLiveListWidgetState @override void dispose() { - _liveList.dispose(); + _liveList?.dispose(); _liveList = null; super.dispose(); } @@ -505,7 +934,6 @@ class _ParseLiveListElementWidgetState with SingleTickerProviderStateMixin { _ParseLiveListElementWidgetState( DataGetter loadedDataGetter, StreamGetter stream) { -// loadedData = loadedDataGetter(); _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); if (stream != null) { _streamSubscription = stream().listen( diff --git a/lib/src/utils/parse_logger.dart b/lib/src/utils/parse_logger.dart index 50fce8190..cb10ef8f5 100644 --- a/lib/src/utils/parse_logger.dart +++ b/lib/src/utils/parse_logger.dart @@ -21,7 +21,7 @@ void logAPIResponse(String className, String type, responseString += '\nType: ${parseResponse.error.type}'; final String errorOrException = - parseResponse.error.isTypeOfException ? 'Exception' : 'Error'; + parseResponse.error.exception != null ? 'Exception' : 'Error'; responseString += '\n$errorOrException: ${parseResponse.error.message}'; } diff --git a/lib/src/utils/parse_login_helpers.dart b/lib/src/utils/parse_login_helpers.dart index ae7459de7..483061c95 100644 --- a/lib/src/utils/parse_login_helpers.dart +++ b/lib/src/utils/parse_login_helpers.dart @@ -7,3 +7,11 @@ Map facebook(String token, String id, DateTime expires) { 'expiration_date': expires.toString() }; } + +Map google(String token, String id, String idToken) { + return { + 'access_token': token, + 'id': id, + 'id_token': idToken + }; +} diff --git a/lib/src/utils/parse_utils.dart b/lib/src/utils/parse_utils.dart index 0180f0021..0fa7b4c2b 100644 --- a/lib/src/utils/parse_utils.dart +++ b/lib/src/utils/parse_utils.dart @@ -65,8 +65,8 @@ String removeTrailingSlash(String serverUrl) { } } -Future batchRequest(List requests, - List objects, +Future batchRequest( + List requests, List objects, {ParseHTTPClient client, bool debug}) async { debug = isDebugEnabled(objectLevelDebug: debug); client = client ?? diff --git a/pubspec.yaml b/pubspec.yaml index 1d024c925..513807a96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,36 +1,34 @@ name: parse_server_sdk description: Flutter plugin for Parse Server, (https://parseplatform.org), (https://back4app.com) -version: 1.0.26 +version: 1.0.27 homepage: https://github.com/phillwiggins/flutter_parse_sdk environment: sdk: ">=2.2.2 <3.0.0" dependencies: - http: ^0.12.0+4 + http: ^0.12.2 flutter: sdk: flutter # Networking web_socket_channel: ^1.1.0 - connectivity: ^0.4.6+2 + connectivity: ^0.4.9+2 #Database - sembast: ^2.3.0 - xxtea: ^2.0.2 - shared_preferences: ^0.5.6 + sembast: ^2.4.7+6 + xxtea: ^2.0.3 + shared_preferences: ^0.5.10 # Utils - path_provider: ^1.5.1 - uuid: ^2.0.4 - package_info: ^0.4.0+13 - devicelocale: ^0.2.1 + path_provider: ^1.6.14 + uuid: ^2.2.2 + package_info: ^0.4.3 + devicelocale: ^0.3.1 meta: ^1.1.8 - path: ^1.6.4 + path: ^1.7.0 dev_dependencies: # Testing - test: ^1.5.1 - mockito: ^4.1.0 - flutter_test: - sdk: flutter + test: ^1.15.3 + mockito: ^4.1.1