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/README.md b/README.md index 395e99465..e199c25ae 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,14 @@ Parse().initialize( ApplicationConstants.keyApplicationId, ApplicationConstants.keyParseServerUrl); ``` +if you want to use secure storage also that's allow using sdk on desktop application +```dart + Parse().initialize(keyParseApplicationId, keyParseServerUrl, + masterKey: keyParseMasterKey, + debug: true, + coreStore: CoreStoreImp.getInstance()); +``` It's possible to add other params, such as ... ```dart @@ -530,6 +537,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) @@ -552,5 +574,5 @@ Objects: ## Author:- This project was authored by Phill Wiggins. You can contact me at phill.wiggins@gmail.com +eyJoaXN0b3J5IjpbLTU4MDA4MDUwNCw3MTg2NTA0MjBdfQ== +--> \ No newline at end of file diff --git a/example/assets/parse.png b/example/assets/parse.png new file mode 100644 index 000000000..fc7e48dd6 Binary files /dev/null and b/example/assets/parse.png differ diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d815bf063..ca2c16c33 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -44,9 +44,11 @@ 24DF2572E6AEEB9F7CE180C9 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 5804EFBD11740E02FC51BC3E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 96499D95196B10F296043703 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; diff --git a/example/lib/data/model/diet_plan.dart b/example/lib/data/model/diet_plan.dart index 8819d6189..d58f111a9 100644 --- a/example/lib/data/model/diet_plan.dart +++ b/example/lib/data/model/diet_plan.dart @@ -32,6 +32,6 @@ class DietPlan extends ParseObject implements ParseCloneable { num get fat => get(keyFat); set fat(num fat) => set(keyFat, fat); - int get status => get(keyStatus); - set status(int status) => set(keyStatus, status); + bool get status => get(keyStatus); + set status(bool status) => set(keyStatus, status); } diff --git a/example/lib/pages/decision_page.dart b/example/lib/pages/decision_page.dart new file mode 100644 index 000000000..2875a3131 --- /dev/null +++ b/example/lib/pages/decision_page.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_plugin_example/data/repositories/diet_plan/provider_api_diet_plan.dart'; +import 'package:flutter_plugin_example/domain/constants/application_constants.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +import 'home_page.dart'; +import 'login_page.dart'; + +class DecisionPage extends StatefulWidget { + @override + _DecisionPageState createState() => _DecisionPageState(); +} + +class _DecisionPageState extends State { + String _parseServerState = 'Checking Parse Server...'; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initParse(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _showLogo(), + const SizedBox( + height: 20, + ), + Center( + child: Text(_parseServerState), + ), + ], + ), + ), + ), + ); + } + + Widget _showLogo() { + return Hero( + tag: 'hero', + child: Padding( + padding: const EdgeInsets.fromLTRB(0.0, 70.0, 0.0, 0.0), + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 48.0, + child: Image.asset('assets/parse.png'), + ), + ), + ); + } + + Future _initParse() async { + try { + Parse().initialize(keyParseApplicationId, keyParseServerUrl, + masterKey: keyParseMasterKey, debug: true); + final ParseResponse response = await Parse().healthCheck(); + if (response.success) { + final ParseUser user = await ParseUser.currentUser(); + if (user != null) { + _redirectToPage(context, HomePage(DietPlanProviderApi())); + } else { + _redirectToPage(context, LoginPage()); + } + } else { + setState(() { + _parseServerState = + 'Parse Server Not avaiable\n due to ${response.error.toString()}'; + }); + } + } catch (e) { + setState(() { + _parseServerState = e.toString(); + }); + } + } + + Future _redirectToPage(BuildContext context, Widget page) async { + final MaterialPageRoute newRoute = + MaterialPageRoute(builder: (BuildContext context) => page); + + 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 new file mode 100644 index 000000000..21578b102 --- /dev/null +++ b/example/lib/pages/home_page.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_plugin_example/data/base/api_response.dart'; +import 'package:flutter_plugin_example/data/model/diet_plan.dart'; +import 'package:flutter_plugin_example/data/repositories/diet_plan/contract_provider_diet_plan.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +class HomePage extends StatefulWidget { + HomePage(this._dietPlanProvider); + final DietPlanProviderContract _dietPlanProvider; + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + List randomDietPlans = []; + + @override + void initState() { + super.initState(); + final List json = const JsonDecoder().convert(dietPlansToAdd); + for (final Map element in json) { + final DietPlan dietPlan = DietPlan().fromJson(element); + randomDietPlans.add(dietPlan); + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: const Text('Parse Server demo'), + actions: [ + FlatButton( + child: Text('Logout', + style: TextStyle(fontSize: 17.0, color: Colors.white)), + onPressed: () async { + final ParseUser user = await ParseUser.currentUser(); + user.logout(deleteLocalUserData: true); + Navigator.pop(context, true); + }) + ], + ), + body: _showDietList(), + floatingActionButton: FloatingActionButton( + onPressed: () async { + DietPlan dietPlan = + randomDietPlans[Random().nextInt(randomDietPlans.length - 1)]; + ParseUser user = await ParseUser.currentUser(); + dietPlan.set('user', user); + await widget._dietPlanProvider.add(dietPlan); + setState(() {}); + }, + tooltip: 'Add Diet Plans', + child: const Icon(Icons.add), + )), + ); + } + + Widget _showDietList() { + return FutureBuilder( + future: widget._dietPlanProvider.getAll(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (snapshot.data.success) { + if (snapshot.data.results == null || + snapshot.data.results.isEmpty) { + return Center( + child: const Text('No Data'), + ); + } + } + return ListView.builder( + 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; + return Dismissible( + key: Key(id), + background: Container(color: Colors.red), + onDismissed: (direction) async { + widget._dietPlanProvider.remove(dietPlan); + }, + child: ListTile( + title: Text( + name, + style: TextStyle(fontSize: 20.0), + ), + subtitle: Text(description), + trailing: IconButton( + icon: status + ? const Icon( + Icons.done_outline, + color: Colors.green, + size: 20.0, + ) + : const Icon(Icons.done, + color: Colors.grey, size: 20.0), + onPressed: () async { + dietPlan.status = !dietPlan.status; + await dietPlan.save(); + setState(() {}); + }), + ), + ); + }); + } else { + return Center( + child: const Text('No Data'), + ); + } + }); + } + + 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":false},' + '{"className":"Diet_Plans","Name":"Body Builder","Description":"Default Body Builders Diet","Fat":20,"Carbs":40,"Protein":40,"Status":true},' + '{"className":"Diet_Plans","Name":"Zone Diet","Description":"Popular with CrossFit users. Zone Diet targets similar macros.","Fat":30,"Carbs":40,"Protein":30,"Status":true},' + '{"className":"Diet_Plans","Name":"Low Fat","Description":"Low fat diet.","Fat":15,"Carbs":60,"Protein":25,"Status":false},' + '{"className":"Diet_Plans","Name":"Low Carb","Description":"Low Carb diet, main focus on quality fats and protein.","Fat":35,"Carbs":25,"Protein":40,"Status":true},' + '{"className":"Diet_Plans","Name":"Paleo","Description":"Paleo diet.","Fat":60,"Carbs":25,"Protein":10,"Status":false},' + '{"className":"Diet_Plans","Name":"Ketogenic","Description":"High quality fats, low carbs.","Fat":65,"Carbs":5,"Protein":30,"Status":true}]'; +} diff --git a/example/lib/pages/login_page.dart b/example/lib/pages/login_page.dart new file mode 100644 index 000000000..e02824fd8 --- /dev/null +++ b/example/lib/pages/login_page.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_plugin_example/data/model/user.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +enum FormMode { LOGIN, SIGNUP } + +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + + String _email; + String _password; + String _errorMessage; + + // Initial form is login form + FormMode _formMode = FormMode.LOGIN; + bool _isLoading; + + // Check if form is valid before perform login or signup + bool _validateAndSave() { + final form = _formKey.currentState; + if (form.validate()) { + form.save(); + return true; + } + return false; + } + + // Perform login or signup + Future _validateAndSubmit() async { + setState(() { + _errorMessage = ""; + _isLoading = true; + }); + if (_validateAndSave()) { + User user = User(_email, _password, _email); + + ParseResponse response; + try { + if (_formMode == FormMode.LOGIN) { + response = await user.login(); + print('Signed in'); + } else { + response = await user.signUp(); + print('Signed up user:'); + } + setState(() { + _isLoading = false; + }); + if (response.success) { + if (_formMode == FormMode.LOGIN) { + Navigator.pop(context, true); + } + } else { + setState(() { + _isLoading = false; + _errorMessage = response.error.toString(); + }); + } + } catch (e) { + print('Error: $e'); + setState(() { + _isLoading = false; + _errorMessage = e.message; + }); + } + } + } + + @override + void initState() { + _errorMessage = ""; + _isLoading = false; + super.initState(); + } + + void _changeFormToSignUp() { + _formKey.currentState.reset(); + _errorMessage = ""; + setState(() { + _formMode = FormMode.SIGNUP; + }); + } + + void _changeFormToLogin() { + _formKey.currentState.reset(); + _errorMessage = ""; + setState(() { + _formMode = FormMode.LOGIN; + }); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: const Text('Parse Server demo'), + ), + body: Stack( + children: [ + _showBody(), + _showCircularProgress(), + ], + )), + ); + } + + Widget _showCircularProgress() { + if (_isLoading) { + return Center(child: const CircularProgressIndicator()); + } + return Container( + height: 0.0, + width: 0.0, + ); + } + + Widget _showBody() { + return Container( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + shrinkWrap: true, + children: [ + _showLogo(), + _showEmailInput(), + _showPasswordInput(), + _showPrimaryButton(), + _showSecondaryButton(), + _showErrorMessage(), + ], + ), + )); + } + + Widget _showErrorMessage() { + if (_errorMessage.isNotEmpty && _errorMessage != null) { + return Text( + _errorMessage, + style: TextStyle( + fontSize: 13.0, + color: Colors.red, + height: 1.0, + fontWeight: FontWeight.w300), + ); + } else { + return Container( + height: 0.0, + ); + } + } + + Widget _showLogo() { + return Hero( + tag: 'hero', + child: Padding( + padding: const EdgeInsets.fromLTRB(0.0, 70.0, 0.0, 0.0), + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 48.0, + child: Image.asset('assets/parse.png'), + ), + ), + ); + } + + Widget _showEmailInput() { + return Padding( + padding: const EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0), + child: TextFormField( + maxLines: 1, + keyboardType: TextInputType.emailAddress, + autofocus: false, + decoration: InputDecoration( + hintText: 'Email', + icon: const Icon( + Icons.mail, + color: Colors.grey, + )), + validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null, + onSaved: (value) => _email = value, + ), + ); + } + + Widget _showPasswordInput() { + return Padding( + padding: const EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0), + child: TextFormField( + maxLines: 1, + obscureText: true, + autofocus: false, + decoration: InputDecoration( + hintText: 'Password', + icon: const Icon( + Icons.lock, + color: Colors.grey, + )), + validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null, + onSaved: (value) => _password = value, + ), + ); + } + + Widget _showSecondaryButton() { + return FlatButton( + child: _formMode == FormMode.LOGIN + ? Text('Create an account', + style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)) + : Text('Have an account? Sign in', + style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)), + onPressed: _formMode == FormMode.LOGIN + ? _changeFormToSignUp + : _changeFormToLogin, + ); + } + + Widget _showPrimaryButton() { + return Padding( + padding: const EdgeInsets.fromLTRB(0.0, 45.0, 0.0, 0.0), + child: SizedBox( + height: 40.0, + child: RaisedButton( + elevation: 5.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0)), + color: Colors.blue, + child: _formMode == FormMode.LOGIN + ? Text('Login', + style: TextStyle(fontSize: 20.0, color: Colors.white)) + : Text('Create account', + style: TextStyle(fontSize: 20.0, color: Colors.white)), + onPressed: _validateAndSubmit, + ), + )); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ca94b4900..e1bfed68f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -31,9 +31,9 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: + assets: # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + - assets/parse.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. diff --git a/example/test/data/repository/repository_mock_utils.dart b/example/test/data/repository/repository_mock_utils.dart index 7d3a9e9ff..469be11a8 100644 --- a/example/test/data/repository/repository_mock_utils.dart +++ b/example/test/data/repository/repository_mock_utils.dart @@ -35,7 +35,7 @@ DietPlan getDummyDietPlan() { ..protein = 40 ..carbs = 40 ..fat = 20 - ..status = 0; + ..status = false; } Future deleteFromApi(List results) async { diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 1061b1908..b52dbe5b5 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -18,61 +18,40 @@ import 'package:web_socket_channel/io.dart'; import 'package:xxtea/xxtea.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/objects/parse_acl.dart'; - +part 'src/utils/parse_date_format.dart'; part 'src/utils/parse_decoder.dart'; - part 'src/utils/parse_encoder.dart'; - part 'src/utils/parse_file_extensions.dart'; - part 'src/utils/parse_logger.dart'; - part 'src/utils/parse_utils.dart'; part 'src/utils/parse_date_format.dart'; diff --git a/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index 637e24534..2f61a8c05 100644 --- a/lib/src/base/parse_constants.dart +++ b/lib/src/base/parse_constants.dart @@ -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/parse_shared_preferences_corestore.dart b/lib/src/data/parse_shared_preferences_corestore.dart new file mode 100644 index 000000000..2f50b4a80 --- /dev/null +++ b/lib/src/data/parse_shared_preferences_corestore.dart @@ -0,0 +1,68 @@ +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/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 80f3ac00f..404e28643 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -99,6 +99,11 @@ class ParseObject extends ParseBase implements ParseCloneable { } } + /// 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 @Deprecated('Prefer to use the setRemove() method in save()') Future remove(String key, dynamic values) async { @@ -174,6 +179,14 @@ class ParseObject extends ParseBase implements ParseCloneable { _arrayOperation('Add', key, values); } + 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 Future _sortArrays(ParseApiRQ apiRQType, String arrayAction, String key, List values) async { diff --git a/lib/src/objects/parse_relation.dart b/lib/src/objects/parse_relation.dart new file mode 100644 index 000000000..bd8ca8b95 --- /dev/null +++ b/lib/src/objects/parse_relation.dart @@ -0,0 +1,44 @@ +part of flutter_parse_sdk; + +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.map((T value) { + return value.toPointer(); + }).toList()); + } + } + + void remove(T object) { + if (object != null) { + _targetClass = object.getClassName(); + _objects.remove(object); + _parent.removeRelation(_key, _objects.map((T value) { + return value.toPointer(); + }).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/utils/parse_decoder.dart b/lib/src/utils/parse_decoder.dart index e2ac51c37..2ae0a4333 100644 --- a/lib/src/utils/parse_decoder.dart +++ b/lib/src/utils/parse_decoder.dart @@ -77,6 +77,8 @@ dynamic parseDecode(dynamic value) { final num longitude = map['longitude'] ?? 0.0; return ParseGeoPoint( latitude: latitude.toDouble(), longitude: longitude.toDouble()); + case 'Relation': + return ParseRelation().fromJson(map); } } diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index f2929c376..322d83b2c 100644 --- a/lib/src/utils/parse_encoder.dart +++ b/lib/src/utils/parse_encoder.dart @@ -34,6 +34,10 @@ dynamic parseEncode(dynamic value, {bool full}) { return value; } + if (value is ParseRelation) { + return value.toJson(); + } + if (value is ParseObject || value is ParseUser) { if (full) { return value.toJson(full: full);