From dd90d3ccb1454054e592c75b19d43e55a57fa2ce Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 24 Mar 2019 19:31:58 +0000 Subject: [PATCH 1/7] Added repo example --- CHANGELOG.md | 3 + README.md | 2 +- example/lib/application_constants.dart | 6 - example/lib/data/base/api_error.dart | 8 + example/lib/data/base/api_response.dart | 29 +++ example/lib/data/model/diet_plan.dart | 37 ++++ .../contract_provider_diet_plan.dart | 20 ++ .../diet_plan/provider_api_diet_plan.dart | 74 +++++++ .../diet_plan/provider_db_diet_plan.dart | 160 +++++++++++++++ .../diet_plan/repository_diet_plan.dart | 174 ++++++++++++++++ example/lib/diet_plan.dart | 39 ---- .../constants/application_constants.dart | 4 + .../lib/domain/utils/collection_utils.dart | 5 + example/lib/{ => ui}/main.dart | 62 ++++-- example/pubspec.yaml | 4 + .../repository_diet_plan_api_test.dart | 192 ++++++++++++++++++ .../repository_diet_plan_db_test.dart | 185 +++++++++++++++++ .../diet_plan/repository_diet_plan_test.dart | 130 ++++++++++++ .../repository/repository_mock_utils.dart | 39 ++++ lib/src/base/parse_constants.dart | 2 +- 20 files changed, 1111 insertions(+), 64 deletions(-) delete mode 100644 example/lib/application_constants.dart create mode 100644 example/lib/data/base/api_error.dart create mode 100644 example/lib/data/base/api_response.dart create mode 100644 example/lib/data/model/diet_plan.dart create mode 100644 example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart create mode 100644 example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart create mode 100644 example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart create mode 100644 example/lib/data/repositories/diet_plan/repository_diet_plan.dart delete mode 100644 example/lib/diet_plan.dart create mode 100644 example/lib/domain/constants/application_constants.dart create mode 100644 example/lib/domain/utils/collection_utils.dart rename example/lib/{ => ui}/main.dart (77%) create mode 100644 example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart create mode 100644 example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart create mode 100644 example/test/data/repository/diet_plan/repository_diet_plan_test.dart create mode 100644 example/test/data/repository/repository_mock_utils.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 04efbf907..45c7ff7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.0.17 + + ## 1.0.16 Bug fixes Fixed object delete diff --git a/README.md b/README.md index c910a5f9b..37f1b35c6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Want to get involved? Join our Slack channel and help out! (http://flutter-parse To install, either add to your pubspec.yaml ```yml dependencies: - parse_server_sdk: ^1.0.16 + parse_server_sdk: ^1.0.17 ``` 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. diff --git a/example/lib/application_constants.dart b/example/lib/application_constants.dart deleted file mode 100644 index 9e77a8bc0..000000000 --- a/example/lib/application_constants.dart +++ /dev/null @@ -1,6 +0,0 @@ -abstract class ApplicationConstants { - static const String keyAppName = ""; - static const String keyParseApplicationId = ""; - static const String keyParseMasterKey = ""; - static const String keyParseServerUrl = ""; -} \ No newline at end of file diff --git a/example/lib/data/base/api_error.dart b/example/lib/data/base/api_error.dart new file mode 100644 index 000000000..ccb11e256 --- /dev/null +++ b/example/lib/data/base/api_error.dart @@ -0,0 +1,8 @@ +class ApiError { + ApiError(this.code, this.message, this.isTypeOfException, this.type); + + final int code; + final String message; + final bool isTypeOfException; + final String type; +} diff --git a/example/lib/data/base/api_response.dart b/example/lib/data/base/api_response.dart new file mode 100644 index 000000000..787eec3b3 --- /dev/null +++ b/example/lib/data/base/api_response.dart @@ -0,0 +1,29 @@ +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +import 'api_error.dart'; + +class ApiResponse { + ApiResponse(this.success, this.statusCode, this.result, this.error); + + final bool success; + final int statusCode; + final dynamic result; + final ApiError error; + + dynamic getResult() { + return result; + } +} + +ApiResponse getApiResponse(ParseResponse response) { + return ApiResponse(response.success, response.statusCode, response.result, + getApiError(response.error)); +} + +ApiError getApiError(ParseError response) { + if (response == null) { + return null; + } + return ApiError(response.code, response.message, response.isTypeOfException, + response.type); +} diff --git a/example/lib/data/model/diet_plan.dart b/example/lib/data/model/diet_plan.dart new file mode 100644 index 000000000..8819d6189 --- /dev/null +++ b/example/lib/data/model/diet_plan.dart @@ -0,0 +1,37 @@ +import 'dart:core'; + +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +const String keyDietPlan = 'Diet_Plans'; +const String keyName = 'Name'; +const String keyDescription = 'Description'; +const String keyProtein = 'Protein'; +const String keyCarbs = 'Carbs'; +const String keyFat = 'Fat'; +const String keyStatus = 'Status'; + +class DietPlan extends ParseObject implements ParseCloneable { + DietPlan() : super(keyDietPlan); + DietPlan.clone() : this(); + + @override + DietPlan clone(Map map) => DietPlan.clone()..fromJson(map); + + String get name => get(keyName); + set name(String name) => set(keyName, name); + + String get description => get(keyDescription); + set description(String description) => set(keyDescription, name); + + num get protein => get(keyProtein); + set protein(num protein) => super.set(keyProtein, protein); + + num get carbs => get(keyCarbs); + set carbs(num carbs) => set(keyCarbs, carbs); + + num get fat => get(keyFat); + set fat(num fat) => set(keyFat, fat); + + int get status => get(keyStatus); + set status(int status) => set(keyStatus, status); +} diff --git a/example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart b/example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart new file mode 100644 index 000000000..9728acecd --- /dev/null +++ b/example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart @@ -0,0 +1,20 @@ +import 'package:flutter_plugin_example/data/base/api_response.dart'; +import 'package:flutter_plugin_example/data/model/diet_plan.dart'; + +abstract class DietPlanProviderContract { + Future add(DietPlan item); + + Future addAll(List items); + + Future update(DietPlan item); + + Future updateAll(List items); + + Future remove(DietPlan item); + + Future getById(String id); + + Future getAll(); + + Future getNewerThan(DateTime date); +} diff --git a/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart b/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart new file mode 100644 index 000000000..0050ceb20 --- /dev/null +++ b/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart @@ -0,0 +1,74 @@ +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 DietPlanProviderApi implements DietPlanProviderContract { + DietPlanProviderApi(); + + @override + Future add(DietPlan item) async { + return getApiResponse(await item.save()); + } + + @override + Future addAll(List items) async { + final List responses = List(); + + for (final DietPlan item in items) { + final ApiResponse response = await add(item); + + if (!response.success) { + return response; + } + + responses.add(response.result); + } + + return ApiResponse(true, 200, responses, null); + } + + @override + Future getAll() async { + return getApiResponse(await DietPlan().getAll()); + } + + @override + Future getById(String id) async { + return getApiResponse(await DietPlan().getObject(id)); + } + + @override + Future getNewerThan(DateTime date) async { + final QueryBuilder query = QueryBuilder(DietPlan()) + ..whereGreaterThan(keyVarCreatedAt, date); + return getApiResponse(await query.query()); + } + + @override + Future remove(DietPlan item) async { + return getApiResponse(await item.delete()); + } + + @override + Future update(DietPlan item) async { + return getApiResponse(await item.save()); + } + + @override + Future updateAll(List items) async { + final List responses = List(); + + for (final DietPlan item in items) { + final ApiResponse response = await update(item); + + if (!response.success) { + return response; + } + + responses.add(response.result); + } + + return ApiResponse(true, 200, responses, null); + } +} diff --git a/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart b/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart new file mode 100644 index 000000000..17e7e78f4 --- /dev/null +++ b/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart @@ -0,0 +1,160 @@ +import 'dart:convert' as json; + +import 'package:flutter_plugin_example/data/base/api_error.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:sembast/sembast.dart'; + +class DietPlanProviderDB implements DietPlanProviderContract { + DietPlanProviderDB(this._db, this._store); + + final Store _store; + final Database _db; + + @override + Future add(DietPlan item) async { + final Map values = convertItemToStorageMap(item); + final Record recordToAdd = Record(_store, values, item.objectId); + final Record recordFromDB = await _db.putRecord(recordToAdd); + return ApiResponse( + true, 200, convertRecordToItem(record: recordFromDB), null); + } + + @override + Future addAll(List items) async { + final List itemsInDb = List(); + + for (final DietPlan item in items) { + final ApiResponse response = await add(item); + if (response.success) { + final DietPlan itemInDB = response.result; + itemsInDb.add(itemInDB); + } + } + + if (itemsInDb.isEmpty) { + return errorResponse; + } else { + return ApiResponse(true, 200, itemsInDb, null); + } + } + + @override + Future getAll() async { + final List foodItems = List(); + + final List sortOrders = List(); + sortOrders.add(SortOrder(keyName)); + final Finder finder = Finder(sortOrders: sortOrders); + final List records = await _store.findRecords(finder); + + if (records.isNotEmpty) { + for (final Record record in records) { + final DietPlan userFood = convertRecordToItem(record: record); + foodItems.add(userFood); + } + } else { + return errorResponse; + } + + return ApiResponse(true, 200, foodItems, null); + } + + @override + Future getById(String id) async { + final Record record = await _store.getRecord(id); + if (record != null) { + final DietPlan userFood = convertRecordToItem(record: record); + return ApiResponse(true, 200, userFood, null); + } else { + return errorResponse; + } + } + + @override + Future getNewerThan(DateTime date) async { + final List foodItems = List(); + + final Finder finder = Finder( + filter: + Filter.greaterThan('keyUpdatedAt', date.millisecondsSinceEpoch)); + + final List records = await _store.findRecords(finder); + + for (final Record record in records) { + final DietPlan convertedDietPlan = convertRecordToItem(record: record); + foodItems.add(convertedDietPlan); + } + + if (records == null) { + return errorResponse; + } + + return ApiResponse(true, 200, foodItems, null); + } + + @override + Future remove(DietPlan item) async { + await _store.delete(item.objectId); + return ApiResponse(true, 200, null, null); + } + + @override + Future updateAll(List items) async { + final List updatedItems = List(); + + for (final DietPlan item in items) { + final ApiResponse response = await update(item); + if (response.success) { + final DietPlan responseItem = response.result; + updatedItems.add(responseItem); + } + } + + if (updatedItems == null) { + return errorResponse; + } + + return ApiResponse(true, 200, updatedItems, null); + } + + @override + Future update(DietPlan item) async { + final Map values = convertItemToStorageMap(item); + final dynamic returnedItems = await _store.update(values, item.objectId); + + if (returnedItems == null) { + return add(item); + } + + return ApiResponse( + true, 200, convertRecordToItem(values: returnedItems), null); + } + + Map convertItemToStorageMap(DietPlan item) { + final Map values = Map(); + // ignore: invalid_use_of_protected_member + values['value'] = json.jsonEncode(item.toJson(full: true)); + values['objectId'] = item.objectId; + if (item.updatedAt != null) { + values['updatedAt'] = item.updatedAt.millisecondsSinceEpoch; + } + + return values; + } + + DietPlan convertRecordToItem({Record record, Map values}) { + try { + values ??= record.value; + final DietPlan item = + DietPlan.clone().fromJson(json.jsonDecode(values['value'])); + return item; + } catch (e) { + return null; + } + } + + static ApiError error = ApiError(1, 'No records found', false, ''); + ApiResponse errorResponse = ApiResponse(false, 1, null, error); +} diff --git a/example/lib/data/repositories/diet_plan/repository_diet_plan.dart b/example/lib/data/repositories/diet_plan/repository_diet_plan.dart new file mode 100644 index 000000000..9d1b4775c --- /dev/null +++ b/example/lib/data/repositories/diet_plan/repository_diet_plan.dart @@ -0,0 +1,174 @@ +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:flutter_plugin_example/data/repositories/diet_plan/provider_api_diet_plan.dart'; +import 'package:flutter_plugin_example/data/repositories/diet_plan/provider_db_diet_plan.dart'; +import 'package:flutter_plugin_example/domain/utils/collection_utils.dart'; +import 'package:sembast/sembast.dart'; + +class DietPlanRepository implements DietPlanProviderContract { + static DietPlanRepository init(Database dbConnection, + {DietPlanProviderContract repositoryDB, + DietPlanProviderContract repositoryAPI}) { + final DietPlanRepository repository = DietPlanRepository(); + + if (repositoryDB != null) { + repository.db = repositoryDB; + } else { + final Store store = dbConnection.getStore('repository-$keyDietPlan'); + repository.db = DietPlanProviderDB(dbConnection, store); + } + + if (repositoryAPI != null) { + repository.api = repositoryAPI; + } else { + repository.api = DietPlanProviderApi(); + } + + return repository; + } + + DietPlanProviderContract api; + DietPlanProviderContract db; + + @override + Future add(DietPlan item, + {bool apiOnly = false, bool dbOnly = false}) async { + if (apiOnly) { + return await api.add(item); + } + if (dbOnly) { + return await db.add(item); + } + + final ApiResponse response = await api.add(item); + if (response.success) { + await db.add(item); + } + + return response; + } + + @override + Future addAll(List items, + {bool apiOnly = false, bool dbOnly = false}) async { + if (apiOnly) { + return await api.addAll(items); + } + if (dbOnly) { + return await db.addAll(items); + } + + final ApiResponse response = await api.addAll(items); + + if (response.success && isValidList(response.result)) { + await db.addAll(items); + } + + return response; + } + + @override + Future getAll( + {bool fromApi = false, bool fromDb = false}) async { + if (fromApi) { + return api.getAll(); + } + if (fromDb) { + return db.getAll(); + } + + ApiResponse response = await db.getAll(); + if (response.result == null) { + response = await api.getAll(); + } + + return db.getAll(); + } + + @override + Future getById(String id, + {bool fromApi = false, bool fromDb = false}) async { + if (fromApi) { + return api.getAll(); + } + if (fromDb) { + return db.getAll(); + } + + ApiResponse response = await db.getById(id); + if (response.result == null) { + response = await api.getById(id); + } + + return response; + } + + @override + Future getNewerThan(DateTime date, + {bool fromApi = false, bool fromDb = false}) async { + if (fromApi) { + return await api.getNewerThan(date); + } + if (fromDb) { + return await db.getNewerThan(date); + } + + final ApiResponse response = await api.getNewerThan(date); + + if (response.success && response.result != null) { + final List list = response.result; + await db.updateAll(list); + } + + return response; + } + + @override + Future remove(DietPlan item, + {bool apiOnly = false, bool dbOnly = false}) async { + if (apiOnly) { + return await api.remove(item); + } + if (dbOnly) { + return await db.remove(item); + } + + ApiResponse response = await api.remove(item); + response = await db.remove(item); + return response; + } + + @override + Future update(DietPlan item, + {bool apiOnly = false, bool dbOnly = false}) async { + if (apiOnly) { + return await api.update(item); + } + if (dbOnly) { + return await db.update(item); + } + + ApiResponse response = await api.update(item); + response = await db.update(item); + return response; + } + + @override + Future updateAll(List items, + {bool apiOnly = false, bool dbOnly = false}) async { + if (apiOnly) { + await api.updateAll(items); + } + if (dbOnly) { + await db.updateAll(items); + } + + ApiResponse response = await api.updateAll(items); + if (response.success && isValidList(response.result)) { + response = await db.updateAll(items); + } + + return response; + } +} diff --git a/example/lib/diet_plan.dart b/example/lib/diet_plan.dart deleted file mode 100644 index 747bc65bc..000000000 --- a/example/lib/diet_plan.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:core'; - -import 'package:parse_server_sdk/parse_server_sdk.dart'; - -class DietPlan extends ParseObject implements ParseCloneable { - DietPlan() : super(_keyTableName, debug: true); - DietPlan.clone() : this(); - - /// Looks strangely hacky but due to Flutter not using reflection, we have to - /// mimic a clone - @override - DietPlan clone(Map map) => DietPlan.clone()..fromJson(map); - - static const String _keyTableName = 'Diet_Plans'; - static const String keyName = 'Name'; - static const String keyDescription = 'Description'; - static const String keyProtein = 'Protein'; - static const String keyCarbs = 'Carbs'; - static const String keyFat = 'Fat'; - static const String keyStatus = 'Status'; - - String get name => get(keyName); - set name(String name) => set(keyName, name); - - String get description => get(keyDescription); - set description(String description) => set(keyDescription, name); - - int get protein => get(keyProtein); - set protein(int protein) => super.set(keyProtein, protein); - - int get carbs => get(keyCarbs); - set carbs(int carbs) => set(keyCarbs, carbs); - - int get fat => get(keyFat); - set fat(int fat) => set(keyFat, fat); - - int get status => get(keyStatus); - set status(int status) => set(keyStatus, status); -} diff --git a/example/lib/domain/constants/application_constants.dart b/example/lib/domain/constants/application_constants.dart new file mode 100644 index 000000000..01491a0dc --- /dev/null +++ b/example/lib/domain/constants/application_constants.dart @@ -0,0 +1,4 @@ +const String keyApplicationName = ''; +const String keyParseApplicationId = ''; +const String keyParseMasterKey = ''; +const String keyParseServerUrl = ''; diff --git a/example/lib/domain/utils/collection_utils.dart b/example/lib/domain/utils/collection_utils.dart new file mode 100644 index 000000000..5451c85ea --- /dev/null +++ b/example/lib/domain/utils/collection_utils.dart @@ -0,0 +1,5 @@ +bool isValidList(List list) => + (list != null && list.isNotEmpty) ? true : false; + +bool isValidMap(Map map) => + (map != null && map.isNotEmpty) ? true : false; diff --git a/example/lib/main.dart b/example/lib/ui/main.dart similarity index 77% rename from example/lib/main.dart rename to example/lib/ui/main.dart index 946941e82..1a578609c 100644 --- a/example/lib/main.dart +++ b/example/lib/ui/main.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; -import 'package:flutter_plugin_example/application_constants.dart'; -import 'package:flutter_plugin_example/diet_plan.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/repository_diet_plan.dart'; +import 'package:flutter_plugin_example/domain/constants/application_constants.dart'; import 'package:flutter_stetho/flutter_stetho.dart'; import 'package:parse_server_sdk/parse_server_sdk.dart'; @@ -38,21 +42,25 @@ class _MyAppState extends State { Future initParse() async { // Initialize parse - Parse().initialize(ApplicationConstants.keyParseApplicationId, - ApplicationConstants.keyParseServerUrl, - masterKey: ApplicationConstants.keyParseMasterKey, debug: true); + Parse().initialize(keyParseApplicationId, keyParseServerUrl, + masterKey: keyParseMasterKey, debug: true); // 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) { - runTestQueries(); + await runTestQueries(); } else { print('Server health check failed'); } } - void runTestQueries() { - createItem(); + Future runTestQueries() async { + // Basic repository example + await repositoryAddItems(); + await repositoryGetAllItems(); + + // Basic usage + /*createItem(); getAllItems(); getAllItemsByName(); getSingleItem(); @@ -60,7 +68,7 @@ class _MyAppState extends State { query(); initUser(); function(); - functionWithParameters(); + functionWithParameters();*/ } Future createItem() async { @@ -71,9 +79,7 @@ class _MyAppState extends State { final ParseResponse apiResponse = await newObject.create(); if (apiResponse.success && apiResponse.result != null) { - print(ApplicationConstants.keyAppName + - ': ' + - apiResponse.result.toString()); + print(keyAppName + ': ' + apiResponse.result.toString()); } } @@ -83,7 +89,7 @@ class _MyAppState extends State { if (apiResponse.success && apiResponse.result != null) { for (final ParseObject testObject in apiResponse.result) { - print(ApplicationConstants.keyAppName + ': ' + testObject.toString()); + print(keyAppName + ': ' + testObject.toString()); } } } @@ -93,10 +99,10 @@ class _MyAppState extends State { if (apiResponse.success && apiResponse.result != null) { for (final DietPlan plan in apiResponse.result) { - print(ApplicationConstants.keyAppName + ': ' + plan.name); + print(keyAppName + ': ' + plan.name); } } else { - print(ApplicationConstants.keyAppName + ': ' + apiResponse.error.message); + print(keyAppName + ': ' + apiResponse.error.message); } } @@ -124,7 +130,7 @@ class _MyAppState extends State { print('Retreiving from pin worked!'); } } else { - print(ApplicationConstants.keyAppName + ': ' + apiResponse.error.message); + print(keyAppName + ': ' + apiResponse.error.message); } } @@ -221,7 +227,7 @@ class _MyAppState extends State { if (apiResponse.success) { final List users = response.result; for (final ParseUser user in users) { - print(ApplicationConstants.keyAppName + ': ' + user.toString()); + print(keyAppName + ': ' + user.toString()); } } } @@ -259,4 +265,26 @@ class _MyAppState extends State { print('We have our configs.'); } } + + Future repositoryAddItems() async { + final List dietPlans = + const JsonDecoder().convert(dietPlansToAdd); + + final DietPlanRepository repository = DietPlanRepository(); + final ApiResponse response = await repository.addAll(dietPlans); + if (response.success) { + print(response.result); + } + } + + Future repositoryGetAllItems() async { + final DietPlanRepository repository = DietPlanRepository(); + final ApiResponse response = await repository.getAll(); + if (response.success) { + print(response.result); + } + } } + +const String dietPlansToAdd = + '[{"className":"Diet_Plans","objectId":"RlOj8JGnEX","createdAt":"2017-10-17T10:44:11.355Z","updatedAt":"2018-01-30T10:15:21.228Z","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}]'; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ba2f6813c..0c81f9927 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,10 +9,14 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 flutter_stetho: ^0.2.2 + sembast: ^1.13.3+1 dev_dependencies: parse_server_sdk: path: ../ + flutter_test: + sdk: flutter + mockito: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart b/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart new file mode 100644 index 000000000..c56b9312d --- /dev/null +++ b/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart @@ -0,0 +1,192 @@ +// ignore_for_file: invalid_use_of_protected_member +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:flutter_plugin_example/data/repositories/diet_plan/provider_api_diet_plan.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../repository_mock_utils.dart'; + +void main() { + DietPlanProviderContract repository; + + Future getRepository() async { + repository ??= DietPlanProviderApi(); + return repository; + } + + setUp(() async { + await setupParseInstance(); + await getRepository(); + }); + + tearDown(() async { + repository = null; + }); + + group('API Integration tests', () { + test('create DB instance', () async { + expect(true, repository != null); + }); + + test('add DietPlan from API', () async { + // Given + final DietPlan expected = getDummyDietPlan(); + expected.getObjectData()['objectId'] = null; + + // When + ApiResponse response = await repository.add(expected); + final DietPlan actual = response.result; + + // CLEAR FROM DB + response = await repository.remove(actual); + + // Then + expect(actual.protein, expected.protein); + }); + + test('addAll DietPlan from API', () async { + // Given + final List actual = List(); + final DietPlan item1 = getDummyDietPlan(); + item1.getObjectData()['objectId'] = null; + item1.protein = 5; + actual.add(item1); + final DietPlan item2 = getDummyDietPlan(); + item2.getObjectData()['objectId'] = null; + item2.protein = 6; + actual.add(item2); + + // When + final ApiResponse response = await repository.addAll(actual); + final List items = await response.result; + + // CLEAR FROM DB + for (final DietPlan item in items) { + await repository.remove(item); + } + + // Then + expect(response.success, true); + expect(actual[1].objectId, items[1].objectId); + }); + + test('getById DietPlan from API', () async { + // Given + final DietPlan dummy = getDummyDietPlan(); + dummy.getObjectData()['objectId'] = null; + + // When + ApiResponse response = await repository.add(dummy); + final DietPlan expected = response.result; + response = await repository.getById(expected.objectId); + final DietPlan actual = response.result; + + // CLEAR FROM DB + response = await repository.remove(actual); + + // Then + expect(actual.objectId, expected.objectId); + expect(actual.protein, expected.protein); + }); + + test('getNewerThan DietPlan from API', () async { + // Given + final DietPlan dummy = getDummyDietPlan(); + dummy.getObjectData()['objectId'] = null; + + // When + final ApiResponse baseResponse = await repository.add(dummy); + final DietPlan userFood = baseResponse.result; + final ApiResponse responseWithResult = await repository + .getNewerThan(DateTime.now().subtract(Duration(days: 1))); + final ApiResponse responseWithoutResult = + await repository.getNewerThan(DateTime.now().add(Duration(days: 1))); + + // CLEAR FROM DB + await repository.remove(userFood); + + // Then + expect(responseWithResult.success, true); + expect(responseWithoutResult.success, true); + expect(responseWithResult.result, isNotNull); + expect(responseWithoutResult.result, isNull); + }); + + test('getAll DietPlan from API', () async { + final List actual = List(); + + final DietPlan item1 = getDummyDietPlan(); + item1.getObjectData()['objectId'] = null; + item1.protein = 5; + actual.add(item1); + final DietPlan item2 = getDummyDietPlan(); + item2.getObjectData()['objectId'] = null; + item2.protein = 6; + actual.add(item2); + + // When + final ApiResponse response = await repository.addAll(actual); + final List items = await response.result; + + // CLEAR FROM DB + for (final DietPlan item in items) { + await repository.remove(item); + } + + // Then + expect(response.success, true); + expect(response.result, isNotNull); + }); + + test('update DietPlan from API', () async { + // Given + final DietPlan expected = getDummyDietPlan(); + expected.getObjectData()['objectId'] = null; + ApiResponse response = await repository.add(expected); + final DietPlan initialResponse = response.result; + + // When + initialResponse.protein = 10; + final ApiResponse updateResponse = + await repository.update(initialResponse); + final DietPlan actual = updateResponse.result; + + // CLEAR FROM DB + response = await repository.remove(actual); + + // Then + expect(actual.protein, 10); + }); + + test('updateAll DietPlan from API', () async { + // Given + final List actual = List(); + + final DietPlan item1 = getDummyDietPlan(); + item1.getObjectData()['objectId'] = null; + item1.protein = 7; + actual.add(item1); + final DietPlan item2 = getDummyDietPlan(); + item2.getObjectData()['objectId'] = null; + item2.protein = 8; + actual.add(item2); + await repository.addAll(actual); + + // When + item1.protein = 9; + item2.protein = 10; + final ApiResponse updateResponse = await repository.updateAll(actual); + final List updated = updateResponse.result; + + // CLEAR FROM DB + for (final DietPlan day in updated) { + await repository.remove(day); + } + + // Then + expect(updated[0].protein, 9); + expect(updated[1].protein, 10); + }); + }); +} diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart b/example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart new file mode 100644 index 000000000..d1d2e8394 --- /dev/null +++ b/example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart @@ -0,0 +1,185 @@ +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:flutter_plugin_example/data/repositories/diet_plan/provider_db_diet_plan.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sembast/sembast.dart'; + +import '../repository_mock_utils.dart'; + +void main() { + DietPlanProviderContract repository; + + Store _getStore(Database database) { + return database.getStore('repository_$keyDietPlan'); + } + + Future getRepository() async { + if (repository == null) { + final Database database = await getDB(); + repository ??= DietPlanProviderDB(database, _getStore(database)); + } + + return repository; + } + + setUp(() async { + await setupParseInstance(); + await getRepository(); + }); + + tearDown(() async { + final Database database = await getDB(); + final Store store = _getStore(database); + store.clear(); + database.clear(); + }); + + test('create DB instance', () async { + expect(true, repository != null); + }); + + test('add DietPlan from DB', () async { + // Given + final DietPlan expected = getDummyDietPlan(); + + // When + final ApiResponse response = await repository.add(expected); + final DietPlan actual = response.result; + + // Then + expect(actual.objectId, expected.objectId); + expect(actual.protein, expected.protein); + }); + + test('addAll DietPlan from DB', () async { + // Given + const String objectIdPrefix = '12345abc'; + final List actual = List(); + + final DietPlan item1 = getDummyDietPlan(); + item1.objectId = '${objectIdPrefix}0'; + actual.add(item1); + + final DietPlan item2 = getDummyDietPlan(); + item2.objectId = '${objectIdPrefix}1'; + actual.add(item2); + + // When + final ApiResponse response = await repository.addAll(actual); + final List items = await response.result; + + // Then + expect(response.success, true); + expect(actual[0].objectId, items[0].objectId); + expect(actual[1].objectId, items[1].objectId); + }); + + test('getById DietPlan from DB', () async { + // Given + final DietPlan actual = getDummyDietPlan(); + + // When + await repository.add(actual); + final ApiResponse response = await repository.getById('1234abcd'); + + // Then + final DietPlan expected = response.result; + expect(actual.objectId, expected.objectId); + expect(actual.protein, expected.protein); + }); + + test('getAll DietPlan from DB', () async { + // Given + const String objectIdPrefix = '12345abc'; + final DietPlan item1 = getDummyDietPlan()..objectId = '${objectIdPrefix}0'; + final DietPlan item2 = getDummyDietPlan()..objectId = '${objectIdPrefix}1'; + final List actual = List()..add(item1)..add(item2); + + // When + await repository.addAll(actual); + + // Then + final ApiResponse response = await repository.getAll(); + final List expected = response.result; + + expect(2, expected.length); + expect(actual[0].objectId, expected[0].objectId); + expect(actual[1].objectId, expected[1].objectId); + }); + + test('getNewerThan DietPlan from DB', () async { + // Given + final DietPlan expected = getDummyDietPlan(); + // ignore: invalid_use_of_protected_member + expected.getObjectData()['keyUpdatedAt'] = DateTime.now(); + await repository.add(expected); + + // When + DateTime dateTime = DateTime.now(); + dateTime = dateTime.subtract(Duration(hours: 1)); + final ApiResponse response = await repository.getNewerThan(dateTime); + final List actual = response.result; + + // Then + expect(actual.isNotEmpty, true); + expect(actual.first.objectId, expected.objectId); + }); + + test('update DietPlan from DB', () async { + // Given + final DietPlan item = getDummyDietPlan(); + item.protein = 1000; + await repository.add(item); + + // When + item.protein = 1000; + final ApiResponse response = await repository.update(item); + final DietPlan userFood = response.result; + + // Then + expect(item.objectId, userFood.objectId); + expect(userFood.protein, 1000); + }); + + test('updateAll DietPlan from DB', () async { + // Given + const String objectIdPrefix = '12345abc'; + + final List actual = List(); + final DietPlan item1 = getDummyDietPlan(); + item1.objectId = '${objectIdPrefix}0'; + actual.add(item1); + + final DietPlan item2 = getDummyDietPlan(); + item2.objectId = '${objectIdPrefix}1'; + actual.add(item2); + + await repository.addAll(actual); + + // When + actual[0].protein = 1000; + actual[1].protein = 1000; + final ApiResponse response = await repository.updateAll(actual); + final List expected = response.result; + + // Then + expect(actual[0].objectId, expected[0].objectId); + expect(actual[1].objectId, expected[1].objectId); + expect(expected[0].protein, 1000); + expect(expected[1].protein, 1000); + }); + + test('delete DietPlan from DB', () async { + // Given + final DietPlan actual = getDummyDietPlan(); + await repository.add(actual); + + // When + await repository.remove(actual); + final ApiResponse response = await repository.getById(actual.objectId); + + // Then + expect(response.result == null, true); + }); +} diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_test.dart b/example/test/data/repository/diet_plan/repository_diet_plan_test.dart new file mode 100644 index 000000000..e10e34fc6 --- /dev/null +++ b/example/test/data/repository/diet_plan/repository_diet_plan_test.dart @@ -0,0 +1,130 @@ +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:flutter_plugin_example/data/repositories/diet_plan/repository_diet_plan.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../repository_mock_utils.dart'; + +void main() { + DietPlanRepository repository; + + DietPlanProviderContract apiRepository; + DietPlanProviderContract dbRepository; + + Future getApiRepository() async { + final DietPlanProviderContract repositoryApi = MockDietPlanProviderApi(); + + const String objectIdPrefix = '12345abc'; + final DietPlan item1 = getDummyDietPlan()..objectId = '${objectIdPrefix}0'; + final DietPlan item2 = getDummyDietPlan()..objectId = '${objectIdPrefix}1'; + final List mockList = List()..add(item1)..add(item2); + + when(repositoryApi.add(any)).thenAnswer((_) async => + Future.value( + ApiResponse(true, 200, getDummyDietPlan(), null))); + when(repositoryApi.addAll(any)).thenAnswer((_) async => + Future.value(ApiResponse(true, 200, mockList, null))); + when(repositoryApi.update(any)).thenAnswer((_) async => + Future.value( + ApiResponse(true, 200, getDummyDietPlan(), null))); + when(repositoryApi.updateAll(any)).thenAnswer((_) async => + Future.value(ApiResponse(true, 200, mockList, null))); + when(repositoryApi.getNewerThan(any)).thenAnswer((_) async => + Future.value(ApiResponse(true, 200, mockList, null))); + when(repositoryApi.getById(any)).thenAnswer((_) async => + Future.value( + ApiResponse(true, 200, getDummyDietPlan(), null))); + when(repositoryApi.getById(any)).thenAnswer((_) async => + Future.value(ApiResponse(true, 200, mockList, null))); + + return repositoryApi; + } + + Future getDBRepository() { + return Future.value(MockDietPlanProviderDB()); + } + + Future getRepository() async { + apiRepository = await getApiRepository(); + dbRepository = await getDBRepository(); + + final DietPlanRepository repository = DietPlanRepository.init(null, + repositoryDB: dbRepository, repositoryAPI: apiRepository); + + return repository; + } + + setUp(() async { + await setupParseInstance(); + repository = await getRepository(); + }); + + test('create DB instance', () async { + expect(true, repository != null); + }); + + test('add DietPlan from DB', () async { + // Given && When + await repository.add(any); + + // Then + verify(dbRepository.add(any)).called(1); + verify(apiRepository.add(any)).called(1); + }); + + test('addAll DietPlan from DB', () async { + // Given && When + await repository.addAll(any); + + // Then + verify(dbRepository.addAll(any)).called(1); + verify(apiRepository.addAll(any)).called(1); + }); + + test('getAll DietPlan from DB', () async { + // Given && When + await repository.getAll(); + + // Then + verify(dbRepository.getAll()).called(1); + verifyNever(apiRepository.getAll()); + }); + + test('getAll DietPlan from API', () async { + // Given && When + await repository.getAll(fromApi: true); + + // Then + verifyNever(dbRepository.getAll()); + verify(apiRepository.getAll()).called(1); + }); + + test('getNewerThan DietPlan from DB', () async { + // Given && When + await repository.getNewerThan(DateTime.now()); + + // Then + verifyNever(dbRepository.getNewerThan(DateTime.now())); + verify(apiRepository.getNewerThan(any)); + }); + + test('updateAll DietPlan from DB', () async { + // Given && When + await repository.updateAll(any); + + // Then + verify(dbRepository.updateAll(any)).called(1); + verify(apiRepository.updateAll(any)).called(1); + }); + + test('delete DietPlan from DB', () async { + // Given && When + await repository.remove(any); + + // Then + verify(dbRepository.remove(any)).called(1); + verify(apiRepository.remove(any)).called(1); + }); +} diff --git a/example/test/data/repository/repository_mock_utils.dart b/example/test/data/repository/repository_mock_utils.dart new file mode 100644 index 000000000..6a89f3ca7 --- /dev/null +++ b/example/test/data/repository/repository_mock_utils.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +import 'package:flutter_plugin_example/data/model/diet_plan.dart'; +import 'package:flutter_plugin_example/data/repositories/diet_plan/provider_api_diet_plan.dart'; +import 'package:flutter_plugin_example/data/repositories/diet_plan/provider_db_diet_plan.dart'; +import 'package:flutter_plugin_example/domain/constants/application_constants.dart'; +import 'package:mockito/mockito.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:path/path.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; + +class MockDietPlanProviderApi extends Mock implements DietPlanProviderApi {} + +class MockDietPlanProviderDB extends Mock implements DietPlanProviderDB {} + +Future getDB() async { + final String dbDirectory = Directory.current.path; + final String dbPath = join(dbDirectory, 'no_sql_test'); + final DatabaseFactory dbFactory = databaseFactoryIo; + return await dbFactory.openDatabase(dbPath); +} + +Future setupParseInstance() async { + Parse().initialize(keyParseApplicationId, keyParseServerUrl, + masterKey: keyParseMasterKey, appName: keyApplicationName, debug: true); +} + +DietPlan getDummyDietPlan() { + return DietPlan() + ..set('objectId', '1234abcd') + ..set(keyVarUpdatedAt, DateTime.now()) + ..name = 'Test Diet Plan' + ..description = 'Some random description about a diet plan' + ..protein = 40 + ..carbs = 40 + ..fat = 20 + ..status = 0; +} diff --git a/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index cc3021fc2..1dc98e59d 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.16'; +const String keySdkVersion = '1.0.17'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points From 2ea134d69f1d5d1aea4ae669e57397b84762562c Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Sun, 14 Apr 2019 14:12:40 -0300 Subject: [PATCH 2/7] Bugfix save and create object / LiveQuery (#157) Bugfix save and create object / LiveQuery --- lib/src/network/parse_live_query.dart | 2 +- lib/src/objects/parse_object.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 066920f8b..71a5d7cdc 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -98,7 +98,7 @@ class LiveQuery { if (_debug) { print('$_printConstLiveQuery: Done'); } - }, onError: (Error error) { + }, onError: (error) { if (_debug) { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 113b19c82..73fb5e871 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -65,7 +65,7 @@ class ParseObject extends ParseBase implements ParseCloneable { Future create() async { try { final Uri url = getSanitisedUri(_client, '$_path'); - final String body = json.encode(toJson()); + 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. @@ -89,7 +89,7 @@ class ParseObject extends ParseBase implements ParseCloneable { } else { try { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); - final String body = json.encode(toJson()); + 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); From 31b80d7272f60dd03cd1677b7fdf73560c0ccbda Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Mon, 15 Apr 2019 08:01:03 -0300 Subject: [PATCH 3/7] Bugfix create object (#158) * Bugfix save and create object / LiveQuery Bugfix save and create object / LiveQuery * Bugfix create object Bugfix create object --- lib/src/objects/parse_object.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 73fb5e871..6b474fb9a 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -65,7 +65,7 @@ class ParseObject extends ParseBase implements ParseCloneable { Future create() async { try { final Uri url = getSanitisedUri(_client, '$_path'); - final String body = json.encode(toJson(forApiRQ: true); + 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. @@ -74,7 +74,7 @@ class ParseObject extends ParseBase implements ParseCloneable { final Map map = json.decode(result.body); objectId = map['objectId'].toString(); } - + return handleResponse( this, result, ParseApiRQ.create, _debug, className); } on Exception catch (e) { From 8b6ace560fd819fdd68bf5f1e06ece75444edd8d Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Mon, 22 Apr 2019 07:19:56 -0300 Subject: [PATCH 4/7] ParseACL implementation / Documentation Review (#160) * Bugfix save and create object / LiveQuery Bugfix save and create object / LiveQuery * Bugfix create object Bugfix create object * ParseACL implementation ParseACL implementation * Update README.MD Update README.MD with ParseACL and ParseCloudFunction * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md --- README.md | 347 +++++++++++++++++++++----------- lib/parse_server_sdk.dart | 2 + lib/src/objects/parse_acl.dart | 150 ++++++++++++++ lib/src/objects/parse_base.dart | 18 +- 4 files changed, 399 insertions(+), 118 deletions(-) create mode 100644 lib/src/objects/parse_acl.dart diff --git a/README.md b/README.md index 7c0f66bb3..668770a47 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,27 @@ You can create custom objects by calling: var dietPlan = ParseObject('DietPlan') ..set('Name', 'Ketogenic') ..set('Fat', 65); +await dietPlan.save() ``` +Verify that the object has been successfully saved using +```dart +var response = await dietPlan.save(); +if (response.success) { + dietPlan = response.result; +} +``` +Types supported: + * String + * Double + * Int + * Boolean + * DateTime + * File + * Geopoint + * ParseObject/ParseUser (Pointer) + * Map + * List (all types supported) + You then have the ability to do the following with that object: The features available are:- * Get @@ -110,6 +130,14 @@ Retrieve it, call ```dart var response = await dietPlan.increment("count", 1); +``` +or using with save function + +```dart +dietPlan.setIncrement('count', 1); +dietPlan.setDecrement('count', 1); +var response = dietPlan.save() + ``` ## Array Operator in objects @@ -122,6 +150,14 @@ var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]); 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']); +var response = dietPlan.save() ``` ## Queries @@ -129,41 +165,41 @@ Once you have setup the project and initialised the instance, you can then retre ```dart var apiResponse = await ParseObject('ParseTableName').getAll(); - if (apiResponse.success){ - for (var testObject in apiResponse.result) { - print(ApplicationConstants.APP_NAME + ": " + testObject.toString()); - } - } +if (apiResponse.success){ + for (var testObject in apiResponse.result) { + print(ApplicationConstants.APP_NAME + ": " + testObject.toString()); + } +} ``` Or you can get an object by its objectId: ```dart var dietPlan = await DietPlan().getObject('R5EonpUDWy'); - if (dietPlan.success) { - print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString()); - } else { - print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message); - } +if (dietPlan.success) { + print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString()); +} else { + print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message); +} ``` ## Complex queries You can create complex queries to really put your database to the test: ```dart - var queryBuilder = QueryBuilder(DietPlan()) - ..startsWith(DietPlan.keyName, "Keto") - ..greaterThan(DietPlan.keyFat, 64) - ..lessThan(DietPlan.keyFat, 66) - ..equals(DietPlan.keyCarbs, 5); - - var response = await queryBuilder.query(); - - if (response.success) { - print(ApplicationConstants.keyAppName + ": " + ((response.result as List).first as DietPlan).toString()); - } else { - print(ApplicationConstants.keyAppName + ": " + response.exception.message); - } +var queryBuilder = QueryBuilder(DietPlan()) + ..startsWith(DietPlan.keyName, "Keto") + ..greaterThan(DietPlan.keyFat, 64) + ..lessThan(DietPlan.keyFat, 66) + ..equals(DietPlan.keyCarbs, 5); + +var response = await queryBuilder.query(); + +if (response.success) { + print(ApplicationConstants.keyAppName + ": " + ((response.result as List).first as DietPlan).toString()); +} else { + print(ApplicationConstants.keyAppName + ": " + response.exception.message); +} ``` The features available are:- @@ -197,15 +233,15 @@ For example, imagine you have Post class and a Comment class, where each Comment You can find comments on posts with images by doing: ```dart - QueryBuilder queryPost = - QueryBuilder(ParseObject('Post')) - ..whereValueExists('image', true); +QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); - QueryBuilder queryComment = - QueryBuilder(ParseObject('Comment')) - ..whereMatchesQuery('post', queryPost); +QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereMatchesQuery('post', queryPost); - var apiResponse = await queryComment.query(); +var apiResponse = await queryComment.query(); ``` If you want to retrieve objects where a field contains an object that does not match another query, you can use the @@ -214,28 +250,28 @@ Imagine you have Post class and a Comment class, where each Comment has a pointe You can find comments on posts without images by doing: ```dart - QueryBuilder queryPost = - QueryBuilder(ParseObject('Post')) - ..whereValueExists('image', true); +QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); - QueryBuilder queryComment = - QueryBuilder(ParseObject('Comment')) - ..whereDoesNotMatchQuery('post', queryPost); +QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereDoesNotMatchQuery('post', queryPost); - var apiResponse = await queryComment.query(); +var apiResponse = await queryComment.query(); ``` ## Counting Objects If you only care about the number of games played by a particular player: ```dart - QueryBuilder queryPlayers = - QueryBuilder(ParseObject('GameScore')) - ..whereEqualTo('playerName', 'Jonathan Walsh'); - var apiResponse = await queryPlayers.count(); - if (apiResponse.success && apiResponse.result != null) { - int countGames = apiResponse.count; - } +QueryBuilder queryPlayers = + QueryBuilder(ParseObject('GameScore')) + ..whereEqualTo('playerName', 'Jonathan Walsh'); +var apiResponse = await queryPlayers.count(); +if (apiResponse.success && apiResponse.result != null) { + int countGames = apiResponse.count; +} ``` ## Live Queries @@ -249,32 +285,32 @@ The Parse Server configuration guide on the server is found here https://docs.pa Initialize the Parse Live Query by entering the parameter liveQueryUrl in Parse().initialize: ```dart - Parse().initialize( - ApplicationConstants.keyApplicationId, - ApplicationConstants.keyParseServerUrl, - clientKey: ApplicationConstants.keyParseClientKey, - debug: true, - liveQueryUrl: ApplicationConstants.keyLiveQueryUrl, - autoSendSessionId: true); +Parse().initialize( + ApplicationConstants.keyApplicationId, + ApplicationConstants.keyParseServerUrl, + clientKey: ApplicationConstants.keyParseClientKey, + debug: true, + liveQueryUrl: ApplicationConstants.keyLiveQueryUrl, + autoSendSessionId: true); ``` Declare LiveQuery: ```dart - final LiveQuery liveQuery = LiveQuery(); +final LiveQuery liveQuery = LiveQuery(); ``` Set the QueryBuilder that will be monitored by LiveQuery: ```dart - QueryBuilder query = - QueryBuilder(ParseObject('TestAPI')) - ..whereEqualTo('intNumber', 1); +QueryBuilder query = + QueryBuilder(ParseObject('TestAPI')) + ..whereEqualTo('intNumber', 1); ``` __Create a subscription__ You’ll get the LiveQuery events through this subscription. The first time you call subscribe, we’ll try to open the WebSocket connection to the LiveQuery server for you. ```dart - await liveQuery.subscribe(query); +await liveQuery.subscribe(query); ``` __Event Handling__ @@ -284,15 +320,15 @@ __Create event__ When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event. The object is the ParseObject which was created. ```dart - liveQuery.on(LiveQueryEvent.create, (value) { - print('*** CREATE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); - }); +liveQuery.on(LiveQueryEvent.create, (value) { + print('*** CREATE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); ``` __Update event__ @@ -300,15 +336,15 @@ When an existing ParseObject which fulfills the QueryBuilder you subscribe is up QueryBuilder before and after changes), you’ll get this event. The object is the ParseObject which was updated. Its content is the latest value of the ParseObject. ```dart - liveQuery.on(LiveQueryEvent.update, (value) { - print('*** UPDATE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); - }); +liveQuery.on(LiveQueryEvent.update, (value) { + print('*** UPDATE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); ``` __Enter event__ @@ -316,15 +352,15 @@ When an existing ParseObject’s old value does not fulfill the QueryBuilder but you’ll get this event. The object is the ParseObject which enters the QueryBuilder. Its content is the latest value of the ParseObject. ```dart - liveQuery.on(LiveQueryEvent.enter, (value) { - print('*** ENTER ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); - }); +liveQuery.on(LiveQueryEvent.enter, (value) { + print('*** ENTER ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); ``` __Leave event__ @@ -332,30 +368,30 @@ When an existing ParseObject’s old value fulfills the QueryBuilder but its new you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. Its content is the latest value of the ParseObject. ```dart - liveQuery.on(LiveQueryEvent.leave, (value) { - print('*** LEAVE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); - }); +liveQuery.on(LiveQueryEvent.leave, (value) { + print('*** LEAVE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); ``` __Delete event__ When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event. The object is the ParseObject which is deleted ```dart - liveQuery.on(LiveQueryEvent.delete, (value) { - print('*** DELETE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); - }); +liveQuery.on(LiveQueryEvent.delete, (value) { + print('*** DELETE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); ``` __Unsubscribe__ @@ -364,7 +400,7 @@ After that, you won’t get any events from the subscription object and will clo LiveQuery server. ```dart - await liveQuery.unSubscribe(); +await liveQuery.unSubscribe(); ``` ## Users @@ -380,11 +416,18 @@ Then have the user sign up: var response = await user.signUp(); if (response.success) user = response.result; ``` -You can also logout and login with the user: +You can also login with the user: ```dart var response = await user.login(); if (response.success) user = response.result; ``` +You can also logout with the user: +```dart +var response = await user.logout(); +if (response.success) { + print('User logout'); +} +``` Also, once logged in you can manage sessions tokens. This feature can be called after Parse().init() on startup to check for a logged in user. ```dart user = ParseUser.currentUser(); @@ -397,8 +440,64 @@ Other user features are:- * Destroy user * Queries +## Security for Objects - ParseACL +For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. +To support this type of security, each object has an access control list, implemented by the __ParseACL__ class. + +If ParseACL is not specified (with the exception of the ParseUser class) all objects are set to Public for read and write. +The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. +To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that +limits access to that user. An object’s ACL is updated when the object is saved, like any other property. + +```dart +ParseUser user = await ParseUser.currentUser() as ParseUser; +ParseACL parseACL = ParseACL(owner: user); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using +__setReadAccess__ and __setWriteAccess__ +```dart +ParseUser user = await ParseUser.currentUser() as ParseUser; +ParseACL parseACL = ParseACL(); +//grant total access to current user +parseACL.setReadAccess(userId: user.objectId, allowed: true); +parseACL.setWriteAccess(userId: user.objectId, allowed: true); +//grant read access to userId: 'TjRuDjuSAO' +parseACL.setReadAccess(userId: 'TjRuDjuSAO', allowed: true); +parseACL.setWriteAccess(userId: 'TjRuDjuSAO', allowed: false); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +You can also grant permissions to all users at once using setPublicReadAccess and setPublicWriteAccess. +```dart +ParseACL parseACL = ParseACL(); +parseACL.setPublicReadAccess(allowed: true); +parseACL.setPublicWriteAccess(allowed: true); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +Operations that are forbidden, such as deleting an object that you do not have write access to, result in a +ParseError with code 101: 'ObjectNotFound'. +For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which +object ids do not exist at all. + +You can retrieve the ACL list of an object using: +```dart +ParseACL parseACL = parseObject.getACL(); +``` + ## Config -The SDK now supports Parse Config. A map of all configs can be grabbed from the server by calling : +The SDK supports Parse Config. A map of all configs can be grabbed from the server by calling : ```dart var response = await ParseConfig().getConfigs(); ``` @@ -408,27 +507,41 @@ and to add a config: ParseConfig().addConfig('TestConfig', 'testing'); ``` +## Cloud Functions +The SDK supports call Cloud Functions. + +Executes a cloud function that returns a ParseObject type +```dart +final ParseCloudFunction function = ParseCloudFunction('hello'); +final ParseResponse result = + await function.executeObjectFunction(); +if (result.success) { + if (result.result is ParseObject) { + final ParseObject parseObject = result.result; + print(parseObject.className); + } +} +``` + +Executes a cloud function with parameters +```dart +final ParseCloudFunction function = ParseCloudFunction('hello'); +final Map params = {'plan': 'paid'}; +function.execute(parameters: params); +``` + ## Other Features of this library Main: -* Installation -* GeoPoints -* Files +* Installation (View the example application) +* GeoPoints (View the example application) +* Files (View the example application) * Persistent storage * Debug Mode - Logging API calls * Manage Session ID's tokens User: -* Create -* Login -* Logout -* CurrentUser -* RequestPasswordReset -* VerificationEmailRequest -* AllUsers -* Save -* Destroy * Queries -* Anonymous +* Anonymous (View the example application) * 3rd Party Authentication Objects: diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 0ec5bcf6e..5541d9a94 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -62,6 +62,8 @@ part 'src/objects/parse_session.dart'; part 'src/objects/parse_user.dart'; +part 'src/objects/parse_acl.dart'; + part 'src/utils/parse_decoder.dart'; part 'src/utils/parse_encoder.dart'; diff --git a/lib/src/objects/parse_acl.dart b/lib/src/objects/parse_acl.dart new file mode 100644 index 000000000..f58dcb209 --- /dev/null +++ b/lib/src/objects/parse_acl.dart @@ -0,0 +1,150 @@ +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] +/// 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 +class ParseACL { + ///Creates an ACL where only the provided user has access. + ///[owner] The only user that can read or write objects governed by this ACL. + ParseACL({ParseUser owner}) { + if (owner != null) { + setReadAccess(userId: owner.objectId, allowed: true); + setWriteAccess(userId: owner.objectId, allowed: true); + } + } + + final String _publicKEY = '*'; + final Map _permissionsById = {}; + + /// Helper for setting stuff + void _setPermissionsIfNonEmpty( + {@required String userId, bool readPermission, bool writePermission}) { + if (!(readPermission || writePermission)) { + _permissionsById.remove(userId); + } else { + _permissionsById[userId] = + _ACLPermissions(readPermission, writePermission); + } + } + + ///Get whether the public is allowed to read this object. + bool getPublicReadAccess() { + return getReadAccess(userId: _publicKEY); + } + + ///Set whether the public is allowed to read this object. + void setPublicReadAccess({@required bool allowed}) { + setReadAccess(userId: _publicKEY, allowed: allowed); + } + + /// Set whether the public is allowed to write this object. + bool getPublicWriteAccess() { + return getWriteAccess(userId: _publicKEY); + } + + ///Set whether the public is allowed to write this object. + void setPublicWriteAccess({@required bool allowed}) { + setWriteAccess(userId: _publicKEY, allowed: allowed); + } + + ///Set whether the given user id is allowed to read this object. + void setReadAccess({@required String userId, bool allowed}) { + if (userId == null) { + throw 'cannot setReadAccess for null userId'; + } + final bool writePermission = getWriteAccess(userId: userId); + _setPermissionsIfNonEmpty( + userId: userId, + readPermission: allowed, + writePermission: writePermission); + } + + /// Get whether the given user id is *explicitly* allowed to read this object. Even if this returns + /// [false], the user may still be able to access it if getPublicReadAccess returns + /// [true] or a role that the user belongs to has read access. + bool getReadAccess({@required String userId}) { + if (userId == null) { + throw 'cannot getReadAccess for null userId'; + } + final _ACLPermissions _permissions = _permissionsById[userId]; + return _permissions != null && _permissions.getReadPermission(); + } + + ///Set whether the given user id is allowed to write this object. + void setWriteAccess({@required String userId, bool allowed}) { + if (userId == null) { + throw 'cannot setWriteAccess for null userId'; + } + final bool readPermission = getReadAccess(userId: userId); + _setPermissionsIfNonEmpty( + userId: userId, + readPermission: readPermission, + writePermission: allowed); + } + + ///Get whether the given user id is *explicitly* allowed to write this object. Even if this + ///returns [false], the user may still be able to write it if getPublicWriteAccess returns + ///[true] or a role that the user belongs to has write access. + bool getWriteAccess({@required String userId}) { + if (userId == null) { + throw 'cannot getWriteAccess for null userId'; + } + final _ACLPermissions _permissions = _permissionsById[userId]; + return _permissions != null && _permissions.getReadPermission(); + } + + Map toJson() { + final Map map = {}; + _permissionsById.forEach((String user, _ACLPermissions permission) { + map[user] = permission.toJson(); + }); + print(map); + return map; + } + + @override + String toString() => json.encode(toJson()); + + ParseACL fromJson(Map map) { + final ParseACL parseACL = ParseACL(); + + map.forEach((String userId, dynamic permission) { + if (permission['read'] != null) { + parseACL.setReadAccess(userId: userId, allowed: permission['read']); + } + if (permission['write'] != null) { + parseACL.setWriteAccess(userId: userId, allowed: permission['write']); + } + }); + return parseACL; + } +} + +class _ACLPermissions { + _ACLPermissions(this._readPermission, this._writePermission); + final String _keyReadPermission = 'read'; + final String _keyWritePermission = 'write'; + bool _readPermission = false; + bool _writePermission = false; + + bool getReadPermission() { + return _readPermission; + } + + bool getWritePermission() { + return _writePermission; + } + + Map toJson() { + final Map map = {}; + if (_readPermission) { + map[_keyReadPermission] = true; + } + if (_writePermission) { + map[_keyWritePermission] = true; + } + return map; + } +} diff --git a/lib/src/objects/parse_base.dart b/lib/src/objects/parse_base.dart index 2cdd579db..906f40ee2 100644 --- a/lib/src/objects/parse_base.dart +++ b/lib/src/objects/parse_base.dart @@ -65,7 +65,7 @@ abstract class ParseBase { map.remove(keyVarCreatedAt); map.remove(keyVarUpdatedAt); map.remove(keyVarClassName); - map.remove(keyVarAcl); + //map.remove(keyVarAcl); map.remove(keyParamSessionToken); } @@ -97,6 +97,8 @@ abstract class ParseBase { } else { set(keyVarUpdatedAt, value); } + } else if (key == keyVarAcl) { + getObjectData()[keyVarAcl] = ParseACL().fromJson(value); } else { getObjectData()[key] = parseDecode(value); } @@ -142,6 +144,20 @@ abstract class ParseBase { } } + ///Set the [ParseACL] governing this object. + void setACL(ParseACL acl) { + getObjectData()[keyVarAcl] = acl; + } + + ///Access the [ParseACL] governing this object. + ParseACL getACL() { + if (getObjectData().containsKey(keyVarAcl)) { + return getObjectData()[keyVarAcl]; + } else { + return ParseACL(); + } + } + /// Gets type [T] from objectData /// /// Returns null or [defaultValue] if provided. To get an int, call From a2ebe9c6b55c7c7ce84989833033e026661fbfa7 Mon Sep 17 00:00:00 2001 From: Phill Date: Mon, 22 Apr 2019 11:30:35 +0100 Subject: [PATCH 5/7] Added emailVerified to user object --- example/lib/ui/main.dart | 8 ++++---- lib/src/network/parse_live_query.dart | 2 +- lib/src/objects/parse_user.dart | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/example/lib/ui/main.dart b/example/lib/ui/main.dart index b5426b9a0..0e127bd16 100644 --- a/example/lib/ui/main.dart +++ b/example/lib/ui/main.dart @@ -65,11 +65,11 @@ class _MyAppState extends State { Future runTestQueries() async { // Basic repository example await repositoryAddUser(); - /*await repositoryAddItems(); - await repositoryGetAllItems()*/ + await repositoryAddItems(); + await repositoryGetAllItems(); //Basic usage - /*createItem(); + createItem(); getAllItems(); getAllItemsByName(); getSingleItem(); @@ -78,7 +78,7 @@ class _MyAppState extends State { initUser(); function(); functionWithParameters(); - test();*/ + test(); } Future test() async { diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 71a5d7cdc..066920f8b 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -98,7 +98,7 @@ class LiveQuery { if (_debug) { print('$_printConstLiveQuery: Done'); } - }, onError: (error) { + }, onError: (Error error) { if (_debug) { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index 2cc6a7d18..c4cb76f4d 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -36,6 +36,7 @@ class ParseUser extends ParseObject implements ParseCloneable { return fromJson(map); } + static const String keyEmailVerified = 'emailVerified'; static const String keyUsername = 'username'; static const String keyEmailAddress = 'email'; static const String path = '$keyEndPointClasses$keyClassUser'; @@ -45,6 +46,11 @@ class ParseUser extends ParseObject implements ParseCloneable { set acl(Map acl) => set>(keyVarAcl, acl); + bool get emailVerified => super.get(keyEmailVerified); + + set emailVerified(bool emailVerified) => + set(keyEmailVerified, emailVerified); + String get username => super.get(keyVarUsername); set username(String username) => set(keyVarUsername, username); @@ -108,7 +114,6 @@ class ParseUser extends ParseObject implements ParseCloneable { /// fromServer can be called and an updated version of the [User] object will be /// returned static Future currentUser({ParseCloneable customUserObject}) async { - if (customUserObject != null) { return await _getUserFromLocalStore(cloneable: customUserObject); } else { From b2896a7c13b89b26a22dfd7c8cebc32cbab72c20 Mon Sep 17 00:00:00 2001 From: Phill Date: Mon, 22 Apr 2019 11:33:35 +0100 Subject: [PATCH 6/7] ParseResponse accepts list results and count --- lib/src/objects/parse_acl.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/objects/parse_acl.dart b/lib/src/objects/parse_acl.dart index f58dcb209..2d3ff2e5e 100644 --- a/lib/src/objects/parse_acl.dart +++ b/lib/src/objects/parse_acl.dart @@ -100,7 +100,6 @@ class ParseACL { _permissionsById.forEach((String user, _ACLPermissions permission) { map[user] = permission.toJson(); }); - print(map); return map; } From 1478c08a96c1bf6f579d67e2cde5b97c93439652 Mon Sep 17 00:00:00 2001 From: Phill Date: Mon, 22 Apr 2019 11:35:47 +0100 Subject: [PATCH 7/7] Updated version codes --- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a0f72dfd..6f112b713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.20 +ACL now working +emailVerified + ## 1.0.19 Bug fix diff --git a/README.md b/README.md index 668770a47..2f080a3e1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Want to get involved? Join our Slack channel and help out! (http://flutter-parse To install, either add to your pubspec.yaml ```yml dependencies: - parse_server_sdk: ^1.0.19 + parse_server_sdk: ^1.0.20 ``` 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. diff --git a/pubspec.yaml b/pubspec.yaml index a6346a0b0..2d8af20f5 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.19 +version: 1.0.20 homepage: https://github.com/phillwiggins/flutter_parse_sdk author: PhillWiggins