From dd90d3ccb1454054e592c75b19d43e55a57fa2ce Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 24 Mar 2019 19:31:58 +0000 Subject: [PATCH 1/4] 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 a622c9591d01ba656ef8ec6b0a852b8686446503 Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Tue, 23 Apr 2019 02:35:27 -0300 Subject: [PATCH 2/4] Bugfix LiveQuery (#162) Bugfix LiveQuery --- lib/src/network/parse_live_query.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()}'); From 425a37e4920d99c32afb0c0e24a5a5003b7fa081 Mon Sep 17 00:00:00 2001 From: Phill Date: Thu, 25 Apr 2019 17:34:41 +0100 Subject: [PATCH 3/4] User logout fixed --- example/lib/ui/main.dart | 26 ++++++++++++++------------ lib/src/objects/parse_user.dart | 3 ++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/example/lib/ui/main.dart b/example/lib/ui/main.dart index 0e127bd16..d1bbf98aa 100644 --- a/example/lib/ui/main.dart +++ b/example/lib/ui/main.dart @@ -64,21 +64,21 @@ class _MyAppState extends State { Future runTestQueries() async { // Basic repository example - await repositoryAddUser(); - await repositoryAddItems(); - await repositoryGetAllItems(); + //await repositoryAddUser(); + //await repositoryAddItems(); + //await repositoryGetAllItems(); //Basic usage - createItem(); - getAllItems(); - getAllItemsByName(); - getSingleItem(); - getConfigs(); - query(); + //createItem(); + //getAllItems(); + //getAllItemsByName(); + //getSingleItem(); + //getConfigs(); + //query(); initUser(); - function(); - functionWithParameters(); - test(); + //function(); + //functionWithParameters(); + // test(); } Future test() async { @@ -225,6 +225,8 @@ class _MyAppState extends State { user = response.result; } + user = await ParseUser.currentUser(); + user = ParseUser('TestFlutter', 'TestPassword123', 'phill.wiggins@gmail.com'); response = await user.login(); diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index c4cb76f4d..825bd8fd2 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -363,7 +363,8 @@ class ParseUser extends ParseObject implements ParseCloneable { type == ParseApiRQ.getAll || type == ParseApiRQ.destroy || type == ParseApiRQ.requestPasswordReset || - type == ParseApiRQ.verificationEmailRequest) { + type == ParseApiRQ.verificationEmailRequest || + type == ParseApiRQ.logout) { return parseResponse; } else { final ParseUser user = parseResponse.result; From 225f78b39cb9bf260f1ee1236a3de71552613856 Mon Sep 17 00:00:00 2001 From: Phill Date: Thu, 25 Apr 2019 17:37:56 +0100 Subject: [PATCH 4/4] v21 release --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/src/base/parse_constants.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f112b713..37975d799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.21 +LiveQuery fix +Logout fix + ## 1.0.20 ACL now working emailVerified diff --git a/README.md b/README.md index 2f080a3e1..97d209623 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.20 + parse_server_sdk: ^1.0.21 ``` 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/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index 0907c1353..2bf12e226 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.19'; +const String keySdkVersion = '1.0.21'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points diff --git a/pubspec.yaml b/pubspec.yaml index 2d8af20f5..178d36fe9 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.20 +version: 1.0.21 homepage: https://github.com/phillwiggins/flutter_parse_sdk author: PhillWiggins