diff --git a/.gitignore b/.gitignore index e8ac88703..c4f26e041 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ build/ .idea example/ios/Frameworks/ example/lib/ui/ + +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 37975d799..eef1ece34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.22 + ## 1.0.21 LiveQuery fix Logout fix diff --git a/README.md b/README.md index 9419be136..07edae11c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,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.21 + parse_server_sdk: ^1.0.22 ``` 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. @@ -163,9 +163,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() ``` @@ -539,6 +539,21 @@ final Map params = {'plan': 'paid'}; function.execute(parameters: params); ``` +## Relation +The SDK supports Relation. + +To Retrive a relation instance for user, call: +```dart +final relation = user.getRelation('dietPlans'); +``` + +and then you can add a relation to the passed in object. + +```dart +relation.add(dietPlan); +final result = await user.save(); +``` + ## Other Features of this library Main: * Installation (View the example application) diff --git a/example/lib/data/repositories/user/provider_db_user.dart b/example/lib/data/repositories/user/provider_db_user.dart index d135418c8..a7e5380e9 100644 --- a/example/lib/data/repositories/user/provider_db_user.dart +++ b/example/lib/data/repositories/user/provider_db_user.dart @@ -79,6 +79,7 @@ class UserProviderDB implements UserProviderContract { Map convertItemToStorageMap(User item) { final Map values = Map(); + // ignore: invalid_use_of_protected_member values['value'] = json.jsonEncode(item.toJson(full: true)); values[keyVarObjectId] = item.objectId; item.updatedAt != null @@ -90,8 +91,7 @@ class UserProviderDB implements UserProviderContract { User convertRecordToItem({Record record, Map values}) { try { values ??= record.value; - final User item = - User.clone().fromJson(json.jsonDecode(values['value'])); + final User item = User.clone().fromJson(json.jsonDecode(values['value'])); return item; } catch (e) { return null; diff --git a/example/lib/main.dart b/example/lib/main.dart index 3e26e8e18..1acc91b32 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,7 +10,6 @@ import 'package:flutter_plugin_example/data/repositories/diet_plan/repository_di import 'package:flutter_plugin_example/data/repositories/user/repository_user.dart'; import 'package:flutter_plugin_example/domain/constants/application_constants.dart'; import 'package:flutter_plugin_example/domain/utils/db_utils.dart'; -import 'package:flutter_plugin_example/pages/decision_page.dart'; import 'package:flutter_stetho/flutter_stetho.dart'; import 'package:parse_server_sdk/parse_server_sdk.dart'; @@ -41,26 +40,32 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { DietPlanRepository dietPlanRepo; UserRepository userRepo; + + String text = ''; + @override void initState() { super.initState(); - // initData(); + initData(); } @override Widget build(BuildContext context) { return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - primarySwatch: Colors.blue, + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text(text), ), - title: 'Parse Server Example', - home: DecisionPage()); + ), + ); } Future initData() async { // Initialize repository - // await initRepository(); + await initRepository(); // Initialize parse Parse().initialize(keyParseApplicationId, keyParseServerUrl, @@ -68,42 +73,49 @@ class _MyAppState extends State { //parse serve with secure store and desktop support -// Parse().initialize(keyParseApplicationId, keyParseServerUrl, -// masterKey: keyParseMasterKey, -// debug: true, -// coreStore: CoreStoreImp.getInstance()); + // Parse().initialize(keyParseApplicationId, keyParseServerUrl, + // masterKey: keyParseMasterKey, + // debug: true, + // coreStore: CoreStoreImp.getInstance()); // Check server is healthy and live - Debug is on in this instance so check logs for result final ParseResponse response = await Parse().healthCheck(); if (response.success) { await runTestQueries(); - print('runTestQueries'); + text += 'runTestQueries\n'; + print(text); } else { - print('Server health check failed'); + text += 'Server health check failed'; + print(text); } } Future runTestQueries() async { // Basic repository example - //await repositoryAddUser(); - //await repositoryAddItems(); - //await repositoryGetAllItems(); + await repositoryAddUser(); + await repositoryAddItems(); + await repositoryGetAllItems(); //Basic usage - // createItem(); - // getAllItems(); - // getAllItemsByName(); - // getSingleItem(); - // getConfigs(); - // query(); - // initUser(); -// var instalattion = await ParseInstallation.currentInstallation(); -// var rees = instalattion.create(); -// print(rees); - //function(); - //functionWithParameters(); - // test(); + await createItem(); + await getAllItems(); + await getAllItemsByName(); + await getSingleItem(); + await getConfigs(); + await query(); + await initUser(); + await initInstallation(); + await function(); + await functionWithParameters(); + await test(); + } + + Future initInstallation() async { + final ParseInstallation installation = + await ParseInstallation.currentInstallation(); + final ParseResponse response = await installation.create(); + print(response); } Future test() async { @@ -240,13 +252,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; } @@ -359,13 +371,13 @@ class _MyAppState extends State { dietPlanRepo ??= DietPlanRepository.init(await getDB()); userRepo ??= UserRepository.init(await getDB()); } - - String dietPlansToAdd = - '[{"className":"Diet_Plans","Name":"Textbook","Description":"For an active lifestyle and a straight forward macro plan, we suggest this plan.","Fat":25,"Carbs":50,"Protein":25,"Status":0},' - '{"className":"Diet_Plans","Name":"Body Builder","Description":"Default Body Builders Diet","Fat":20,"Carbs":40,"Protein":40,"Status":0},' - '{"className":"Diet_Plans","Name":"Zone Diet","Description":"Popular with CrossFit users. Zone Diet targets similar macros.","Fat":30,"Carbs":40,"Protein":30,"Status":0},' - '{"className":"Diet_Plans","Name":"Low Fat","Description":"Low fat diet.","Fat":15,"Carbs":60,"Protein":25,"Status":0},' - '{"className":"Diet_Plans","Name":"Low Carb","Description":"Low Carb diet, main focus on quality fats and protein.","Fat":35,"Carbs":25,"Protein":40,"Status":0},' - '{"className":"Diet_Plans","Name":"Paleo","Description":"Paleo diet.","Fat":60,"Carbs":25,"Protein":10,"Status":0},' - '{"className":"Diet_Plans","Name":"Ketogenic","Description":"High quality fats, low carbs.","Fat":65,"Carbs":5,"Protein":30,"Status":0}]'; } + +const String dietPlansToAdd = + '[{"className":"Diet_Plans","Name":"Textbook","Description":"For an active lifestyle and a straight forward macro plan, we suggest this plan.","Fat":25,"Carbs":50,"Protein":25,"Status":0},' + '{"className":"Diet_Plans","Name":"Body Builder","Description":"Default Body Builders Diet","Fat":20,"Carbs":40,"Protein":40,"Status":0},' + '{"className":"Diet_Plans","Name":"Zone Diet","Description":"Popular with CrossFit users. Zone Diet targets similar macros.","Fat":30,"Carbs":40,"Protein":30,"Status":0},' + '{"className":"Diet_Plans","Name":"Low Fat","Description":"Low fat diet.","Fat":15,"Carbs":60,"Protein":25,"Status":0},' + '{"className":"Diet_Plans","Name":"Low Carb","Description":"Low Carb diet, main focus on quality fats and protein.","Fat":35,"Carbs":25,"Protein":40,"Status":0},' + '{"className":"Diet_Plans","Name":"Paleo","Description":"Paleo diet.","Fat":60,"Carbs":25,"Protein":10,"Status":0},' + '{"className":"Diet_Plans","Name":"Ketogenic","Description":"High quality fats, low carbs.","Fat":65,"Carbs":5,"Protein":30,"Status":0}]'; diff --git a/example/lib/pages/decision_page.dart b/example/lib/pages/decision_page.dart index 2875a3131..12ce0f405 100644 --- a/example/lib/pages/decision_page.dart +++ b/example/lib/pages/decision_page.dart @@ -88,7 +88,7 @@ class _DecisionPageState extends State { final MaterialPageRoute newRoute = MaterialPageRoute(builder: (BuildContext context) => page); - bool nav = await Navigator.of(context) + final bool nav = await Navigator.of(context) .pushAndRemoveUntil(newRoute, ModalRoute.withName('/')); if (nav == true) { _initParse(); diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 21578b102..00f569346 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -8,7 +8,8 @@ import 'package:flutter_plugin_example/data/repositories/diet_plan/contract_prov import 'package:parse_server_sdk/parse_server_sdk.dart'; class HomePage extends StatefulWidget { - HomePage(this._dietPlanProvider); + const HomePage(this._dietPlanProvider); + final DietPlanProviderContract _dietPlanProvider; @override @@ -16,7 +17,7 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - List randomDietPlans = []; + List randomDietPlans = []; @override void initState() { @@ -50,9 +51,9 @@ class _HomePageState extends State { body: _showDietList(), floatingActionButton: FloatingActionButton( onPressed: () async { - DietPlan dietPlan = + final DietPlan dietPlan = randomDietPlans[Random().nextInt(randomDietPlans.length - 1)]; - ParseUser user = await ParseUser.currentUser(); + final ParseUser user = await ParseUser.currentUser(); dietPlan.set('user', user); await widget._dietPlanProvider.add(dietPlan); setState(() {}); @@ -80,15 +81,15 @@ class _HomePageState extends State { shrinkWrap: true, itemCount: snapshot.data.results.length, itemBuilder: (BuildContext context, int index) { - DietPlan dietPlan = snapshot.data.results[index]; - String id = dietPlan.objectId; - String name = dietPlan.name; - String description = dietPlan.description; - bool status = dietPlan.status; + final DietPlan dietPlan = snapshot.data.results[index]; + final String id = dietPlan.objectId; + final String name = dietPlan.name; + final String description = dietPlan.description; + final bool status = dietPlan.status; return Dismissible( key: Key(id), background: Container(color: Colors.red), - onDismissed: (direction) async { + onDismissed: (DismissDirection direction) async { widget._dietPlanProvider.remove(dietPlan); }, child: ListTile( diff --git a/example/lib/pages/login_page.dart b/example/lib/pages/login_page.dart index e02824fd8..14e0d93ab 100644 --- a/example/lib/pages/login_page.dart +++ b/example/lib/pages/login_page.dart @@ -10,7 +10,7 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { - final _formKey = GlobalKey(); + final GlobalKey _formKey = GlobalKey(); String _email; String _password; @@ -22,7 +22,7 @@ class _LoginPageState extends State { // Check if form is valid before perform login or signup bool _validateAndSave() { - final form = _formKey.currentState; + final FormState form = _formKey.currentState; if (form.validate()) { form.save(); return true; @@ -33,11 +33,11 @@ class _LoginPageState extends State { // Perform login or signup Future _validateAndSubmit() async { setState(() { - _errorMessage = ""; + _errorMessage = ''; _isLoading = true; }); if (_validateAndSave()) { - User user = User(_email, _password, _email); + final User user = User(_email, _password, _email); ParseResponse response; try { @@ -73,14 +73,14 @@ class _LoginPageState extends State { @override void initState() { - _errorMessage = ""; + _errorMessage = ''; _isLoading = false; super.initState(); } void _changeFormToSignUp() { _formKey.currentState.reset(); - _errorMessage = ""; + _errorMessage = ''; setState(() { _formMode = FormMode.SIGNUP; }); @@ -88,7 +88,7 @@ class _LoginPageState extends State { void _changeFormToLogin() { _formKey.currentState.reset(); - _errorMessage = ""; + _errorMessage = ''; setState(() { _formMode = FormMode.LOGIN; }); @@ -185,8 +185,8 @@ class _LoginPageState extends State { Icons.mail, color: Colors.grey, )), - validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null, - onSaved: (value) => _email = value, + validator: (String value) => value.isEmpty ? 'Email can\'t be empty' : null, + onSaved: (String value) => _email = value, ), ); } @@ -204,8 +204,8 @@ class _LoginPageState extends State { Icons.lock, color: Colors.grey, )), - validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null, - onSaved: (value) => _password = value, + validator: (String value) => value.isEmpty ? 'Password can\'t be empty' : null, + onSaved: (String value) => _password = value, ), ); } diff --git a/example/windows/find_vcvars.dart b/example/windows/find_vcvars.dart index 375651e0a..bbbe33adb 100644 --- a/example/windows/find_vcvars.dart +++ b/example/windows/find_vcvars.dart @@ -17,14 +17,15 @@ import 'dart:io'; int main() { - final programDir = Platform.environment['PROGRAMFILES(X86)']; - final pathPrefix = '$programDir\\Microsoft Visual Studio'; - const pathSuffix = 'VC\\Auxiliary\\Build\\vcvars64.bat'; - final years = ['2017', '2019']; - final flavors = ['Community', 'Professional', 'Enterprise', 'Preview']; - for (final year in years) { - for (final flavor in flavors) { - final testPath = '$pathPrefix\\$year\\$flavor\\$pathSuffix'; + final String programDir = Platform.environment['PROGRAMFILES(X86)']; + final String pathPrefix = '$programDir\\Microsoft Visual Studio'; + const String pathSuffix = 'VC\\Auxiliary\\Build\\vcvars64.bat'; + final List years = ['2017', '2019']; + final List flavors = [ + 'Community', 'Professional', 'Enterprise', 'Preview']; + for (final String year in years) { + for (final String flavor in flavors) { + final String testPath = '$pathPrefix\\$year\\$flavor\\$pathSuffix'; if (File(testPath).existsSync()) { print(testPath); return 0; diff --git a/example/windows/generate_props.dart b/example/windows/generate_props.dart index 5a918942e..d895b0298 100644 --- a/example/windows/generate_props.dart +++ b/example/windows/generate_props.dart @@ -18,8 +18,8 @@ import 'dart:io'; void main(List arguments) { - final outputPath = arguments[0]; - final settings = { + final String outputPath = arguments[0]; + final Map settings = { 'FLUTTER_ROOT': arguments[1], 'EXTRA_BUNDLE_FLAGS': arguments[2], }; @@ -39,16 +39,16 @@ ${getItemGroupContent(settings)} } String getUserMacrosContent(Map settings) { - final macroList = StringBuffer(); - for (final setting in settings.entries) { + final StringBuffer macroList = StringBuffer(); + for (final MapEntry setting in settings.entries) { macroList.writeln(' <${setting.key}>${setting.value}'); } return macroList.toString(); } String getItemGroupContent(Map settings) { - final macroList = StringBuffer(); - for (final name in settings.keys) { + final StringBuffer macroList = StringBuffer(); + for (final String name in settings.keys) { macroList.writeln(''' \$($name) true diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index db983ef03..adec583e3 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter/foundation.dart'; @@ -13,35 +12,32 @@ class S implements WidgetsLocalizations { const S(); static const GeneratedLocalizationsDelegate delegate = - const GeneratedLocalizationsDelegate(); + GeneratedLocalizationsDelegate(); static S of(BuildContext context) => Localizations.of(context, WidgetsLocalizations); @override TextDirection get textDirection => TextDirection.ltr; - } class en extends S { const en(); } - -class GeneratedLocalizationsDelegate extends LocalizationsDelegate { +class GeneratedLocalizationsDelegate + extends LocalizationsDelegate { const GeneratedLocalizationsDelegate(); List get supportedLocales { return const [ - - const Locale("en", ""), - + Locale("en", ""), ]; } LocaleResolutionCallback resolution({Locale fallback}) { return (Locale locale, Iterable supported) { - final Locale languageLocale = new Locale(locale.languageCode, ""); + final Locale languageLocale = Locale(locale.languageCode, ""); if (supported.contains(locale)) return locale; else if (supported.contains(languageLocale)) @@ -57,12 +53,11 @@ class GeneratedLocalizationsDelegate extends LocalizationsDelegate load(Locale locale) { final String lang = getLang(locale); switch (lang) { - case "en": - return new SynchronousFuture(const en()); + return SynchronousFuture(const en()); default: - return new SynchronousFuture(const S()); + return SynchronousFuture(const S()); } } @@ -75,4 +70,4 @@ class GeneratedLocalizationsDelegate extends LocalizationsDelegate l.countryCode != null && l.countryCode.isEmpty ? l.languageCode - : l.toString(); + : l.toString(); \ No newline at end of file diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 5a4775a4e..0112b5791 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -3,8 +3,10 @@ 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'; import 'package:devicelocale/devicelocale.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; @@ -12,49 +14,76 @@ import 'package:meta/meta.dart'; import 'package:package_info/package_info.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:sembast/sembast.dart'; -import 'package:sembast/sembast_io.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; import 'package:web_socket_channel/io.dart'; import 'package:xxtea/xxtea.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; + +part 'package:parse_server_sdk/src/objects/response/parse_response_utils.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 'src/base/parse_constants.dart'; -part 'src/data/core_store.dart'; -part 'src/data/core_store_impl.dart'; + part 'src/data/parse_core_data.dart'; -part 'src/data/parse_shared_preferences_corestore.dart'; -part 'src/data/xxtea_codec.dart'; + part 'src/enums/parse_enum_api_rq.dart'; + part 'src/network/parse_http_client.dart'; + part 'src/network/parse_live_query.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_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_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/objects/parse_acl.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_date_format.dart'; + +part 'src/data/core_store.dart'; +part 'src/data/core_store_impl.dart'; +part 'src/data/xxtea_codec.dart'; + class Parse { ParseCoreData data; bool _hasBeenInitialized = false; @@ -80,7 +109,7 @@ class Parse { String sessionId, bool autoSendSessionId, SecurityContext securityContext, - FutureOr coreStore}) { + CoreStore coreStore}) { final String url = removeTrailingSlash(serverUrl); ParseCoreData.init(appId, url, @@ -102,14 +131,15 @@ class Parse { bool hasParseBeenInitialized() => _hasBeenInitialized; Future healthCheck( - {bool debug, ParseHTTPClient client, bool autoSendSessionId}) async { + {bool debug, ParseHTTPClient client, bool sendSessionIdByDefault}) async { ParseResponse parseResponse; final bool _debug = isDebugEnabled(objectLevelDebug: debug); + final ParseHTTPClient _client = client ?? ParseHTTPClient( sendSessionId: - autoSendSessionId ?? ParseCoreData().autoSendSessionId, + sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, securityContext: ParseCoreData().securityContext); const String className = 'parseBase'; @@ -118,7 +148,6 @@ class Parse { try { final Response response = await _client.get('${ParseCoreData().serverUrl}$keyEndPointHealth'); - parseResponse = handleResponse(null, response, type, _debug, className); } on Exception catch (e) { diff --git a/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index 2bf12e226..2f61a8c05 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.21'; +const String keySdkVersion = '1.0.22'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points @@ -36,6 +36,7 @@ const String keyClassSession = '_Session'; const String keyClassInstallation = '_Installation'; const String keyGeoPoint = 'GeoPoint'; const String keyFile = 'File'; +const String keyRelation = 'Relation'; // Headers const String keyHeaderSessionToken = 'X-Parse-Session-Token'; diff --git a/lib/src/data/core_store.dart b/lib/src/data/core_store.dart index 13a1c6fd5..b7d2fe450 100644 --- a/lib/src/data/core_store.dart +++ b/lib/src/data/core_store.dart @@ -15,17 +15,17 @@ abstract class CoreStore { Future> getStringList(String key); - Future setBool(String key, bool value); + Future setBool(String key, bool value); - Future setInt(String key, int value); + Future setInt(String key, int value); - Future setDouble(String key, double value); + Future setDouble(String key, double value); - Future setString(String key, String value); + Future setString(String key, String value); - Future setStringList(String key, List values); + Future setStringList(String key, List values); - Future remove(String key); + Future remove(String key); - Future clear(); + Future clear(); } diff --git a/lib/src/data/core_store_impl.dart b/lib/src/data/core_store_impl.dart index 3a65c7eb4..554dbef1b 100644 --- a/lib/src/data/core_store_impl.dart +++ b/lib/src/data/core_store_impl.dart @@ -31,62 +31,67 @@ class CoreStoreImp implements CoreStore { } @override - Future get(String key) { + Future get(String key) { return _store.get(key); } @override Future getBool(String key) async { - return await _store.get(key) as bool; + final bool storedItem = await _store.get(key); + return storedItem; } @override Future getDouble(String key) async { - return await _store.get(key) as double; + final double storedItem = await _store.get(key); + return storedItem; } @override Future getInt(String key) async { - return await _store.get(key) as int; + final int storedItem = await _store.get(key); + return storedItem; } @override Future getString(String key) async { - return await _store.get(key) as String; + final String storedItem = await _store.get(key); + return storedItem; } @override Future> getStringList(String key) async { - return await _store.get(key) as List; + final List storedItem = await _store.get(key); + return storedItem; } @override - Future remove(String key) { + Future remove(String key) { return _store.delete(key); } @override - Future setBool(String key, bool value) { + Future setBool(String key, bool value) { return _store.put(value, key); } @override - Future setDouble(String key, double value) { + Future setDouble(String key, double value) { return _store.put(value, key); } @override - Future setInt(String key, int value) { + Future setInt(String key, int value) { return _store.put(value, key); } @override - Future setString(String key, String value) { + Future setString(String key, String value) { return _store.put(value, key); } @override - Future setStringList(String key, List values) { + Future setStringList(String key, List values) { return _store.put(values, key); } -} +} \ No newline at end of file diff --git a/lib/src/data/parse_core_data.dart b/lib/src/data/parse_core_data.dart index b0e98593f..7b84fb07d 100644 --- a/lib/src/data/parse_core_data.dart +++ b/lib/src/data/parse_core_data.dart @@ -23,20 +23,36 @@ class ParseCoreData { String sessionId, bool autoSendSessionId, SecurityContext securityContext, - FutureOr store}) { + CoreStore store}) { _instance = ParseCoreData._init(appId, serverUrl); - _instance.storage ??= store ?? - Future.value( - SharedPreferencesCoreStore(SharedPreferences.getInstance())); - if (debug != null) _instance.debug = debug; - if (appName != null) _instance.appName = appName; - if (liveQueryUrl != null) _instance.liveQueryURL = liveQueryUrl; - if (clientKey != null) _instance.clientKey = clientKey; - if (masterKey != null) _instance.masterKey = masterKey; - if (sessionId != null) _instance.sessionId = sessionId; - if (autoSendSessionId != null) + + _instance.storage ??= + store ?? CoreStoreImp.getInstance(password: masterKey); + + if (debug != null) { + _instance.debug = debug; + } + if (appName != null) { + _instance.appName = appName; + } + if (liveQueryUrl != null) { + _instance.liveQueryURL = liveQueryUrl; + } + if (clientKey != null) { + _instance.clientKey = clientKey; + } + if (masterKey != null) { + _instance.masterKey = masterKey; + } + if (sessionId != null) { + _instance.sessionId = sessionId; + } + if (autoSendSessionId != null) { _instance.autoSendSessionId = autoSendSessionId; - if (securityContext != null) _instance.securityContext = securityContext; + } + if (securityContext != null) { + _instance.securityContext = securityContext; + } } String appName; @@ -49,7 +65,7 @@ class ParseCoreData { bool autoSendSessionId; SecurityContext securityContext; bool debug; - FutureOr storage; + Future storage; /// Sets the current sessionId. /// diff --git a/lib/src/data/parse_shared_preferences_corestore.dart b/lib/src/data/parse_shared_preferences_corestore.dart deleted file mode 100644 index 2f50b4a80..000000000 --- a/lib/src/data/parse_shared_preferences_corestore.dart +++ /dev/null @@ -1,68 +0,0 @@ -part of flutter_parse_sdk; - -class SharedPreferencesCoreStore implements CoreStore { - SharedPreferencesCoreStore(FutureOr sharedPreference) - : _sharedPreferencesFuture = Future.value(sharedPreference); - - Future _sharedPreferencesFuture; - - @override - Future clear() async { - final SharedPreferences sharedPreferences = await _sharedPreferencesFuture; - final bool result = await sharedPreferences.clear(); - return result; - } - - @override - Future get(String key) => - _sharedPreferencesFuture.then((shared) => shared.get(key)); - - @override - Future getBool(String key) => - _sharedPreferencesFuture.then((shared) => shared.getBool(key)); - - @override - Future getDouble(String key) => - _sharedPreferencesFuture.then((shared) => shared.getDouble(key)); - - @override - Future getInt(String key) => - _sharedPreferencesFuture.then((shared) => shared.getInt(key)); - - @override - Future getString(String key) => - _sharedPreferencesFuture.then((shared) => shared.getString(key)); - - @override - Future> getStringList(String key) => - _sharedPreferencesFuture.then((shared) => shared.getStringList(key)); - - @override - Future remove(String key) => - _sharedPreferencesFuture.then((shared) => shared.remove(key)); - - @override - Future setBool(String key, bool value) => - _sharedPreferencesFuture.then((shared) => shared.setBool(key, value)); - - @override - Future setDouble(String key, double value) => - _sharedPreferencesFuture.then((shared) => shared.setDouble(key, value)); - - @override - Future setInt(String key, int value) => - _sharedPreferencesFuture.then((shared) => shared.setInt(key, value)); - - @override - Future setString(String key, String value) => - _sharedPreferencesFuture.then((shared) => shared.setString(key, value)); - - @override - Future setStringList(String key, List values) => - _sharedPreferencesFuture - .then((shared) => shared.setStringList(key, values)); - - @override - Future containsKey(String key) => - _sharedPreferencesFuture.then((shared) => shared.containsKey(key)); -} 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/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 066920f8b..5b11aadd9 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -11,7 +11,7 @@ class LiveQuery { securityContext: ParseCoreData().securityContext); _debug = isDebugEnabled(objectLevelDebug: debug); - _sendSessionId = autoSendSessionId ?? ParseCoreData().autoSendSessionId; + _sendSessionId = autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; } WebSocket _webSocket; @@ -22,9 +22,9 @@ class LiveQuery { Map _connectMessage; Map _subscribeMessage; Map _unsubscribeMessage; - Map eventCallbacks = {}; + Map eventCallbacks = {}; int _requestIdCount = 1; - final List _liveQueryEvent = [ + final List _liveQueryEvent = [ 'create', 'enter', 'update', @@ -38,7 +38,9 @@ class LiveQuery { return _requestIdCount++; } - Future subscribe(QueryBuilder query) async { + // ignore: always_specify_types + Future subscribe(QueryBuilder query) async { + String _liveQueryURL = _client.data.liveQueryURL; if (_liveQueryURL.contains('https')) { _liveQueryURL = _liveQueryURL.replaceAll('https', 'wss'); @@ -47,8 +49,9 @@ class LiveQuery { } final String _className = query.object.className; - query.limiters.clear(); //Remove limites in LiveQuery + query.limiters.clear(); //Remove limits in LiveQuery final String _where = query._buildQuery().replaceAll('where=', ''); + //Convert where condition to Map Map _whereMap = Map(); if (_where != '') { @@ -67,14 +70,14 @@ class LiveQuery { } else { if (_debug) { print('$_printConstLiveQuery: Error when connection client'); - return Future.value(null); + return Future.value(null); } } _channel = IOWebSocketChannel(_webSocket); _channel.stream.listen((dynamic message) { if (_debug) { - print('$_printConstLiveQuery: Listen: ${message}'); + print('$_printConstLiveQuery: Listen: $message'); } final Map actionData = jsonDecode(message); @@ -103,7 +106,7 @@ class LiveQuery { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); } - return Future.value(handleException( + return Future.value(handleException( Exception(error), ParseApiRQ.liveQuery, _debug, _className)); }); @@ -128,7 +131,7 @@ class LiveQuery { _subscribeMessage = { 'op': 'subscribe', 'requestId': requestId, - 'query': { + 'query': { 'className': _className, 'where': _whereMap, } @@ -167,7 +170,7 @@ class LiveQuery { print( '$_printConstLiveQuery: UnsubscribeMessage: $_unsubscribeMessage'); } - await _channel.sink.add(jsonEncode(_unsubscribeMessage)); + _channel.sink.add(jsonEncode(_unsubscribeMessage)); await _channel.sink.close(); } } diff --git a/lib/src/network/parse_query.dart b/lib/src/network/parse_query.dart index d32a5c1ea..749527e96 100644 --- a/lib/src/network/parse_query.dart +++ b/lib/src/network/parse_query.dart @@ -138,6 +138,12 @@ class QueryBuilder { MapEntry(column, value), '\$exists')); } + /// Retrieves related objets where [String] column is a relation field to the class [String] className + void whereRelatedTo(String column, String className, String objectId) { + queries.add(MapEntry(_SINGLE_QUERY, + '\"\$relatedTo\":{\"object\":{\"__type\":\"Pointer\",\"className\":\"$className\",\"objectId\":\"$objectId\"},\"key\":\"$column\"}')); + } + /// Returns an object where the [String] column contains select void selectKeys(String column, dynamic value) { queries.add(_buildQueryWithColumnValueAndOperator( @@ -238,16 +244,18 @@ class QueryBuilder { } // Add a constraint to the query that requires a particular key's value match another QueryBuilder + // ignore: always_specify_types void whereMatchesQuery(String column, QueryBuilder query) { - String inQuery = query._buildQueryRelational(query.object.className); + final String inQuery = query._buildQueryRelational(query.object.className); queries.add(MapEntry( _SINGLE_QUERY, '\"$column\":{\"\$inQuery\":$inQuery}')); } //Add a constraint to the query that requires a particular key's value does not match another QueryBuilder + // ignore: always_specify_types void whereDoesNotMatchQuery(String column, QueryBuilder query) { - String inQuery = query._buildQueryRelational(query.object.className); + final String inQuery = query._buildQueryRelational(query.object.className); queries.add(MapEntry( _SINGLE_QUERY, '\"$column\":{\"\$notInQuery\":$inQuery}')); @@ -305,8 +313,7 @@ class QueryBuilder { if (item == queries.first) { queryBuilder += item; } else { - // ignore: prefer_single_quotes - queryBuilder += ",$item"; + queryBuilder += ',$item'; } } @@ -316,7 +323,7 @@ class QueryBuilder { /// Creates a query param using the column, the value and the queryOperator /// that the column and value are being queried against MapEntry _buildQueryWithColumnValueAndOperator( - MapEntry columnAndValue, String queryOperator) { + MapEntry columnAndValue, String queryOperator) { final String key = columnAndValue.key; final dynamic value = convertValueToCorrectType(parseEncode(columnAndValue.value)); @@ -370,10 +377,10 @@ class QueryBuilder { // Compact all the queries in the correct format for (MapEntry queryToCompact in listOfQueriesCompact) { - var queryToCompactValue = queryToCompact.value.toString(); - queryToCompactValue = queryToCompactValue.replaceFirst("{", ""); + String queryToCompactValue = queryToCompact.value.toString(); + queryToCompactValue = queryToCompactValue.replaceFirst('{', ''); queryToCompactValue = queryToCompactValue.replaceRange( - queryToCompactValue.length - 1, queryToCompactValue.length, ""); + queryToCompactValue.length - 1, queryToCompactValue.length, ''); if (listOfQueriesCompact.first == queryToCompact) { queryEnd += queryToCompactValue.replaceAll(queryStart, ' '); } else { diff --git a/lib/src/objects/parse_acl.dart b/lib/src/objects/parse_acl.dart index 2d3ff2e5e..7919343ab 100644 --- a/lib/src/objects/parse_acl.dart +++ b/lib/src/objects/parse_acl.dart @@ -1,7 +1,7 @@ part of flutter_parse_sdk; /// [ParseACL] is used to control which users can access or modify a particular object -/// [ParseObject] can have its own [ParceACL] +/// [ParseObject] can have its own [ParseACL] /// You can grant read and write permissions separately to specific users /// or you can grant permissions to "the public" so that, for example, any user could read a particular object but /// only a particular set of users could write to that object @@ -16,7 +16,8 @@ class ParseACL { } final String _publicKEY = '*'; - final Map _permissionsById = {}; + final Map _permissionsById = + {}; /// Helper for setting stuff void _setPermissionsIfNonEmpty( @@ -125,8 +126,8 @@ class _ACLPermissions { _ACLPermissions(this._readPermission, this._writePermission); final String _keyReadPermission = 'read'; final String _keyWritePermission = 'write'; - bool _readPermission = false; - bool _writePermission = false; + final bool _readPermission; + final bool _writePermission; bool getReadPermission() { return _readPermission; diff --git a/lib/src/objects/parse_error.dart b/lib/src/objects/parse_error.dart index eda1e30b9..39fd9ad19 100644 --- a/lib/src/objects/parse_error.dart +++ b/lib/src/objects/parse_error.dart @@ -2,7 +2,6 @@ part of flutter_parse_sdk; /// ParseException is used in [ParseResult] to inform the user of the exception class ParseError { - ParseError( {this.code = -1, this.message = 'Unknown error', diff --git a/lib/src/objects/parse_file.dart b/lib/src/objects/parse_file.dart index bd2a53909..ee93e3f40 100644 --- a/lib/src/objects/parse_file.dart +++ b/lib/src/objects/parse_file.dart @@ -93,9 +93,16 @@ class ParseFile extends ParseObject { 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, className); + final Map response = { + 'url': url, + 'name': name + }; + return handleResponse( + this, + Response(json.encode(response), 201), + ParseApiRQ.upload, + _debug, + className); } final String ext = path.extension(file.path).replaceAll('.', ''); diff --git a/lib/src/objects/parse_function.dart b/lib/src/objects/parse_function.dart index 16bcf12e6..8f80d6a3d 100644 --- a/lib/src/objects/parse_function.dart +++ b/lib/src/objects/parse_function.dart @@ -34,8 +34,9 @@ class ParseCloudFunction extends ParseObject { } final Response result = - await _client.post(uri, body: json.encode(getObjectData())); - return handleResponse(this, result, ParseApiRQ.execute, _debug, className); + await _client.post(uri, body: json.encode(getObjectData())); + return handleResponse( + this, result, ParseApiRQ.execute, _debug, className); } /// Executes a cloud function that returns a ParseObject type diff --git a/lib/src/objects/parse_geo_point.dart b/lib/src/objects/parse_geo_point.dart index f6c16168b..61c52e894 100644 --- a/lib/src/objects/parse_geo_point.dart +++ b/lib/src/objects/parse_geo_point.dart @@ -4,7 +4,6 @@ const String keyLatitude = 'latitude'; const String keyLongitude = 'longitude'; class ParseGeoPoint extends ParseObject { - /// Creates a Parse Object of type GeoPoint ParseGeoPoint( {double latitude = 0.0, @@ -13,7 +12,6 @@ class ParseGeoPoint extends ParseObject { ParseHTTPClient client, bool autoSendSessionId}) : super(keyGeoPoint) { - this.latitude = latitude; this.longitude = longitude; @@ -32,7 +30,8 @@ class ParseGeoPoint extends ParseObject { set longitude(double longitude) => set(keyLongitude, longitude); @override - Map toJson({bool full = false, bool forApiRQ = false}) => { + Map toJson({bool full = false, bool forApiRQ = false}) => + { '__type': 'GeoPoint', 'latitude': latitude, 'longitude': longitude diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index 381ad4ce9..4aa0f5130 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -16,14 +16,17 @@ class ParseInstallation extends ParseObject { ParseInstallation.forQuery() : super(keyClassUser); static final List readOnlyKeys = [ - // TODO - keyDeviceToken, keyDeviceType, keyInstallationId, - keyAppName, keyAppVersion, keyAppIdentifier, keyParseVersion + keyDeviceToken, + keyDeviceType, + keyInstallationId, + keyAppName, + keyAppVersion, + keyAppIdentifier, + keyParseVersion ]; static String _currentInstallationId; //Getters/setters - Map get acl => super.get>(keyVarAcl); set acl(Map acl) => @@ -65,15 +68,17 @@ class ParseInstallation extends ParseObject { /// Updates the installation with current device data Future _updateInstallation() async { //Device type - if (Platform.isAndroid) + if (Platform.isAndroid) { set(keyDeviceType, 'android'); - else if (Platform.isIOS) + } else if (Platform.isIOS) { set(keyDeviceType, 'ios'); - else if (Platform.isLinux) + } else if (Platform.isLinux) { set(keyDeviceType, 'Linux'); - else if (Platform.isMacOS) + } else if (Platform.isMacOS) { set(keyDeviceType, 'MacOS'); - else if (Platform.isWindows) set(keyDeviceType, 'Windows'); + } else if (Platform.isWindows) { + set(keyDeviceType, 'Windows'); + } //Locale final String locale = await Devicelocale.currentLocale; @@ -82,9 +87,7 @@ class ParseInstallation extends ParseObject { } //Timezone - final String currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); - set(keyTimeZone, currentTimeZone); - + //App info final PackageInfo packageInfo = await PackageInfo.fromPlatform(); set(keyAppName, packageInfo.appName); @@ -96,8 +99,10 @@ class ParseInstallation extends ParseObject { @override Future create() async { final bool isCurrent = await ParseInstallation.isCurrent(this); - if (isCurrent) await _updateInstallation(); - //ParseResponse parseResponse = await super.create(); + if (isCurrent) { + await _updateInstallation(); + } + final ParseResponse parseResponse = await _create(); if (parseResponse.success && isCurrent) { saveInStorage(keyParseStoreInstallation); @@ -144,6 +149,7 @@ class ParseInstallation extends ParseObject { /// so it creates and sets the static current installation UUID static Future _createInstallation() async { _currentInstallationId ??= Uuid().v4(); + final ParseInstallation installation = ParseInstallation(); installation._installationId = _currentInstallationId; await installation._updateInstallation(); @@ -200,13 +206,15 @@ class ParseInstallation extends ParseObject { ///Subscribes the device to a channel of push notifications. void subscribeToChannel(String value) { final List channel = [value]; - addUnique('channels', channel); + setAddAllUnique('channels', channel); + save(); } ///Unsubscribes the device to a channel of push notifications. void unsubscribeFromChannel(String value) { final List channel = [value]; - removeAll('channels', channel); + setRemove('channels', channel); + save(); } ///Returns an > containing all the channel names this device is subscribed to. @@ -217,7 +225,8 @@ class ParseInstallation extends ParseObject { if (apiResponse.success) { final ParseObject installation = apiResponse.result; - return Future.value(installation.get>('channels')); + return Future>.value( + installation.get>('channels')); } else { return null; } diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 6b474fb9a..592ae8aa8 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -1,5 +1,6 @@ part of flutter_parse_sdk; +// ignore_for_file: always_specify_types class ParseObject extends ParseBase implements ParseCloneable { /// Creates a new Parse Object /// @@ -68,13 +69,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,21 +76,191 @@ 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; + } + + 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); } /// Removes an element from an Array @@ -110,8 +274,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 @@ -154,8 +318,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); } @@ -170,8 +337,16 @@ 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) { + _arrayOperation('AddRelation', key, values); + } + + void removeRelation(String key, List values) { + _arrayOperation('RemoveRelation', key, values); } /// Can be used to add arrays to a given type @@ -195,6 +370,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 new file mode 100644 index 000000000..50aa8584e --- /dev/null +++ b/lib/src/objects/parse_relation.dart @@ -0,0 +1,41 @@ +part of flutter_parse_sdk; + +// ignore_for_file: always_specify_types +class ParseRelation { + ParseRelation({ParseObject parent, String key}) { + _parent = parent; + _key = key; + } + + String _targetClass; + ParseObject _parent; + String _key; + Set _objects = Set(); + + QueryBuilder getQuery() { + return QueryBuilder(ParseObject(_targetClass)); + } + + void add(T object) { + if (object != null) { + _targetClass = object.getClassName(); + _objects.add(object); + _parent.addRelation(_key, _objects.toList()); + } + } + + void remove(T object) { + if (object != null) { + _targetClass = object.getClassName(); + _objects.remove(object); + _parent.removeRelation(_key, _objects.toList()); + } + } + + Map toJson() => + {'__type': keyRelation, 'className': _objects?.first?.className, 'objects': parseEncode(_objects?.toList())}; + + ParseRelation fromJson(Map map) => ParseRelation() + .._objects = parseDecode(map['objects']) + .._targetClass = map['className']; +} \ No newline at end of file diff --git a/lib/src/objects/response/parse_error_response.dart b/lib/src/objects/response/parse_error_response.dart index b90196166..f7a50a623 100644 --- a/lib/src/objects/response/parse_error_response.dart +++ b/lib/src/objects/response/parse_error_response.dart @@ -2,13 +2,13 @@ part of flutter_parse_sdk; /// Handles any errors returned in response ParseResponse buildErrorResponse(ParseResponse response, Response apiResponse) { - if (apiResponse.body == null) { return null; } final Map responseData = json.decode(apiResponse.body); - response.error = ParseError(code: responseData[keyCode], message: responseData[keyError].toString()); + response.error = ParseError( + code: responseData[keyCode], message: responseData[keyError].toString()); response.statusCode = responseData[keyCode]; return response; } diff --git a/lib/src/objects/response/parse_exception_response.dart b/lib/src/objects/response/parse_exception_response.dart index e8b3f4367..9a798c8b0 100644 --- a/lib/src/objects/response/parse_exception_response.dart +++ b/lib/src/objects/response/parse_exception_response.dart @@ -3,6 +3,7 @@ part of flutter_parse_sdk; /// Handles exception instead of throwing an exception ParseResponse buildParseResponseWithException(Exception exception) { final ParseResponse response = ParseResponse(); - response.error = ParseError(message: exception.toString(), isTypeOfException: true); + response.error = + ParseError(message: exception.toString(), isTypeOfException: true); return response; } diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index 76b77d6c8..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)); } @@ -113,6 +131,6 @@ class _ParseResponseBuilder { } bool isHealthCheck(Response apiResponse) { - return apiResponse.body == "{\"status\":\"ok\"}"; + 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 0fff98d21..db209f58b 100644 --- a/lib/src/objects/response/parse_response_utils.dart +++ b/lib/src/objects/response/parse_response_utils.dart @@ -2,13 +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); @@ -21,9 +18,8 @@ ParseResponse handleResponse(ParseCloneable object, Response response, @protected ParseResponse handleException( Exception exception, ParseApiRQ type, bool debug, String className) { - final ParseResponse parseResponse = - buildParseResponseWithException(exception); + buildParseResponseWithException(exception); if (debug) { logAPIResponse(className, type.toString(), parseResponse); @@ -50,11 +46,18 @@ bool shouldReturnAsABaseResult(ParseApiRQ type) { } } -bool isUnsuccessfulResponse(Response apiResponse) => apiResponse.statusCode != 200 && apiResponse.statusCode != 201; +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; @@ -62,4 +65,3 @@ bool isSuccessButNoResults(Response apiResponse) { return results.isEmpty; } - diff --git a/lib/src/objects/response/parse_success_no_results.dart b/lib/src/objects/response/parse_success_no_results.dart index 053dbadb4..e49eb989a 100644 --- a/lib/src/objects/response/parse_success_no_results.dart +++ b/lib/src/objects/response/parse_success_no_results.dart @@ -1,7 +1,8 @@ part of flutter_parse_sdk; /// Handles successful responses with no results -ParseResponse buildSuccessResponseWithNoResults(ParseResponse response, int code, String value) { +ParseResponse buildSuccessResponseWithNoResults(ParseResponse response, + int code, String value) { response.success = true; response.statusCode = 200; response.error = ParseError(code: code, message: value); diff --git a/lib/src/utils/parse_date_format.dart b/lib/src/utils/parse_date_format.dart index fc394d734..c025878d7 100644 --- a/lib/src/utils/parse_date_format.dart +++ b/lib/src/utils/parse_date_format.dart @@ -63,4 +63,4 @@ class _ParseDateFormat { } return '0$n'; } -} \ No newline at end of file +} diff --git a/lib/src/utils/parse_decoder.dart b/lib/src/utils/parse_decoder.dart index cff6fcb87..4c7629fbe 100644 --- a/lib/src/utils/parse_decoder.dart +++ b/lib/src/utils/parse_decoder.dart @@ -53,7 +53,7 @@ dynamic parseDecode(dynamic value) { switch (map['__type']) { case 'Date': final String iso = map['iso']; - return _parseDateFormat.parse(iso); + return _parseDateFormat.parse(iso); case 'Bytes': final String val = map['base64']; return base64.decode(val); @@ -77,6 +77,9 @@ dynamic parseDecode(dynamic value) { final num longitude = map['longitude'] ?? 0.0; return ParseGeoPoint( latitude: latitude.toDouble(), longitude: longitude.toDouble()); + case 'Relation': + // ignore: always_specify_types + return ParseRelation().fromJson(map); } } diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index f2929c376..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; } @@ -34,6 +40,10 @@ dynamic parseEncode(dynamic value, {bool full}) { return value; } + if (value is ParseRelation) { + return value; + } + if (value is ParseObject || value is ParseUser) { if (full) { return value.toJson(full: full); diff --git a/lib/src/utils/parse_file_extensions.dart b/lib/src/utils/parse_file_extensions.dart index 7dc4a829a..6482c2c41 100644 --- a/lib/src/utils/parse_file_extensions.dart +++ b/lib/src/utils/parse_file_extensions.dart @@ -1,5 +1,7 @@ part of flutter_parse_sdk; +// ignore_for_file: always_specify_types + /// Get the extension type of the file String getExtension(String contentType) { if (_extensions.containsKey(contentType) && diff --git a/lib/src/utils/parse_logger.dart b/lib/src/utils/parse_logger.dart index 047d3d37d..50fce8190 100644 --- a/lib/src/utils/parse_logger.dart +++ b/lib/src/utils/parse_logger.dart @@ -1,10 +1,7 @@ part of flutter_parse_sdk; -void logAPIResponse( - String className, - String type, +void logAPIResponse(String className, String type, ParseResponse parseResponse) { - const String spacer = ' \n'; String responseString = ''; @@ -66,10 +63,10 @@ void logRequest( if (name.isNotEmpty) { name = '$appName '; } - requestString += '----\n${name}API Request ($className : $type) :'; - requestString += '\nUri: $uri'; - requestString += '\nBody: $body'; + requestString += '----\n${name}API Request ($className : $type) :'; + requestString += '\nUri: $uri'; + requestString += '\nBody: $body'; - requestString += '\n----\n'; - print(requestString); + requestString += '\n----\n'; + print(requestString); } diff --git a/lib/src/utils/parse_utils.dart b/lib/src/utils/parse_utils.dart index c2d8a090f..d8f88a0b9 100644 --- a/lib/src/utils/parse_utils.dart +++ b/lib/src/utils/parse_utils.dart @@ -15,7 +15,7 @@ dynamic convertValueToCorrectType(dynamic value) { /*if (value is String && !value.contains('__type')) { return '\"$value\"'; }*/ - + if (value is DateTime || value is ParseObject) { return parseEncode(value); } else { @@ -26,7 +26,6 @@ dynamic convertValueToCorrectType(dynamic value) { /// Sanitises a url Uri getSanitisedUri(ParseHTTPClient client, String pathToAppend, {Map queryParams, String query}) { - final Uri tempUri = Uri.parse(client.data.serverUrl); final Uri url = Uri( @@ -39,11 +38,47 @@ 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) == '/') { - return serverUrl.substring(0, 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'); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 6c598f2fb..1223c4559 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: parse_server_sdk description: Flutter plugin for Parse Server, (https://parseplatform.org), (https://back4app.com) -version: 1.0.21 +version: 1.0.22 homepage: https://github.com/phillwiggins/flutter_parse_sdk author: PhillWiggins @@ -8,18 +8,19 @@ environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: + flutter: + sdk: flutter # Networking web_socket_channel: ^1.0.9 http: ^0.12.0 - + #Database - shared_preferences: ^0.5.2 sembast: ^1.15.1 xxtea: ^2.0.2 - - flutter_native_timezone: ^1.0.4 - + shared_preferences: ^0.5.2 + + # Utils path_provider: ^0.5.0+1 uuid: ^2.0.0 @@ -29,5 +30,6 @@ dependencies: dev_dependencies: # Testing test: ^1.5.1 + mockito: ^4.1.0 flutter_test: sdk: flutter diff --git a/test/parse_query_test.dart b/test/parse_query_test.dart new file mode 100644 index 000000000..364bba82a --- /dev/null +++ b/test/parse_query_test.dart @@ -0,0 +1,31 @@ +import 'package:mockito/mockito.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:test/test.dart'; + +class MockClient extends Mock implements ParseHTTPClient {} + +void main() { + group('queryBuilder', () { + test('whereRelatedTo', () async { + final MockClient client = MockClient(); + + Parse().initialize('appId', 'https://test.parse.com', debug: true); + + final QueryBuilder queryBuilder = + QueryBuilder(ParseObject('_User', client: client)); + queryBuilder.whereRelatedTo('likes', 'Post', '8TOXdXf3tz'); + + when(client.data).thenReturn(ParseCoreData()); + await queryBuilder.query(); + + final Uri result = verify(client.get(captureAny)).captured.single; + + expect(result.path, '/classes/_User'); + + final Uri expectedQuery = Uri( + query: + 'where={"\$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}'); + expect(result.query, expectedQuery.query); + }); + }); +}