diff --git a/packages/dart/CHANGELOG.md b/packages/dart/CHANGELOG.md index 60e84f80e..4fb53def1 100644 --- a/packages/dart/CHANGELOG.md +++ b/packages/dart/CHANGELOG.md @@ -1,3 +1,9 @@ +## [6.2.0](https://github.com/parse-community/Parse-SDK-Flutter/compare/dart-6.1.0...dart-6.2.0) (2023-10-18) + +### Features + +* Added `saveEventually` and `deleteEventually` in `ParseObject` ([#911](https://github.com/parse-community/Parse-SDK-Flutter/pull/911)) + ## [6.1.0](https://github.com/parse-community/Parse-SDK-Flutter/compare/dart-6.0.0...dart-6.1.0) (2023-10-17) ### Features diff --git a/packages/dart/lib/parse_server_sdk.dart b/packages/dart/lib/parse_server_sdk.dart index 93ada12ac..847c1bd61 100644 --- a/packages/dart/lib/parse_server_sdk.dart +++ b/packages/dart/lib/parse_server_sdk.dart @@ -27,62 +27,116 @@ export 'src/network/parse_dio_client.dart'; export 'src/network/parse_http_client.dart'; part 'src/base/parse_constants.dart'; + part 'src/data/parse_core_data.dart'; + part 'src/data/parse_subclass_handler.dart'; + part 'src/enums/parse_enum_api_rq.dart'; + part 'src/network/options.dart'; + part 'src/network/parse_client.dart'; + part 'src/network/parse_connectivity.dart'; + part 'src/network/parse_live_query.dart'; + part 'src/network/parse_query.dart'; + part 'src/objects/parse_acl.dart'; + part 'src/objects/parse_array.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_number.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_object.dart'; + part 'src/objects/parse_exception.dart'; + part 'src/objects/parse_operation/parse_add_operation.dart'; + part 'src/objects/parse_operation/parse_add_relation_operation.dart'; + part 'src/objects/parse_operation/parse_add_unique_operation.dart'; + part 'src/objects/parse_operation/parse_increment_operation.dart'; + part 'src/objects/parse_operation/parse_operation.dart'; + part 'src/objects/parse_operation/parse_remove_operation.dart'; + part 'src/objects/parse_operation/parse_remove_relation_operation.dart'; + part 'src/objects/parse_relation.dart'; + part 'src/objects/parse_response.dart'; + part 'src/objects/parse_save_state_aware_child.dart'; + part 'src/objects/parse_session.dart'; + part 'src/objects/parse_user.dart'; + part 'src/objects/response/parse_error_response.dart'; + part 'src/objects/response/parse_exception_response.dart'; + part 'src/objects/response/parse_response_builder.dart'; + part 'src/objects/response/parse_response_utils.dart'; + part 'src/objects/response/parse_success_no_results.dart'; + part 'src/storage/core_store.dart'; + part 'src/storage/core_store_memory.dart'; + part 'src/storage/core_store_sem_impl.dart'; + part 'src/storage/xxtea_codec.dart'; + part 'src/utils/parse_date_format.dart'; + part 'src/utils/parse_decoder.dart'; + part 'src/utils/parse_encoder.dart'; + part 'src/utils/parse_live_list.dart'; + part 'src/utils/parse_logger.dart'; + part 'src/utils/parse_login_helpers.dart'; + part 'src/utils/parse_utils.dart'; + part 'src/utils/valuable.dart'; class Parse { bool _hasBeenInitialized = false; + static bool objectsExistForEventually = false; + /// To initialize Parse Server in your application /// /// This should be initialized in MyApp() creation @@ -148,6 +202,12 @@ class Parse { _hasBeenInitialized = true; + objectsExistForEventually = await checkObjectsExistForEventually(); + + if (objectsExistForEventually) { + ParseObject.submitEventually(); + } + return this; } diff --git a/packages/dart/lib/src/base/parse_constants.dart b/packages/dart/lib/src/base/parse_constants.dart index d3030f0f3..b7768b217 100644 --- a/packages/dart/lib/src/base/parse_constants.dart +++ b/packages/dart/lib/src/base/parse_constants.dart @@ -1,7 +1,7 @@ part of flutter_parse_sdk; // Library -const String keySdkVersion = '6.1.0'; +const String keySdkVersion = '6.2.0'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points @@ -59,6 +59,8 @@ const String keyParamSessionToken = 'sessionToken'; // Storage const String keyParseStoreBase = 'flutter_parse_sdk_'; const String keyParseStoreUser = '${keyParseStoreBase}user'; +const String keyParseStoreObjects = '${keyParseStoreBase}objects'; +const String keyParseStoreDeletes = '${keyParseStoreBase}deletes'; const String keyParseStoreInstallation = '${keyParseStoreBase}installation'; // Installation @@ -82,5 +84,6 @@ const String keyVarInstallationId = 'installationId'; // Error const String keyError = 'error'; const String keyCode = 'code'; +const String keyNetworkError = 'NetworkError'; const bool parseIsWeb = identical(0, 0.0); diff --git a/packages/dart/lib/src/network/parse_dio_client.dart b/packages/dart/lib/src/network/parse_dio_client.dart index c262fe254..ddf12e548 100644 --- a/packages/dart/lib/src/network/parse_dio_client.dart +++ b/packages/dart/lib/src/network/parse_dio_client.dart @@ -229,6 +229,8 @@ class _ParseDioClient with dio.DioMixin implements dio.Dio { _logCUrl(options, data, path); } + checkForSubmitEventually(); + return super.request( path, data: data, diff --git a/packages/dart/lib/src/network/parse_http_client.dart b/packages/dart/lib/src/network/parse_http_client.dart index 86210be50..cf21724b7 100644 --- a/packages/dart/lib/src/network/parse_http_client.dart +++ b/packages/dart/lib/src/network/parse_http_client.dart @@ -158,6 +158,8 @@ class _ParseHTTPClient extends http.BaseClient { _logCUrl(request); } + checkForSubmitEventually(); + return _client.send(request); } diff --git a/packages/dart/lib/src/objects/parse_object.dart b/packages/dart/lib/src/objects/parse_object.dart index 09891c2f1..33ce02a5b 100644 --- a/packages/dart/lib/src/objects/parse_object.dart +++ b/packages/dart/lib/src/objects/parse_object.dart @@ -168,6 +168,25 @@ class ParseObject extends ParseBase implements ParseCloneable { } } + Future saveEventually() async { + // save object + final ParseResponse response = await save(); + + if (response.success) { + // return success response + return response; + } else { + // if have network connection error + if ((response.error?.message ?? "").contains(keyNetworkError)) { + // save this object in CoreStore + await _addThisObjectToParseCoreDataList(keyParseStoreObjects); + } else { + return response; + } + } + return response; + } + /// Saves the current object online. /// /// If the object not saved yet, this will create it. Otherwise, @@ -201,7 +220,7 @@ class ParseObject extends ParseBase implements ParseCloneable { /// /// Prefer using [save] over [update] and [create] Future save({dynamic context}) async { - final ParseResponse childrenResponse = await _saveChildren(this); + final ParseResponse childrenResponse = await _saveChildren(this, _client); if (childrenResponse.success) { ParseResponse? response; if (objectId == null) { @@ -222,7 +241,7 @@ class ParseObject extends ParseBase implements ParseCloneable { return childrenResponse; } - Future _saveChildren(dynamic object) async { + static Future _saveChildren(dynamic object, client) async { final Set uniqueObjects = {}; final Set uniqueFiles = {}; if (!_collectionDirtyChildren( @@ -268,7 +287,7 @@ class ParseObject extends ParseBase implements ParseCloneable { // 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 + This splitting will split the overall array into segments of up to 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) { @@ -286,7 +305,7 @@ class ParseObject extends ParseBase implements ParseCloneable { final ParseResponse response = await batchRequest( requests, chunk, - client: _client, + client: client, ); totalResponse.success &= response.success; @@ -386,7 +405,7 @@ class ParseObject extends ParseBase implements ParseCloneable { return true; } - bool _collectionDirtyChildren( + static bool _collectionDirtyChildren( dynamic object, Set uniqueObjects, Set uniqueFiles, @@ -640,6 +659,32 @@ class ParseObject extends ParseBase implements ParseCloneable { } } + Future deleteEventually() async { + // save object + final ParseResponse response = await delete(); + + if (response.success) { + // return success response + return response; + } else { + // if have network connection error + if ((response.error?.message ?? "").contains(keyNetworkError)) { + // save this object in CoreStore + await _addThisObjectToParseCoreDataList(keyParseStoreDeletes); + } else { + return response; + } + } + return response; + } + + Future _addThisObjectToParseCoreDataList(String key) async { + final CoreStore coreStore = ParseCoreData().getStore(); + List list = await coreStore.getStringList(key) ?? []; + list.add(json.encode(toJson(full: true))); + await coreStore.setStringList(key, list); + } + /// Deletes the current object locally and online Future delete({ String? id, @@ -692,4 +737,114 @@ class ParseObject extends ParseBase implements ParseCloneable { return this; } } + + static Future submitEventually( + {ParseClient? client, bool? autoSendSessionId}) async { + await submitSaveEventually( + client: client, autoSendSessionId: autoSendSessionId); + await submitDeleteEventually( + client: client, autoSendSessionId: autoSendSessionId); + + Parse.objectsExistForEventually = await checkObjectsExistForEventually(); + } + + static Future submitSaveEventually( + {ParseClient? client, bool? autoSendSessionId}) async { + // get client + ParseClient localClient = client ?? + ParseCoreData().clientCreator( + sendSessionId: + autoSendSessionId ?? ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); + + // preparation ParseCoreData + final CoreStore coreStore = ParseCoreData().getStore(); + + // save + // get json parse saved objects + List? listSaves = + await coreStore.getStringList(keyParseStoreObjects); + + if (listSaves != null) { + List parseObjectList = []; + for (var element in listSaves) { + // decode json + dynamic object = json.decode(element); + parseObjectList + .add(ParseObject(object[keyVarClassName]).fromJson(object)); + } + + // send parseObjects to server + ParseResponse response = + await ParseObject._saveChildren(parseObjectList, localClient); + + // if success clear all objects + if (response.success) { + coreStore.setStringList(keyParseStoreObjects, []); + } + + return response; + } + + return null; + } + + static Future submitDeleteEventually( + {ParseClient? client, bool? autoSendSessionId}) async { + // preparation ParseCoreData + final CoreStore coreStore = ParseCoreData().getStore(); + + // delete + // get json parse saved objects + List? listDeletes = + await coreStore.getStringList(keyParseStoreDeletes); + + if (listDeletes != null) { + int firstLength = listDeletes.length; + List elementsToRemove = []; + for (var element in listDeletes) { + // decode json + dynamic object = json.decode(element); + + // crate parse object + ParseObject parseObject = ParseObject(object[keyVarClassName], + client: client, autoSendSessionId: autoSendSessionId) + .fromJson(object); + + // delete parse object + ParseResponse deleteResponse = await parseObject.delete(); + + if (deleteResponse.success) { + // remove from list Deletes + elementsToRemove.add(element); + } + } + + // Remove the elements after the loop + for (var elementToRemove in elementsToRemove) { + listDeletes.remove(elementToRemove); + } + + // set new list deletes in coreStore + coreStore.setStringList(keyParseStoreDeletes, listDeletes); + + bool success = true; + int statusCode = 200; + + if (listDeletes.length == firstLength) { + // Nothing has been deleted + success = false; + statusCode = -1; + } + + final ParseResponse response = ParseResponse() + ..success = success + ..results = [] + ..statusCode = statusCode; + + return response; + } + + return null; + } } diff --git a/packages/dart/lib/src/utils/parse_utils.dart b/packages/dart/lib/src/utils/parse_utils.dart index 03bb37781..4a122952b 100644 --- a/packages/dart/lib/src/utils/parse_utils.dart +++ b/packages/dart/lib/src/utils/parse_utils.dart @@ -127,3 +127,41 @@ List removeDuplicateParseObjectByObjectId(Iterable iterable) { return list; } + +// check the coreStore for existing objects to delete or save eventually +Future checkObjectsExistForEventually() async { + // preparation ParseCoreData + final CoreStore coreStore = ParseCoreData().getStore(); + + List? listSaves = await coreStore.getStringList(keyParseStoreObjects); + + if (listSaves != null) { + if (listSaves.isNotEmpty) { + return true; + } + } + + List? listDeletes = + await coreStore.getStringList(keyParseStoreDeletes); + + if (listDeletes != null) { + if (listDeletes.isNotEmpty) { + return true; + } + } + + return false; +} + +// To get out of the cycle +bool _inSubmitEventually = false; + +Future checkForSubmitEventually() async { + if (_inSubmitEventually) return; + + if (Parse.objectsExistForEventually) { + _inSubmitEventually = true; + await ParseObject.submitEventually(); + _inSubmitEventually = false; + } +} diff --git a/packages/dart/pubspec.yaml b/packages/dart/pubspec.yaml index 4ef783246..87b45f563 100644 --- a/packages/dart/pubspec.yaml +++ b/packages/dart/pubspec.yaml @@ -1,6 +1,6 @@ name: parse_server_sdk description: The Dart SDK to connect to Parse Server. Build your apps faster with Parse Platform, the complete application stack. -version: 6.1.0 +version: 6.2.0 homepage: https://parseplatform.org repository: https://github.com/parse-community/Parse-SDK-Flutter issue_tracker: https://github.com/parse-community/Parse-SDK-Flutter/issues diff --git a/packages/dart/test/src/objects/parse_object/parse_object_delete_eventually_test.dart b/packages/dart/test/src/objects/parse_object/parse_object_delete_eventually_test.dart new file mode 100644 index 000000000..0e333eb13 --- /dev/null +++ b/packages/dart/test/src/objects/parse_object/parse_object_delete_eventually_test.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:mockito/mockito.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:test/test.dart'; + +import '../../../parse_query_test.mocks.dart'; +import '../../../test_utils.dart'; + +void main() { + setUpAll(() async { + await initializeParse(); + }); + + group('deleteEventually()', () { + late MockParseClient client1; + late MockParseClient client2; + + late ParseObject dietPlansObject; + + setUp(() { + client1 = MockParseClient(); + client2 = MockParseClient(); + + dietPlansObject = ParseObject("Diet_Plans", client: client1); + + dietPlansObject.objectId = "fakeObjectId"; + }); + + test( + 'should exist parse object in ParseCoreData next saveEventually', + () async { + // arrange + when(client1.delete( + any, + options: anyNamed("options"), + )).thenThrow(Exception('NetworkError')); + + when(client2.delete( + "$serverUrl/classes/Diet_Plans/fakeObjectId", + options: anyNamed("options"), + )).thenAnswer( + (_) async => ParseNetworkResponse( + statusCode: 200, + data: jsonEncode([ + { + "success": { + "delete": "ok", + } + } + ]), + ), + ); + + // act + await dietPlansObject.deleteEventually(); + + final CoreStore coreStore = ParseCoreData().getStore(); + List list = + await coreStore.getStringList(keyParseStoreDeletes) ?? []; + + // assert + expect(list.length, 1); + + // act + await ParseObject.submitEventually(client: client2); + + List list2 = + await coreStore.getStringList(keyParseStoreDeletes) ?? []; + + // assert + expect(list2.length, 0); + }, + ); + }); +} diff --git a/packages/dart/test/src/objects/parse_object/parse_object_save_eventually_test.dart b/packages/dart/test/src/objects/parse_object/parse_object_save_eventually_test.dart new file mode 100644 index 000000000..5ba020a01 --- /dev/null +++ b/packages/dart/test/src/objects/parse_object/parse_object_save_eventually_test.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:mockito/mockito.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:test/test.dart'; + +import '../../../parse_query_test.mocks.dart'; +import '../../../test_utils.dart'; + +void main() { + setUpAll(() async { + await initializeParse(); + }); + + group('saveEventually()', () { + late MockParseClient client; + + late ParseObject dietPlansObject; + + setUp(() { + client = MockParseClient(); + + dietPlansObject = ParseObject("Diet_Plans", client: client); + }); + + test( + 'should exist parse object in ParseCoreData next saveEventually', + () async { + // arrange + when(client.post( + any, + options: anyNamed("options"), + data: anyNamed("data"), + )).thenThrow(Exception('NetworkError')); + + when(client.post( + "$serverUrl/batch", + options: anyNamed("options"), + data: anyNamed("data"), + )).thenAnswer( + (_) async => ParseNetworkResponse( + statusCode: 200, + data: jsonEncode([ + { + "success": { + "add": "ok", + } + } + ]), + ), + ); + + // act + await dietPlansObject.saveEventually(); + + final CoreStore coreStore = ParseCoreData().getStore(); + List list = + await coreStore.getStringList(keyParseStoreObjects) ?? []; + + // assert + expect(list.length, 1); + + // act + await ParseObject.submitEventually(client: client); + + List list2 = + await coreStore.getStringList(keyParseStoreObjects) ?? []; + + // assert + expect(list2.length, 0); + }, + ); + }); +}