diff --git a/README.md b/README.md index 66405aff8..1261f5d99 100644 --- a/README.md +++ b/README.md @@ -154,9 +154,9 @@ var response = await dietPlan.remove("listKeywords", ["a"]); or using with save function ```dart -dietPlan.setAdd('listKeywords', ['a','a','d']); -dietPlan.setAddUnique('listKeywords', ['a','a','d']); -dietPlan.setRemove('listKeywords', ['a']); +dietPlan.setAddAll('listKeywords', ['a','a','d']); +dietPlan.setAddAllUnique('listKeywords', ['a','a','d']); +dietPlan.setRemoveAll('listKeywords', ['a']); var response = dietPlan.save() ``` diff --git a/example/lib/main.dart b/example/lib/main.dart index d18963a20..f861e776d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -105,7 +105,7 @@ class _MyAppState extends State { Future initInstallation() async { final ParseInstallation installation = - await ParseInstallation.currentInstallation(); + await ParseInstallation.currentInstallation(); final ParseResponse response = await installation.create(); print(response); } @@ -244,13 +244,13 @@ class _MyAppState extends State { /// Update current user from server - Best done to verify user is still a valid user response = await ParseUser.getCurrentUserFromServer( token: user?.get(keyHeaderSessionToken)); - if (response.success) { + if (response?.success ?? false) { user = response.result; } /// log user out - response = await user.logout(); - if (response.success) { + response = await user?.logout(); + if (response?.success ?? false) { user = response.result; } diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index dc057537a..0112b5791 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -3,6 +3,7 @@ library flutter_parse_sdk; import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:sembast/sembast.dart'; import 'package:sembast/sembast_io.dart'; diff --git a/lib/src/data/core_store_impl.dart b/lib/src/data/core_store_impl.dart index 8448ac8cd..93cf5e406 100644 --- a/lib/src/data/core_store_impl.dart +++ b/lib/src/data/core_store_impl.dart @@ -16,7 +16,7 @@ class CoreStoreImp implements CoreStore { if (Platform.isIOS || Platform.isAndroid) { dbDirectory = (await getApplicationDocumentsDirectory()).path; - final String dbPath = path.join('$dbDirectory+/parse', 'parse.db'); + final String dbPath = path.join('$dbDirectory/parse', 'parse.db'); final Database db = await factory.openDatabase(dbPath, codec: codec); _instance = CoreStoreImp._internal(db); } diff --git a/lib/src/enums/parse_enum_api_rq.dart b/lib/src/enums/parse_enum_api_rq.dart index 73eafd895..ae4651151 100644 --- a/lib/src/enums/parse_enum_api_rq.dart +++ b/lib/src/enums/parse_enum_api_rq.dart @@ -31,5 +31,6 @@ enum ParseApiRQ { decrement, getConfigs, addConfig, - liveQuery + liveQuery, + batch } diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index a0ea8350b..4aa0f5130 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -206,7 +206,7 @@ class ParseInstallation extends ParseObject { ///Subscribes the device to a channel of push notifications. void subscribeToChannel(String value) { final List channel = [value]; - setAddUnique('channels', channel); + setAddAllUnique('channels', channel); save(); } diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 404e28643..1c07ef550 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -68,13 +68,6 @@ class ParseObject extends ParseBase implements ParseCloneable { final String body = json.encode(toJson(forApiRQ: true)); final Response result = await _client.post(url, body: body); - //Set the objectId on the object after it is created. - //This allows you to perform operations on the object after creation - if (result.statusCode == 201) { - final Map map = json.decode(result.body); - objectId = map['objectId'].toString(); - } - return handleResponse( this, result, ParseApiRQ.create, _debug, className); } on Exception catch (e) { @@ -82,24 +75,189 @@ class ParseObject extends ParseBase implements ParseCloneable { } } + Future update() async { + try { + final Uri url = getSanitisedUri(_client, '$_path/$objectId'); + final String body = json.encode(toJson(forApiRQ: true)); + final Response result = await _client.put(url, body: body); + return handleResponse( + this, result, ParseApiRQ.save, _debug, className); + } on Exception catch (e) { + return handleException(e, ParseApiRQ.save, _debug, className); + } + } + /// Saves the current object online Future save() async { - if (getObjectData()[keyVarObjectId] == null) { - return create(); + final ParseResponse response = await _saveChildren(this); + if (response.success) { + if (objectId == null) { + return create(); + } else { + return update(); + } } else { - try { - final Uri url = getSanitisedUri(_client, '$_path/$objectId'); - final String body = json.encode(toJson(forApiRQ: true)); - final Response result = await _client.put(url, body: body); - return handleResponse( - this, result, ParseApiRQ.save, _debug, className); - } on Exception catch (e) { - return handleException(e, ParseApiRQ.save, _debug, className); + return response; + } + } + + Future _saveChildren(dynamic object) async { + final Set uniqueObjects = Set(); + final Set uniqueFiles = Set(); + if (!_collectionDirtyChildren(object, uniqueObjects, uniqueFiles, + Set(), Set())) { + final ParseResponse response = ParseResponse(); + return response; + } + if (object is ParseObject) { + uniqueObjects.remove(object); + } + for (ParseFile file in uniqueFiles) { + final ParseResponse response = await file.save(); + if (!response.success) { + return response; + } + } + List remaining = uniqueObjects.toList(); + final List finished = List(); + final ParseResponse totalResponse = ParseResponse() + ..success = true + ..results = List() + ..statusCode = 200; + while (remaining.isNotEmpty) { + /* Partition the objects into two sets: those that can be save immediately, + and those that rely on other objects to be created first. */ + final List current = List(); + final List nextBatch = List(); + for (ParseObject object in remaining) { + if (object._canbeSerialized(finished)) { + current.add(object); + } else { + nextBatch.add(object); + } + } + remaining = nextBatch; + // TODO(yulingtianxia): lazy User + /* Batch requests have currently a limit of 50 packaged requests per single request + This splitting will split the overall array into segments of upto 50 requests + and execute them concurrently with a wrapper task for all of them. */ + final List> chunks = []; + for (int i = 0; i < current.length; i += 50) { + chunks.add(current.sublist(i, min(current.length, i + 50))); + } + + for (List chunk in chunks) { + final List requests = chunk.map((ParseObject obj) { + return obj.getRequestJson(obj.objectId == null ? 'POST' : 'PUT'); + }).toList(); + final ParseResponse response = await batchRequest(requests, chunk); + totalResponse.success &= response.success; + if (response.success) { + totalResponse.results.addAll(response.results); + totalResponse.count += response.count; + } else { + // TODO(yulingtianxia): If there was an error, we want to roll forward the save changes before rethrowing. + totalResponse.statusCode = response.statusCode; + totalResponse.error = response.error; + } + } + finished.addAll(current); + } + return totalResponse; + } + + dynamic getRequestJson(String method) { + final Uri tempUri = Uri.parse(_client.data.serverUrl); + final String parsePath = tempUri.path; + final dynamic request = { + 'method': method, + 'path': '$parsePath$_path' + (objectId != null ? '/$objectId' : ''), + 'body': toJson(forApiRQ: true) + }; + return request; + } + + bool _canbeSerialized(List aftersaving, {dynamic value}) { + if (value != null) { + if (value is ParseObject) { + if (value.objectId == null && !aftersaving.contains(value)) { + return false; + } + } else if (value is Map) { + for (dynamic child in value.values) { + if (!_canbeSerialized(aftersaving, value: child)) { + return false; + } + } + } else if (value is List) { + for (dynamic child in value) { + if (!_canbeSerialized(aftersaving, value: child)) { + return false; + } + } } + } else if (!_canbeSerialized(aftersaving, value: getObjectData())) { + return false; } + // TODO(yulingtianxia): handle ACL + return true; } - /// Get the instance of ParseRelation class associated with the given key. + bool _collectionDirtyChildren(dynamic object, Set uniqueObjects, + Set uniqueFiles, Set seen, Set seenNew) { + if (object is List) { + for (dynamic child in object) { + if (!_collectionDirtyChildren( + child, uniqueObjects, uniqueFiles, seen, seenNew)) { + return false; + } + } + } else if (object is Map) { + for (dynamic child in object.values) { + if (!_collectionDirtyChildren( + child, uniqueObjects, uniqueFiles, seen, seenNew)) { + return false; + } + } + } else if (object is ParseACL) { + // TODO(yulingtianxia): handle ACL + } else if (object is ParseFile) { + if (object.url == null) { + uniqueFiles.add(object); + } + } else if (object is ParseObject) { + /* Check for cycles of new objects. Any such cycle means it will be + impossible to save this collection of objects, so throw an exception. */ + if (object.objectId != null) { + seenNew = Set(); + } else { + if (seenNew.contains(object)) { + // TODO(yulingtianxia): throw an error? + return false; + } + seenNew.add(object); + } + + /* Check for cycles of any object. If this occurs, then there's no + problem, but we shouldn't recurse any deeper, because it would be + an infinite recursion. */ + if (seen.contains(object)) { + return true; + } + seen.add(object); + + if (!_collectionDirtyChildren( + object.getObjectData(), uniqueObjects, uniqueFiles, seen, seenNew)) { + return false; + } + + // TODO(yulingtianxia): Check Dirty + uniqueObjects.add(object); + } + return true; + } + + /// Get the instance of ParseRelation class associated with the given key. ParseRelation getRelation(String key) { return ParseRelation(parent: this, key: key); } @@ -115,8 +273,8 @@ class ParseObject extends ParseBase implements ParseCloneable { } /// Removes an element from an Array - void setRemove(String key, dynamic values) { - _arrayOperation('Remove', key, values); + void setRemove(String key, dynamic value) { + _arrayOperation('Remove', key, [value]); } /// Remove multiple elements from an array of an object @@ -159,8 +317,11 @@ class ParseObject extends ParseBase implements ParseCloneable { } } + void setAddUnique(String key, dynamic value) { + _arrayOperation('AddUnique', key, [value]); + } /// Add a multiple elements to an array of an object - void setAddUnique(String key, List values) { + void setAddAllUnique(String key, List values) { _arrayOperation('AddUnique', key, values); } @@ -175,8 +336,8 @@ class ParseObject extends ParseBase implements ParseCloneable { } /// Add a single element to an array of an object - void setAdd(String key, dynamic values) { - _arrayOperation('Add', key, values); + void setAdd(String key, dynamic value) { + _arrayOperation('Add', key, [value]); } void addRelation(String key, List values) { @@ -208,6 +369,7 @@ class ParseObject extends ParseBase implements ParseCloneable { /// Used in array Operations in save() method void _arrayOperation(String arrayAction, String key, List values) { + // TODO(yulingtianxia): Array operations should be incremental. Merge add and remove operation. set>( key, {'__op': arrayAction, 'objects': values}); } diff --git a/lib/src/objects/parse_relation.dart b/lib/src/objects/parse_relation.dart index bd8ca8b95..ec0e8918b 100644 --- a/lib/src/objects/parse_relation.dart +++ b/lib/src/objects/parse_relation.dart @@ -19,9 +19,7 @@ class ParseRelation { if (object != null) { _targetClass = object.getClassName(); _objects.add(object); - _parent.addRelation(_key, _objects.map((T value) { - return value.toPointer(); - }).toList()); + _parent.addRelation(_key, _objects.toList()); } } @@ -29,9 +27,7 @@ class ParseRelation { if (object != null) { _targetClass = object.getClassName(); _objects.remove(object); - _parent.removeRelation(_key, _objects.map((T value) { - return value.toPointer(); - }).toList()); + _parent.removeRelation(_key, _objects.toList()); } } diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index a28e51c01..1a783eba9 100644 --- a/lib/src/objects/response/parse_response_builder.dart +++ b/lib/src/objects/response/parse_response_builder.dart @@ -8,10 +8,10 @@ part of flutter_parse_sdk; /// 3. Success with simple OK. /// 4. Success with results. Again [ParseResponse()] is returned class _ParseResponseBuilder { - ParseResponse handleResponse(dynamic object, Response apiResponse, - {bool returnAsResult = false}) { + ParseResponse handleResponse( + dynamic object, Response apiResponse, ParseApiRQ type) { final ParseResponse parseResponse = ParseResponse(); - + final bool returnAsResult = shouldReturnAsABaseResult(type); if (apiResponse != null) { parseResponse.statusCode = apiResponse.statusCode; @@ -27,7 +27,7 @@ class _ParseResponseBuilder { return _handleSuccessWithoutParseObject( parseResponse, object, apiResponse.body); } else { - return _handleSuccess(parseResponse, object, apiResponse.body); + return _handleSuccess(parseResponse, object, apiResponse.body, type); } } else { parseResponse.error = ParseError( @@ -60,30 +60,49 @@ class _ParseResponseBuilder { } /// Handles successful response with results - ParseResponse _handleSuccess( - ParseResponse response, dynamic object, String responseBody) { + ParseResponse _handleSuccess(ParseResponse response, dynamic object, + String responseBody, ParseApiRQ type) { response.success = true; - final Map map = json.decode(responseBody); - - if (object is Parse) { - response.result = map; - } else if (map != null && map.length == 1 && map.containsKey('results')) { - final List results = map['results']; - final List items = _handleMultipleResults(object, results); - response.results = items; - response.result = items; - response.count = items.length; - } else if (map != null && map.length == 2 && map.containsKey('count')) { - final List results = [map['count']]; - response.results = results; - response.result = results; - response.count = map['count']; - } else { - final T item = _handleSingleResult(object, map, false); - response.count = 1; - response.result = item; - response.results = [item]; + final dynamic result = json.decode(responseBody); + + if (type == ParseApiRQ.batch) { + final List list = result; + if (object is List && object.length == list.length) { + response.count = object.length; + response.results = List(); + for (int i = 0; i < object.length; i++) { + final Map objectResult = list[i]; + if (objectResult.containsKey('success')) { + final T item = _handleSingleResult(object[i], objectResult['success'], false); + response.results.add(item); + } else { + final ParseError error = ParseError(code: objectResult[keyCode], message: objectResult[keyError].toString()); + response.results.add(error); + } + } + } + } else if (result is Map) { + final Map map = result; + if (object is Parse) { + response.result = map; + } else if (map != null && map.length == 1 && map.containsKey('results')) { + final List results = map['results']; + final List items = _handleMultipleResults(object, results); + response.results = items; + response.result = items; + response.count = items.length; + } else if (map != null && map.length == 2 && map.containsKey('count')) { + final List results = [map['count']]; + response.results = results; + response.result = results; + response.count = map['count']; + } else { + final T item = _handleSingleResult(object, map, false); + response.count = 1; + response.result = item; + response.results = [item]; + } } return response; @@ -92,7 +111,6 @@ class _ParseResponseBuilder { /// Handles a response with a multiple result object List _handleMultipleResults(dynamic object, List data) { final List resultsList = List(); - for (dynamic value in data) { resultsList.add(_handleSingleResult(object, value, true)); } @@ -115,4 +133,4 @@ class _ParseResponseBuilder { bool isHealthCheck(Response apiResponse) { return apiResponse.body == '{\"status\":\"ok\"}'; } -} +} \ No newline at end of file diff --git a/lib/src/objects/response/parse_response_utils.dart b/lib/src/objects/response/parse_response_utils.dart index 12a96bc06..db209f58b 100644 --- a/lib/src/objects/response/parse_response_utils.dart +++ b/lib/src/objects/response/parse_response_utils.dart @@ -2,11 +2,10 @@ part of flutter_parse_sdk; /// Handles an API response and logs data if [bool] debug is enabled @protected -ParseResponse handleResponse(ParseCloneable object, Response response, +ParseResponse handleResponse(dynamic object, Response response, ParseApiRQ type, bool debug, String className) { - final ParseResponse parseResponse = _ParseResponseBuilder().handleResponse( - object, response, - returnAsResult: shouldReturnAsABaseResult(type)); + final ParseResponse parseResponse = + _ParseResponseBuilder().handleResponse(object, response, type); if (debug) { logAPIResponse(className, type.toString(), parseResponse); @@ -20,7 +19,7 @@ ParseResponse handleResponse(ParseCloneable object, Response response, ParseResponse handleException( Exception exception, ParseApiRQ type, bool debug, String className) { final ParseResponse parseResponse = - buildParseResponseWithException(exception); + buildParseResponseWithException(exception); if (debug) { logAPIResponse(className, type.toString(), parseResponse); @@ -51,8 +50,14 @@ bool isUnsuccessfulResponse(Response apiResponse) => apiResponse.statusCode != 200 && apiResponse.statusCode != 201; bool isSuccessButNoResults(Response apiResponse) { - final Map decodedResponse = jsonDecode(apiResponse.body); - final List results = decodedResponse['results']; + final dynamic decodedResponse = jsonDecode(apiResponse.body); + List results; + if (decodedResponse is Map) { + results = decodedResponse['results']; + } else if (decodedResponse is List) { + results = decodedResponse; + } + if (results == null) { return false; diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index 31545a076..800526e42 100644 --- a/lib/src/utils/parse_encoder.dart +++ b/lib/src/utils/parse_encoder.dart @@ -26,6 +26,12 @@ dynamic parseEncode(dynamic value, {bool full}) { }).toList(); } + if (value is Map) { + value.forEach((dynamic k, dynamic v) { + value[k] = parseEncode(v); + }); + } + if (value is ParseGeoPoint) { return value; } diff --git a/lib/src/utils/parse_utils.dart b/lib/src/utils/parse_utils.dart index 7ec88e4a5..d8f88a0b9 100644 --- a/lib/src/utils/parse_utils.dart +++ b/lib/src/utils/parse_utils.dart @@ -39,11 +39,46 @@ Uri getSanitisedUri(ParseHTTPClient client, String pathToAppend, return url; } +/// Sanitises a url +Uri getCustomUri(ParseHTTPClient client, String path, + {Map queryParams, String query}) { + final Uri tempUri = Uri.parse(client.data.serverUrl); + + final Uri url = Uri( + scheme: tempUri.scheme, + host: tempUri.host, + port: tempUri.port, + path: path, + queryParameters: queryParams, + query: query); + + return url; +} + /// Removes unncessary / String removeTrailingSlash(String serverUrl) { - if (serverUrl.substring(serverUrl.length - 1) == '/') { + if (serverUrl.isNotEmpty && serverUrl.substring(serverUrl.length - 1) == '/') { return serverUrl.substring(0, serverUrl.length - 1); } else { return serverUrl; } } + +Future batchRequest(List requests, + List objects, {ParseHTTPClient client, bool debug}) async { + debug = isDebugEnabled(objectLevelDebug: debug); + client = client ?? + ParseHTTPClient( + sendSessionId: ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); + try { + final Uri url = getSanitisedUri(client, '/batch'); + final String body = json.encode({'requests': requests}); + final Response result = await client.post(url, body: body); + + return handleResponse( + objects, result, ParseApiRQ.batch, debug, 'parse_utils'); + } on Exception catch (e) { + return handleException(e, ParseApiRQ.batch, debug, 'parse_utils'); + } +}