From dd90d3ccb1454054e592c75b19d43e55a57fa2ce Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 24 Mar 2019 19:31:58 +0000 Subject: [PATCH 01/25] 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 f36885f6014932f9da2a65fe308eec4ff387272f Mon Sep 17 00:00:00 2001 From: Phill Date: Mon, 25 Mar 2019 18:59:48 +0000 Subject: [PATCH 02/25] Corrected repo example --- example/lib/domain/utils/db_utils.dart | 11 +++++ example/lib/ui/main.dart | 59 +++++++++++++++++--------- lib/src/objects/parse_response.dart | 4 +- 3 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 example/lib/domain/utils/db_utils.dart diff --git a/example/lib/domain/utils/db_utils.dart b/example/lib/domain/utils/db_utils.dart new file mode 100644 index 000000000..051c7ee3b --- /dev/null +++ b/example/lib/domain/utils/db_utils.dart @@ -0,0 +1,11 @@ +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; + +Future getDB() async { + final String dbDirectory = (await getApplicationDocumentsDirectory()).path; + final String dbPath = join(dbDirectory, 'no_sql'); + final DatabaseFactory dbFactory = databaseFactoryIo; + return await dbFactory.openDatabase(dbPath); +} diff --git a/example/lib/ui/main.dart b/example/lib/ui/main.dart index 1a578609c..ee1116ff8 100644 --- a/example/lib/ui/main.dart +++ b/example/lib/ui/main.dart @@ -5,6 +5,7 @@ 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_plugin_example/domain/utils/db_utils.dart'; import 'package:flutter_stetho/flutter_stetho.dart'; import 'package:parse_server_sdk/parse_server_sdk.dart'; @@ -19,10 +20,12 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + DietPlanRepository repository; + @override void initState() { super.initState(); - initParse(); + WidgetsBinding.instance.addPostFrameCallback((_) => initData()); } @override @@ -35,12 +38,14 @@ class _MyAppState extends State { body: Center( child: const Text('Running Parse init'), ), - floatingActionButton: FloatingActionButton(onPressed: runTestQueries), ), ); } - Future initParse() async { + Future initData() async { + // Initialize repository + await initRepository(); + // Initialize parse Parse().initialize(keyParseApplicationId, keyParseServerUrl, masterKey: keyParseMasterKey, debug: true); @@ -56,19 +61,19 @@ class _MyAppState extends State { Future runTestQueries() async { // Basic repository example - await repositoryAddItems(); - await repositoryGetAllItems(); + //await repositoryAddItems(); + //await repositoryGetAllItems(); // Basic usage - /*createItem(); - getAllItems(); - getAllItemsByName(); + //createItem(); + //getAllItems(); + //getAllItemsByName(); getSingleItem(); - getConfigs(); - query(); - initUser(); - function(); - functionWithParameters();*/ + //getConfigs(); + //query(); + //initUser(); + //function(); + //functionWithParameters(); } Future createItem() async { @@ -98,6 +103,8 @@ class _MyAppState extends State { final ParseResponse apiResponse = await DietPlan().getAll(); if (apiResponse.success && apiResponse.result != null) { + String json = JsonEncoder().convert(apiResponse.result); + print(json); for (final DietPlan plan in apiResponse.result) { print(keyAppName + ': ' + plan.name); } @@ -107,7 +114,7 @@ class _MyAppState extends State { } Future getSingleItem() async { - final ParseResponse apiResponse = await DietPlan().getObject('R5EonpUDWy'); + final ParseResponse apiResponse = await DietPlan().getObject('B0xtU0Ekqi'); if (apiResponse.success && apiResponse.result != null) { final DietPlan dietPlan = apiResponse.result; @@ -267,10 +274,15 @@ class _MyAppState extends State { } Future repositoryAddItems() async { - final List dietPlans = - const JsonDecoder().convert(dietPlansToAdd); + final List dietPlans = List(); + + final List json = const JsonDecoder().convert(dietPlansToAdd); + for (final Map element in json) { + final DietPlan dietPlan = DietPlan().fromJson(element); + dietPlans.add(dietPlan); + } - final DietPlanRepository repository = DietPlanRepository(); + await initRepository(); final ApiResponse response = await repository.addAll(dietPlans); if (response.success) { print(response.result); @@ -278,13 +290,22 @@ class _MyAppState extends State { } Future repositoryGetAllItems() async { - final DietPlanRepository repository = DietPlanRepository(); final ApiResponse response = await repository.getAll(); if (response.success) { print(response.result); } } + + Future initRepository() async { + repository ??= DietPlanRepository.init(await getDB()); + } } 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}]'; + '[{"className":"Diet_Plans","Name":"Textbook","Description":"For an active lifestyle and a straight forward macro plan, we suggest this plan.","Fat":25,"Carbs":50,"Protein":25,"Status":0},' + '{"className":"Diet_Plans","Name":"Body Builder","Description":"Default Body Builders Diet","Fat":20,"Carbs":40,"Protein":40,"Status":0},' + '{"className":"Diet_Plans","Name":"Zone Diet","Description":"Popular with CrossFit users. Zone Diet targets similar macros.","Fat":30,"Carbs":40,"Protein":30,"Status":0},' + '{"className":"Diet_Plans","Name":"Low Fat","Description":"Low fat diet.","Fat":15,"Carbs":60,"Protein":25,"Status":0},' + '{"className":"Diet_Plans","Name":"Low Carb","Description":"Low Carb diet, main focus on quality fats and protein.","Fat":35,"Carbs":25,"Protein":40,"Status":0},' + '{"className":"Diet_Plans","Name":"Paleo","Description":"Paleo diet.","Fat":60,"Carbs":25,"Protein":10,"Status":0},' + '{"className":"Diet_Plans","Name":"Ketogenic","Description":"High quality fats, low carbs.","Fat":65,"Carbs":5,"Protein":30,"Status":0}]'; diff --git a/lib/src/objects/parse_response.dart b/lib/src/objects/parse_response.dart index b736ee51e..e566afe5d 100644 --- a/lib/src/objects/parse_response.dart +++ b/lib/src/objects/parse_response.dart @@ -1,8 +1,8 @@ part of flutter_parse_sdk; -class ParseResponse { +class ParseResponse { bool success = false; int statusCode = -1; dynamic result; ParseError error; -} \ No newline at end of file +} From 2be1e1b5f130ea21c260e1da181479573ae3e681 Mon Sep 17 00:00:00 2001 From: Phill Date: Tue, 26 Mar 2019 20:46:20 +0000 Subject: [PATCH 03/25] ParseResponse accepts list results and count --- example/lib/ui/main.dart | 40 +++++++++---------- lib/src/objects/parse_response.dart | 9 ++++- .../response/parse_response_builder.dart | 16 +++++--- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/example/lib/ui/main.dart b/example/lib/ui/main.dart index ee1116ff8..4155e8eae 100644 --- a/example/lib/ui/main.dart +++ b/example/lib/ui/main.dart @@ -61,19 +61,19 @@ class _MyAppState extends State { Future runTestQueries() async { // Basic repository example - //await repositoryAddItems(); - //await repositoryGetAllItems(); + await repositoryAddItems(); + await repositoryGetAllItems(); - // Basic usage - //createItem(); - //getAllItems(); - //getAllItemsByName(); + //Basic usage + createItem(); + getAllItems(); + getAllItemsByName(); getSingleItem(); - //getConfigs(); - //query(); - //initUser(); - //function(); - //functionWithParameters(); + getConfigs(); + query(); + initUser(); + function(); + functionWithParameters(); } Future createItem() async { @@ -83,7 +83,7 @@ class _MyAppState extends State { final ParseResponse apiResponse = await newObject.create(); - if (apiResponse.success && apiResponse.result != null) { + if (apiResponse.success && apiResponse.count > 0) { print(keyAppName + ': ' + apiResponse.result.toString()); } } @@ -92,8 +92,8 @@ class _MyAppState extends State { final ParseResponse apiResponse = await ParseObject('TestObjectForApi').getAll(); - if (apiResponse.success && apiResponse.result != null) { - for (final ParseObject testObject in apiResponse.result) { + if (apiResponse.success && apiResponse.count > 0) { + for (final ParseObject testObject in apiResponse.results) { print(keyAppName + ': ' + testObject.toString()); } } @@ -102,10 +102,8 @@ class _MyAppState extends State { Future getAllItems() async { final ParseResponse apiResponse = await DietPlan().getAll(); - if (apiResponse.success && apiResponse.result != null) { - String json = JsonEncoder().convert(apiResponse.result); - print(json); - for (final DietPlan plan in apiResponse.result) { + if (apiResponse.success && apiResponse.count > 0) { + for (final DietPlan plan in apiResponse.results) { print(keyAppName + ': ' + plan.name); } } else { @@ -116,7 +114,7 @@ class _MyAppState extends State { Future getSingleItem() async { final ParseResponse apiResponse = await DietPlan().getObject('B0xtU0Ekqi'); - if (apiResponse.success && apiResponse.result != null) { + if (apiResponse.success && apiResponse.count > 0) { final DietPlan dietPlan = apiResponse.result; // Shows example of storing values in their proper type and retrieving them @@ -148,7 +146,7 @@ class _MyAppState extends State { final ParseResponse apiResponse = await queryBuilder.query(); - if (apiResponse.success && apiResponse.result != null) { + if (apiResponse.success && apiResponse.count > 0) { final List listFromApi = apiResponse.result; final ParseObject parseObject = listFromApi?.first; print('Result: ${parseObject.toString()}'); @@ -231,7 +229,7 @@ class _MyAppState extends State { ..whereStartsWith(ParseUser.keyUsername, 'phillw'); final ParseResponse apiResponse = await queryBuilder.query(); - if (apiResponse.success) { + if (apiResponse.success && apiResponse.count > 0) { final List users = response.result; for (final ParseUser user in users) { print(keyAppName + ': ' + user.toString()); diff --git a/lib/src/objects/parse_response.dart b/lib/src/objects/parse_response.dart index e566afe5d..62f673a7a 100644 --- a/lib/src/objects/parse_response.dart +++ b/lib/src/objects/parse_response.dart @@ -1,8 +1,15 @@ part of flutter_parse_sdk; -class ParseResponse { +class ParseResponse { bool success = false; int statusCode = -1; + + /// If result is a singular result, i.e. getByObjectID dynamic result; + + /// All results stored as a list - Even if only one response is returned + // ignore: always_specify_types + List results; + int count = 0; ParseError error; } diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index 714a86e51..5fcc6dd42 100644 --- a/lib/src/objects/response/parse_response_builder.dart +++ b/lib/src/objects/response/parse_response_builder.dart @@ -8,9 +8,7 @@ part of flutter_parse_sdk; /// 3. Success with simple OK. /// 4. Success with results. Again [ParseResponse()] is returned class _ParseResponseBuilder { - ParseResponse handleResponse( - dynamic object, - Response apiResponse, + ParseResponse handleResponse(dynamic object, Response apiResponse, {bool returnAsResult = false}) { final ParseResponse parseResponse = ParseResponse(); @@ -69,9 +67,14 @@ class _ParseResponseBuilder { response.result = map; } else if (map != null && map.length == 1 && map.containsKey('results')) { final List results = map['results']; - response.result = _handleMultipleResults(object, results); + final List items = _handleMultipleResults(object, results); + response.results = items; + response.count = items.length; } else { - response.result = _handleSingleResult(object, map, false); + final T item = _handleSingleResult(object, map, false); + response.count = 1; + response.result = item; + response.results = [item]; } return response; @@ -89,7 +92,8 @@ class _ParseResponseBuilder { } /// Handles a response with a single result object - T _handleSingleResult(T object, Map map, bool createNewObject) { + T _handleSingleResult( + T object, Map map, bool createNewObject) { if (createNewObject && object is ParseCloneable) { return object.clone(map); } else if (object is ParseObject) { From 2d6bb3a4608c8e187dbbdbf4ea1804aacf822811 Mon Sep 17 00:00:00 2001 From: Phill Wiggins Date: Thu, 28 Mar 2019 14:22:40 +0000 Subject: [PATCH 04/25] Private/test (#140) * ParseResponse accepts list results and count * Corrected toPointer logic * Corrected date issue * Code clean * Code clean --- example/lib/data/model/day.dart | 39 ++++++++++++++++ example/lib/data/model/user.dart | 42 +++++++++++++++++ example/lib/ui/main.dart | 23 ++++++++++ lib/parse_server_sdk.dart | 7 ++- lib/src/network/parse_http_client.dart | 25 +---------- lib/src/network/parse_query.dart | 1 + lib/src/objects/parse_base.dart | 6 +-- lib/src/objects/parse_object.dart | 45 ++++++++++--------- lib/src/objects/parse_user.dart | 20 +++------ .../response/parse_exception_response.dart | 3 +- .../response/parse_response_utils.dart | 20 +++++++-- lib/src/utils/parse_encoder.dart | 20 +++++++-- lib/src/utils/parse_logger.dart | 44 ++++++++++++++---- lib/src/utils/parse_utils.dart | 16 ++++++- 14 files changed, 228 insertions(+), 83 deletions(-) create mode 100644 example/lib/data/model/day.dart create mode 100644 example/lib/data/model/user.dart diff --git a/example/lib/data/model/day.dart b/example/lib/data/model/day.dart new file mode 100644 index 000000000..79ed02098 --- /dev/null +++ b/example/lib/data/model/day.dart @@ -0,0 +1,39 @@ +import 'dart:core'; + +import 'package:flutter_plugin_example/data/model/user.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +class Day extends ParseObject implements ParseCloneable { + Day() : super(_keyTableName); + + Day.clone() : this(); + + @override + Day clone(Map map) => Day.clone()..fromJson(map); + + @override + Day fromJson(Map objectData) { + super.fromJson(objectData); + if (objectData.containsKey(keyOwner)) { + owner = User.clone().fromJson(objectData[keyOwner]); + } + return this; + } + + static const String _keyTableName = 'FoodDiary_Day'; + static const String keyDate = 'Date'; + static const String keyOwner = 'Owner'; + static const String keyStatus = 'Status'; + + DateTime get date => get(keyDate); + + set date(DateTime date) => set(keyDate, date); + + User get owner => get(keyOwner); + + set owner(User owner) => set(keyOwner, owner); + + int get status => get(keyStatus); + + set status(int status) => set(keyStatus, status); +} diff --git a/example/lib/data/model/user.dart b/example/lib/data/model/user.dart new file mode 100644 index 000000000..9084debe8 --- /dev/null +++ b/example/lib/data/model/user.dart @@ -0,0 +1,42 @@ +import 'dart:core'; + +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +class User extends ParseUser implements ParseCloneable { + User(String username, String password, String emailAddress) + : super(username, password, emailAddress); + + User.clone() : this(null, null, null); + + @override + User clone(Map map) => User.clone()..fromJson(map); + + static const String keyDob = 'DOB'; + static const String keyGender = 'Gender'; + static const String keyHeight = 'Height'; + static const String keyFirebaseID = 'FirebaseID'; + static const String keyName = 'Name'; + static const String keyDisplayPicture = 'DisplayPicture'; + static const String keyProUser = 'ProUser'; + + DateTime get dob => get(keyDob); + set dob(DateTime dob) => set(keyDob, dob); + + num get gender => get(keyGender); + set gender(num gender) => set(keyGender, gender); + + num get height => get(keyHeight); + set height(num height) => set(keyHeight, height); + + String get firebaseId => get(keyHeight); + set firebaseId(String firebaseId) => set(keyHeight, firebaseId); + + String get name => get(keyName); + set name(String name) => set(keyName, name); + + String get displayPicture => get(keyDisplayPicture); + set displayPicture(String displayPicture) => set(keyDisplayPicture, displayPicture); + + bool get proUser => get(keyProUser); + set proUser(bool proUser) => set(keyProUser, proUser); +} diff --git a/example/lib/ui/main.dart b/example/lib/ui/main.dart index 4155e8eae..e4d6b9751 100644 --- a/example/lib/ui/main.dart +++ b/example/lib/ui/main.dart @@ -2,7 +2,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_plugin_example/data/base/api_response.dart'; +import 'package:flutter_plugin_example/data/model/day.dart'; import 'package:flutter_plugin_example/data/model/diet_plan.dart'; +import 'package:flutter_plugin_example/data/model/user.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_plugin_example/domain/utils/db_utils.dart'; @@ -74,6 +76,27 @@ class _MyAppState extends State { initUser(); function(); functionWithParameters(); + test(); + } + + Future test() async { + User user = User('test_user', 'test_password', 'test@gmail.com'); + final ParseResponse signUpResponse = await user.signUp(); + + if (signUpResponse.success) { + user = signUpResponse.result; + } else { + final ParseResponse loginResponse = await user.login(); + + if (loginResponse.success) { + user = loginResponse.result; + } + } + + final QueryBuilder query = QueryBuilder(Day()) + ..whereEqualTo(Day.keyOwner, user); + var item = await query.query(); + print(item.toString()); } Future createItem() async { diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index b0fcda35c..0ec5bcf6e 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -99,7 +99,12 @@ class Parse { String sessionId, bool autoSendSessionId, SecurityContext securityContext}) { - ParseCoreData.init(appId, serverUrl, + + final String url = removeTrailingSlash(serverUrl); + + ParseCoreData.init( + appId, + url, debug: debug, appName: appName, liveQueryUrl: liveQueryUrl, diff --git a/lib/src/network/parse_http_client.dart b/lib/src/network/parse_http_client.dart index 4604f58e6..07c07e5c4 100644 --- a/lib/src/network/parse_http_client.dart +++ b/lib/src/network/parse_http_client.dart @@ -36,32 +36,9 @@ class ParseHTTPClient extends BaseClient { } if (data.debug) { - _logging(request); + logCUrl(request); } return _client.send(request); } - - void _logging(BaseRequest request) { - String curlCmd = 'curl'; - curlCmd += ' -X ' + request.method; - bool compressed = false; - request.headers.forEach((String name, String value) { - if (name?.toLowerCase() == 'accept-encoding' && - value?.toLowerCase() == 'gzip') { - compressed = true; - } - curlCmd += ' -H \'$name: $value\''; - }); - if (request.method == 'POST' || request.method == 'PUT') { - if (request is Request) { - final String body = latin1.decode(request.bodyBytes); - curlCmd += ' -d \'$body\''; - } - } - curlCmd += (compressed ? ' --compressed ' : ' ') + request.url.toString(); - print('╭-- cURL'); - print(curlCmd); - print('╰-- (copy and paste the above line to a terminal)'); - } } diff --git a/lib/src/network/parse_query.dart b/lib/src/network/parse_query.dart index 877c4ceeb..a6514f90a 100644 --- a/lib/src/network/parse_query.dart +++ b/lib/src/network/parse_query.dart @@ -284,6 +284,7 @@ class QueryBuilder { /// that the column and value are being queried against MapEntry _buildQueryWithColumnValueAndOperator( MapEntry columnAndValue, String queryOperator) { + final String key = columnAndValue.key; final dynamic value = convertValueToCorrectType(parseEncode(columnAndValue.value)); diff --git a/lib/src/objects/parse_base.dart b/lib/src/objects/parse_base.dart index 55d116aae..bd703f27f 100644 --- a/lib/src/objects/parse_base.dart +++ b/lib/src/objects/parse_base.dart @@ -206,9 +206,5 @@ abstract class ParseBase { return null; } - Map toPointer() => { - '__type': 'Pointer', - keyVarClassName: className, - keyVarObjectId: objectId - }; + Map toPointer() => encodeObject(className, objectId); } diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index d2949c890..9a60f5b45 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -33,7 +33,7 @@ class ParseObject extends ParseBase implements ParseCloneable { /// Gets an object from the server using it's [String] objectId Future getObject(String objectId) async { try { - String uri =_path; + String uri = _path; if (objectId != null) { uri += '/$objectId'; @@ -42,7 +42,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$uri'); final Response result = await _client.get(url); - return handleResponse(this, result, ParseApiRQ.get, _debug, className); + return handleResponse( + this, result, ParseApiRQ.get, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.get, _debug, className); } @@ -53,7 +54,8 @@ class ParseObject extends ParseBase implements ParseCloneable { try { final Uri url = getSanitisedUri(_client, '$_path'); final Response result = await _client.get(url); - return handleResponse(this, result, ParseApiRQ.getAll, _debug, className); + return handleResponse( + this, result, ParseApiRQ.getAll, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.getAll, _debug, className); } @@ -73,7 +75,8 @@ class ParseObject extends ParseBase implements ParseCloneable { objectId = map['objectId'].toString(); } - return handleResponse(this, result, ParseApiRQ.create, _debug, className); + return handleResponse( + this, result, ParseApiRQ.create, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.create, _debug, className); } @@ -88,7 +91,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); final String body = json.encode(toJson(forApiRQ: true)); final Response result = await _client.put(url, body: body); - return handleResponse(this, result, ParseApiRQ.save, _debug, className); + return handleResponse( + this, result, ParseApiRQ.save, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.save, _debug, className); } @@ -179,7 +183,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final String body = '{\"$key\":{\"__op\":\"$arrayAction\",\"objects\":${json.encode(parseEncode(values))}}}'; final Response result = await _client.put(url, body: body); - return handleResponse(this, result, apiRQType, _debug, className); + return handleResponse( + this, result, apiRQType, _debug, className); } else { return null; } @@ -190,7 +195,8 @@ class ParseObject extends ParseBase implements ParseCloneable { /// Used in array Operations in save() method void _arrayOperation(String arrayAction, String key, List values) { - set>(key, {'__op': arrayAction, 'objects': values}); + set>( + key, {'__op': arrayAction, 'objects': values}); } /// Increases a num of an object by x amount @@ -206,7 +212,7 @@ class ParseObject extends ParseBase implements ParseCloneable { /// Increases a num of an object by x amount void setIncrement(String key, num amount) { set>( - key, {'__op': 'Increment', 'amount': amount}); + key, {'__op': 'Increment', 'amount': amount}); } /// Decreases a num of an object by x amount @@ -222,7 +228,7 @@ class ParseObject extends ParseBase implements ParseCloneable { /// Decreases a num of an object by x amount void setDecrement(String key, num amount) { set>( - key, {'__op': 'Increment', 'amount': -amount}); + key, {'__op': 'Increment', 'amount': -amount}); } /// Can be used to add arrays to a given type @@ -231,9 +237,11 @@ class ParseObject extends ParseBase implements ParseCloneable { try { if (objectId != null) { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); - final String body = '{\"$key\":{\"__op\":\"$countAction\",\"amount\":$amount}}'; + final String body = + '{\"$key\":{\"__op\":\"$countAction\",\"amount\":$amount}}'; final Response result = await _client.put(url, body: body); - return handleResponse(this, result, apiRQType, _debug, className); + return handleResponse( + this, result, apiRQType, _debug, className); } else { return null; } @@ -245,16 +253,10 @@ class ParseObject extends ParseBase implements ParseCloneable { /// Can be used to create custom queries Future query(String query) async { try { - final Uri tempUri = Uri.parse(ParseCoreData().serverUrl); - - final Uri url = Uri( - scheme: tempUri.scheme, - host: tempUri.host, - port: tempUri.port, - path: '${tempUri.path}$_path', - query: query); + final Uri url = getSanitisedUri(_client, '$_path', query: query); final Response result = await _client.get(url); - return handleResponse(this, result, ParseApiRQ.query, _debug, className); + return handleResponse( + this, result, ParseApiRQ.query, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.query, _debug, className); } @@ -267,7 +269,8 @@ class ParseObject extends ParseBase implements ParseCloneable { objectId ??= objectId; final Uri url = getSanitisedUri(_client, '$_path/$objectId'); final Response result = await _client.delete(url); - return handleResponse(this, result, ParseApiRQ.delete, _debug, className); + return handleResponse( + this, result, ParseApiRQ.delete, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.delete, _debug, className); } diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index a5bf6e68f..c05eb4f3f 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -145,25 +145,19 @@ class ParseUser extends ParseObject implements ParseCloneable { /// provided, call this method to login. Future login() async { try { - final Uri tempUri = Uri.parse(_client.data.serverUrl); - - final Uri url = Uri( - scheme: tempUri.scheme, - host: tempUri.host, - port: tempUri.port, - path: '${tempUri.path}$keyEndPointLogin', - queryParameters: { - keyVarUsername: username, - keyVarPassword: password - }); + final Map queryParams = { + keyVarUsername: username, + keyVarPassword: password + }; + + final Uri url = getSanitisedUri(_client, '$keyEndPointLogin', queryParams: queryParams); final Response response = await _client.get(url, headers: { keyHeaderRevocableSession: '1', }); - return _handleResponse( - this, response, ParseApiRQ.login, _debug, className); + return _handleResponse(this, response, ParseApiRQ.login, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.login, _debug, className); } diff --git a/lib/src/objects/response/parse_exception_response.dart b/lib/src/objects/response/parse_exception_response.dart index 9a798c8b0..e8b3f4367 100644 --- a/lib/src/objects/response/parse_exception_response.dart +++ b/lib/src/objects/response/parse_exception_response.dart @@ -3,7 +3,6 @@ part of flutter_parse_sdk; /// Handles exception instead of throwing an exception ParseResponse buildParseResponseWithException(Exception exception) { final ParseResponse response = ParseResponse(); - response.error = - ParseError(message: exception.toString(), isTypeOfException: true); + response.error = ParseError(message: exception.toString(), isTypeOfException: true); return response; } diff --git a/lib/src/objects/response/parse_response_utils.dart b/lib/src/objects/response/parse_response_utils.dart index 17fc181b2..0fff98d21 100644 --- a/lib/src/objects/response/parse_response_utils.dart +++ b/lib/src/objects/response/parse_response_utils.dart @@ -4,12 +4,14 @@ part of flutter_parse_sdk; @protected ParseResponse handleResponse(ParseCloneable object, Response response, ParseApiRQ type, bool debug, String className) { + final ParseResponse parseResponse = _ParseResponseBuilder().handleResponse( - object, response, + object, + response, returnAsResult: shouldReturnAsABaseResult(type)); if (debug) { - logger(ParseCoreData().appName, className, type.toString(), parseResponse); + logAPIResponse(className, type.toString(), parseResponse); } return parseResponse; @@ -19,11 +21,12 @@ ParseResponse handleResponse(ParseCloneable object, Response response, @protected ParseResponse handleException( Exception exception, ParseApiRQ type, bool debug, String className) { + final ParseResponse parseResponse = buildParseResponseWithException(exception); if (debug) { - logger(ParseCoreData().appName, className, type.toString(), parseResponse); + logAPIResponse(className, type.toString(), parseResponse); } return parseResponse; @@ -49,5 +52,14 @@ bool shouldReturnAsABaseResult(ParseApiRQ type) { bool isUnsuccessfulResponse(Response apiResponse) => apiResponse.statusCode != 200 && apiResponse.statusCode != 201; -bool isSuccessButNoResults(Response apiResponse) => apiResponse.body == '{\"results\":[]}'; +bool isSuccessButNoResults(Response apiResponse) { + final Map decodedResponse = jsonDecode(apiResponse.body); + final List results = decodedResponse['results']; + + if (results == null) { + return false; + } + + return results.isEmpty; +} diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index 9b769c569..b7d31fc95 100644 --- a/lib/src/utils/parse_encoder.dart +++ b/lib/src/utils/parse_encoder.dart @@ -23,7 +23,7 @@ dynamic parseEncode(dynamic value, {bool full}) { if (value is ParseGeoPoint) { return value; } - + if (value is ParseFile) { return value; } @@ -40,9 +40,23 @@ dynamic parseEncode(dynamic value, {bool full}) { } Map _encodeUint8List(Uint8List value) { - return {'__type': 'Bytes', 'base64': base64.encode(value)}; + return { + '__type': 'Bytes', + 'base64': base64.encode(value) + }; } Map _encodeDate(DateTime date) { - return {'__type': 'Date', 'iso': _parseDateFormat.format(date)}; + return { + '__type': 'Date', + 'iso': _parseDateFormat.format(date) + }; +} + +Map encodeObject(String className, String objectId) { + return { + '\"__type\"': '\"Pointer\"', + '\"$keyVarClassName\"': '\"$className\"', + '\"$keyVarObjectId\"': '\"$objectId\"' + }; } diff --git a/lib/src/utils/parse_logger.dart b/lib/src/utils/parse_logger.dart index 8d62d0ea6..047d3d37d 100644 --- a/lib/src/utils/parse_logger.dart +++ b/lib/src/utils/parse_logger.dart @@ -1,14 +1,16 @@ part of flutter_parse_sdk; -void logger(String appName, String className, String type, +void logAPIResponse( + String className, + String type, ParseResponse parseResponse) { - String responseString = ' \n'; - String name = appName; - if (name.isNotEmpty) { - name = '$appName '; - } - responseString += '----\n${name}API Response ($className : $type) :'; + const String spacer = ' \n'; + String responseString = ''; + + responseString += '╭-- Parse Response'; + responseString += '\nClass: $className'; + responseString += '\nFunction: $type'; if (parseResponse.success) { responseString += '\nStatus Code: ${parseResponse.statusCode}'; @@ -27,10 +29,36 @@ void logger(String appName, String className, String type, responseString += '\n$errorOrException: ${parseResponse.error.message}'; } - responseString += '\n----\n'; + responseString += '\n╰-- \n'; + responseString += spacer; print(responseString); } +void logCUrl(BaseRequest request) { + String curlCmd = 'curl'; + curlCmd += ' -X ' + request.method; + bool compressed = false; + request.headers.forEach((String name, String value) { + if (name?.toLowerCase() == 'accept-encoding' && + value?.toLowerCase() == 'gzip') { + compressed = true; + } + curlCmd += ' -H \'$name: $value\''; + }); + if (request.method == 'POST' || request.method == 'PUT') { + if (request is Request) { + final String body = latin1.decode(request.bodyBytes); + curlCmd += ' -d \'$body\''; + } + } + + curlCmd += (compressed ? ' --compressed ' : ' ') + request.url.toString(); + curlCmd += '\n\n ${Uri.decodeFull(request.url.toString())}'; + print('╭-- Parse Request'); + print(curlCmd); + print('╰--'); +} + void logRequest( String appName, String className, String type, String uri, String body) { String requestString = ' \n'; diff --git a/lib/src/utils/parse_utils.dart b/lib/src/utils/parse_utils.dart index b1a7fd6f7..2cc15f71a 100644 --- a/lib/src/utils/parse_utils.dart +++ b/lib/src/utils/parse_utils.dart @@ -24,14 +24,26 @@ dynamic convertValueToCorrectType(dynamic value) { } /// Sanitises a url -Uri getSanitisedUri(ParseHTTPClient client, String pathToAppend) { +Uri getSanitisedUri(ParseHTTPClient client, String pathToAppend, + {Map queryParams, String query}) { + final Uri tempUri = Uri.parse(client.data.serverUrl); final Uri url = Uri( scheme: tempUri.scheme, host: tempUri.host, port: tempUri.port, - path: '${tempUri.path}$pathToAppend'); + path: '${tempUri.path}$pathToAppend', + queryParameters: queryParams, + query: query); return url; } +/// Removes unncessary / +String removeTrailingSlash(String serverUrl) { + if (serverUrl.substring(serverUrl.length -1) == '/') { + return serverUrl.substring(0, serverUrl.length -1); + } else { + return serverUrl; + } +} From 0513df2e6a3481d753b0faa8776c711e212c257a Mon Sep 17 00:00:00 2001 From: Phill Wiggins Date: Sat, 30 Mar 2019 07:37:08 +0000 Subject: [PATCH 05/25] Private/test (#141) * ParseResponse accepts list results and count * Corrected toPointer logic * Corrected date issue * Code clean * Code clean * Code clean --- lib/src/network/parse_query.dart | 3 +-- lib/src/utils/parse_encoder.dart | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/network/parse_query.dart b/lib/src/network/parse_query.dart index a6514f90a..d06536b07 100644 --- a/lib/src/network/parse_query.dart +++ b/lib/src/network/parse_query.dart @@ -289,8 +289,7 @@ class QueryBuilder { final dynamic value = convertValueToCorrectType(parseEncode(columnAndValue.value)); if (queryOperator == _NO_OPERATOR_NEEDED) { - return MapEntry( - _NO_OPERATOR_NEEDED, "\"${key}\": $value"); + return MapEntry(_NO_OPERATOR_NEEDED, '\"$key\": ${jsonEncode(value)}'); } else { String queryString = '\"$key\":'; final Map queryOperatorAndValueMap = Map(); diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index b7d31fc95..c64d439a4 100644 --- a/lib/src/utils/parse_encoder.dart +++ b/lib/src/utils/parse_encoder.dart @@ -55,8 +55,8 @@ Map _encodeDate(DateTime date) { Map encodeObject(String className, String objectId) { return { - '\"__type\"': '\"Pointer\"', - '\"$keyVarClassName\"': '\"$className\"', - '\"$keyVarObjectId\"': '\"$objectId\"' + '__type': 'Pointer', + keyVarClassName: className, + keyVarObjectId: objectId }; } From 45f4f5396a56f10a5e79b4b1081f75e0e49458c8 Mon Sep 17 00:00:00 2001 From: Phill Date: Sat, 30 Mar 2019 10:30:06 +0000 Subject: [PATCH 06/25] Fixed delete --- lib/src/objects/parse_object.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 9a60f5b45..80f3ac00f 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -263,11 +263,11 @@ class ParseObject extends ParseBase implements ParseCloneable { } /// Deletes the current object locally and online - Future delete({String objectId, String path}) async { + Future delete({String id, String path}) async { try { path ??= _path; - objectId ??= objectId; - final Uri url = getSanitisedUri(_client, '$_path/$objectId'); + id ??= objectId; + final Uri url = getSanitisedUri(_client, '$_path/$id'); final Response result = await _client.delete(url); return handleResponse( this, result, ParseApiRQ.delete, _debug, className); From 5fd3b13debb357875b91ce4df3b3a8f47f5003de Mon Sep 17 00:00:00 2001 From: wigginsp Date: Mon, 1 Apr 2019 08:37:05 +0100 Subject: [PATCH 07/25] Fixed ParseUser setting ParseObject extensions --- lib/src/objects/parse_user.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index a5bf6e68f..2f093e77d 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -281,16 +281,7 @@ class ParseUser extends ParseObject implements ParseCloneable { if (objectId == null) { return signUp(); } else { - try { - final Uri url = getSanitisedUri(_client, '$_path/$objectId'); - final String body = - json.encode(toJson(forApiRQ: true), toEncodable: dateTimeEncoder); - final Response response = await _client.put(url, body: body); - return _handleResponse( - this, response, ParseApiRQ.save, _debug, className); - } on Exception catch (e) { - return handleException(e, ParseApiRQ.save, _debug, className); - } + return super.save(); } } From c2d34a0a6fa311a64a308f4546f1bf12fc1e17bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bj=C3=B8rnbakk?= Date: Mon, 1 Apr 2019 11:24:56 +0200 Subject: [PATCH 08/25] Fix lost lat, lng data (#143) Update so SubClass of ParseObject uses internal Map for value storage. Changes in v1.0.17 breaks ParseGeoPoint. This is tested and seems to fix the issue. --- lib/src/objects/parse_geo_point.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/src/objects/parse_geo_point.dart b/lib/src/objects/parse_geo_point.dart index 3ffff43e6..f6c16168b 100644 --- a/lib/src/objects/parse_geo_point.dart +++ b/lib/src/objects/parse_geo_point.dart @@ -1,5 +1,8 @@ part of flutter_parse_sdk; +const String keyLatitude = 'latitude'; +const String keyLongitude = 'longitude'; + class ParseGeoPoint extends ParseObject { /// Creates a Parse Object of type GeoPoint @@ -11,8 +14,8 @@ class ParseGeoPoint extends ParseObject { bool autoSendSessionId}) : super(keyGeoPoint) { - latitude = latitude; - longitude = longitude; + this.latitude = latitude; + this.longitude = longitude; _debug = isDebugEnabled(objectLevelDebug: debug); _client = client ?? @@ -22,8 +25,11 @@ class ParseGeoPoint extends ParseObject { securityContext: ParseCoreData().securityContext); } - double latitude; - double longitude; + double get latitude => super.get(keyLatitude); + set latitude(double latitude) => set(keyLatitude, latitude); + + double get longitude => super.get(keyLongitude); + set longitude(double longitude) => set(keyLongitude, longitude); @override Map toJson({bool full = false, bool forApiRQ = false}) => { From c1fda8376fe6fa0aaba51ca43abfc8f2c897b73e Mon Sep 17 00:00:00 2001 From: wigginsp Date: Tue, 2 Apr 2019 08:30:50 +0100 Subject: [PATCH 09/25] Deprecated result --- lib/src/objects/parse_response.dart | 5 +++++ lib/src/objects/response/parse_response_builder.dart | 3 +++ 2 files changed, 8 insertions(+) diff --git a/lib/src/objects/parse_response.dart b/lib/src/objects/parse_response.dart index 62f673a7a..a1afeaee9 100644 --- a/lib/src/objects/parse_response.dart +++ b/lib/src/objects/parse_response.dart @@ -5,6 +5,11 @@ class ParseResponse { int statusCode = -1; /// If result is a singular result, i.e. getByObjectID + /// + /// This is now deprecated - Please use results. This will contain a list of + /// results, no need to check if its a list or a list of elements anymore. + /// + @deprecated dynamic result; /// All results stored as a list - Even if only one response is returned diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index 5fcc6dd42..2ee69e563 100644 --- a/lib/src/objects/response/parse_response_builder.dart +++ b/lib/src/objects/response/parse_response_builder.dart @@ -70,6 +70,9 @@ class _ParseResponseBuilder { final List items = _handleMultipleResults(object, results); response.results = items; response.count = items.length; + if (items.isNotEmpty) { + response.result = items.first; + } } else { final T item = _handleSingleResult(object, map, false); response.count = 1; From 07bdae4801af3c2ba95ca3c01f7e73621c14e5b5 Mon Sep 17 00:00:00 2001 From: wigginsp Date: Tue, 2 Apr 2019 08:53:23 +0100 Subject: [PATCH 10/25] Corrected logic on response --- lib/src/objects/response/parse_response_builder.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index 2ee69e563..74075ca03 100644 --- a/lib/src/objects/response/parse_response_builder.dart +++ b/lib/src/objects/response/parse_response_builder.dart @@ -69,10 +69,8 @@ class _ParseResponseBuilder { final List results = map['results']; final List items = _handleMultipleResults(object, results); response.results = items; + response.result = items; response.count = items.length; - if (items.isNotEmpty) { - response.result = items.first; - } } else { final T item = _handleSingleResult(object, map, false); response.count = 1; From 4a2756fa2bea122320da0ff194b2d26b5723bafa Mon Sep 17 00:00:00 2001 From: Phill Date: Tue, 2 Apr 2019 18:38:42 +0100 Subject: [PATCH 11/25] ParseFile fix --- lib/src/base/parse_constants.dart | 2 ++ lib/src/objects/parse_file.dart | 8 ++++++-- lib/src/objects/parse_user.dart | 6 ++++-- lib/src/utils/parse_utils.dart | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index 1dc98e59d..b746b5569 100644 --- a/lib/src/base/parse_constants.dart +++ b/lib/src/base/parse_constants.dart @@ -26,6 +26,8 @@ const String keyVarEmail = 'email'; const String keyVarPassword = 'password'; const String keyVarSessionToken = 'sessionToken'; const String keyVarAcl = 'ACL'; +const String keyVarName = 'name'; +const String keyVarURL = 'url'; // Classes const String keyClassMain = 'ParseMain'; diff --git a/lib/src/objects/parse_file.dart b/lib/src/objects/parse_file.dart index b5c869917..594626658 100644 --- a/lib/src/objects/parse_file.dart +++ b/lib/src/objects/parse_file.dart @@ -28,8 +28,12 @@ class ParseFile extends ParseObject { } File file; - String name; - String url; + + String get name => super.get(keyVarName); + set name(String name) => set(keyVarName, name); + + String get url => super.get(keyVarURL); + set url(String url) => set(keyVarURL, url); @override // ignore: overridden_fields diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index 87ad70939..25d1a5c8b 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -150,14 +150,16 @@ class ParseUser extends ParseObject implements ParseCloneable { keyVarPassword: password }; - final Uri url = getSanitisedUri(_client, '$keyEndPointLogin', queryParams: queryParams); + final Uri url = getSanitisedUri(_client, '$keyEndPointLogin', + queryParams: queryParams); final Response response = await _client.get(url, headers: { keyHeaderRevocableSession: '1', }); - return _handleResponse(this, response, ParseApiRQ.login, _debug, className); + return _handleResponse( + this, response, ParseApiRQ.login, _debug, className); } on Exception catch (e) { return handleException(e, ParseApiRQ.login, _debug, className); } diff --git a/lib/src/utils/parse_utils.dart b/lib/src/utils/parse_utils.dart index 2cc15f71a..c2d8a090f 100644 --- a/lib/src/utils/parse_utils.dart +++ b/lib/src/utils/parse_utils.dart @@ -12,9 +12,9 @@ bool isDebugEnabled({bool objectLevelDebug}) { /// /// Strings are wrapped with "" but integers and others are not dynamic convertValueToCorrectType(dynamic value) { - if (value is String && !value.contains('__type')) { + /*if (value is String && !value.contains('__type')) { return '\"$value\"'; - } + }*/ if (value is DateTime || value is ParseObject) { return parseEncode(value); From 2ee4880eed404d2d28f5550eb23034eb6038f176 Mon Sep 17 00:00:00 2001 From: Phill Date: Tue, 2 Apr 2019 19:23:32 +0100 Subject: [PATCH 12/25] Removed protected --- lib/src/objects/parse_base.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/objects/parse_base.dart b/lib/src/objects/parse_base.dart index bd703f27f..2cdd579db 100644 --- a/lib/src/objects/parse_base.dart +++ b/lib/src/objects/parse_base.dart @@ -75,7 +75,6 @@ abstract class ParseBase { @override String toString() => json.encode(toJson()); - @protected dynamic fromJson(Map objectData) { if (objectData == null) { return this; @@ -120,7 +119,6 @@ abstract class ParseBase { Map getObjectData() => _objectData ?? Map(); /// Saves in storage - @protected Future saveInStorage(String key) async { final String objectJson = json.encode(toJson(full: true)); await ParseCoreData().getStore() From f166bd5866eff47f72b692fc52d164ffdc163a15 Mon Sep 17 00:00:00 2001 From: Phill Date: Thu, 4 Apr 2019 21:30:01 +0100 Subject: [PATCH 13/25] Testing ParseUser conversion --- example/lib/data/base/api_response.dart | 13 ++- .../diet_plan/provider_api_diet_plan.dart | 4 +- .../diet_plan/provider_db_diet_plan.dart | 13 ++- .../diet_plan/repository_diet_plan.dart | 38 +++---- .../user/contract_provider_user.dart | 17 +++ .../repositories/user/provider_api_user.dart | 61 +++++++++++ .../repositories/user/provider_db_user.dart | 103 ++++++++++++++++++ .../repositories/user/repository_user.dart | 86 +++++++++++++++ .../repository_diet_plan_api_test.dart | 45 ++++---- .../repository_diet_plan_db_test.dart | 65 ++++++++--- .../diet_plan/repository_diet_plan_test.dart | 10 +- .../repository/repository_mock_utils.dart | 8 ++ lib/src/objects/parse_response.dart | 2 - lib/src/objects/parse_user.dart | 10 +- 14 files changed, 386 insertions(+), 89 deletions(-) create mode 100644 example/lib/data/repositories/user/contract_provider_user.dart create mode 100644 example/lib/data/repositories/user/provider_api_user.dart create mode 100644 example/lib/data/repositories/user/provider_db_user.dart create mode 100644 example/lib/data/repositories/user/repository_user.dart diff --git a/example/lib/data/base/api_response.dart b/example/lib/data/base/api_response.dart index 787eec3b3..5f6fdf336 100644 --- a/example/lib/data/base/api_response.dart +++ b/example/lib/data/base/api_response.dart @@ -3,20 +3,20 @@ import 'package:parse_server_sdk/parse_server_sdk.dart'; import 'api_error.dart'; class ApiResponse { - ApiResponse(this.success, this.statusCode, this.result, this.error); + ApiResponse(this.success, this.statusCode, this.results, this.error) + : count = results?.length ?? 0, + result = results?.first; final bool success; final int statusCode; + final List results; final dynamic result; + int count; final ApiError error; - - dynamic getResult() { - return result; - } } ApiResponse getApiResponse(ParseResponse response) { - return ApiResponse(response.success, response.statusCode, response.result, + return ApiResponse(response.success, response.statusCode, response.results, getApiError(response.error)); } @@ -24,6 +24,7 @@ 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/repositories/diet_plan/provider_api_diet_plan.dart b/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart index 0050ceb20..41583c797 100644 --- a/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart +++ b/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart @@ -22,7 +22,7 @@ class DietPlanProviderApi implements DietPlanProviderContract { return response; } - responses.add(response.result); + response?.results?.forEach(responses.add); } return ApiResponse(true, 200, responses, null); @@ -66,7 +66,7 @@ class DietPlanProviderApi implements DietPlanProviderContract { return response; } - responses.add(response.result); + response?.results?.forEach(responses.add); } 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 index 17e7e78f4..4ad891866 100644 --- a/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart +++ b/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart @@ -4,6 +4,7 @@ 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:parse_server_sdk/parse_server_sdk.dart'; import 'package:sembast/sembast.dart'; class DietPlanProviderDB implements DietPlanProviderContract { @@ -18,7 +19,7 @@ class DietPlanProviderDB implements DietPlanProviderContract { final Record recordToAdd = Record(_store, values, item.objectId); final Record recordFromDB = await _db.putRecord(recordToAdd); return ApiResponse( - true, 200, convertRecordToItem(record: recordFromDB), null); + true, 200, [convertRecordToItem(record: recordFromDB)], null); } @override @@ -66,7 +67,7 @@ class DietPlanProviderDB implements DietPlanProviderContract { final Record record = await _store.getRecord(id); if (record != null) { final DietPlan userFood = convertRecordToItem(record: record); - return ApiResponse(true, 200, userFood, null); + return ApiResponse(true, 200, [userFood], null); } else { return errorResponse; } @@ -78,7 +79,7 @@ class DietPlanProviderDB implements DietPlanProviderContract { final Finder finder = Finder( filter: - Filter.greaterThan('keyUpdatedAt', date.millisecondsSinceEpoch)); + Filter.greaterThan(keyVarUpdatedAt, date.millisecondsSinceEpoch)); final List records = await _store.findRecords(finder); @@ -129,16 +130,16 @@ class DietPlanProviderDB implements DietPlanProviderContract { } return ApiResponse( - true, 200, convertRecordToItem(values: returnedItems), null); + 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; + values[keyVarObjectId] = item.objectId; if (item.updatedAt != null) { - values['updatedAt'] = item.updatedAt.millisecondsSinceEpoch; + values[keyVarUpdatedAt] = item.updatedAt.millisecondsSinceEpoch; } return values; diff --git a/example/lib/data/repositories/diet_plan/repository_diet_plan.dart b/example/lib/data/repositories/diet_plan/repository_diet_plan.dart index 9d1b4775c..beaebff14 100644 --- a/example/lib/data/repositories/diet_plan/repository_diet_plan.dart +++ b/example/lib/data/repositories/diet_plan/repository_diet_plan.dart @@ -8,19 +8,19 @@ import 'package:sembast/sembast.dart'; class DietPlanRepository implements DietPlanProviderContract { static DietPlanRepository init(Database dbConnection, - {DietPlanProviderContract repositoryDB, - DietPlanProviderContract repositoryAPI}) { + {DietPlanProviderContract mockDBProvider, + DietPlanProviderContract mockAPIProvider}) { final DietPlanRepository repository = DietPlanRepository(); - if (repositoryDB != null) { - repository.db = repositoryDB; + if (mockDBProvider != null) { + repository.db = mockDBProvider; } else { - final Store store = dbConnection.getStore('repository-$keyDietPlan'); + final Store store = dbConnection.getStore('repository_store'); repository.db = DietPlanProviderDB(dbConnection, store); } - if (repositoryAPI != null) { - repository.api = repositoryAPI; + if (mockAPIProvider != null) { + repository.api = mockAPIProvider; } else { repository.api = DietPlanProviderApi(); } @@ -61,27 +61,18 @@ class DietPlanRepository implements DietPlanProviderContract { final ApiResponse response = await api.addAll(items); - if (response.success && isValidList(response.result)) { - await db.addAll(items); + if (response.success && isValidList(response.results)) { + await db.addAll(response.results); } return response; } @override - Future getAll( - {bool fromApi = false, bool fromDb = false}) async { + Future getAll({bool fromApi = false}) { 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(); } @@ -116,9 +107,8 @@ class DietPlanRepository implements DietPlanProviderContract { final ApiResponse response = await api.getNewerThan(date); - if (response.success && response.result != null) { - final List list = response.result; - await db.updateAll(list); + if (response.success && isValidList(response.results)) { + await db.updateAll(response.results); } return response; @@ -165,8 +155,8 @@ class DietPlanRepository implements DietPlanProviderContract { } ApiResponse response = await api.updateAll(items); - if (response.success && isValidList(response.result)) { - response = await db.updateAll(items); + if (response.success && isValidList(response.results)) { + response = await db.updateAll(response.results); } return response; diff --git a/example/lib/data/repositories/user/contract_provider_user.dart b/example/lib/data/repositories/user/contract_provider_user.dart new file mode 100644 index 000000000..0c12f2edf --- /dev/null +++ b/example/lib/data/repositories/user/contract_provider_user.dart @@ -0,0 +1,17 @@ +import 'package:flutter_plugin_example/data/base/api_response.dart'; +import 'package:flutter_plugin_example/data/model/user.dart'; + +abstract class UserProviderContract { + Future createUser( + String username, String password, String emailAddress); + Future currentUser(); + Future signUp(User user); + Future login(User user); + void logout(User user); + Future getCurrentUserFromServer(); + Future requestPasswordReset(User user); + Future verificationEmailRequest(User user); + Future save(User user); + Future destroy(User user); + Future allUsers(); +} diff --git a/example/lib/data/repositories/user/provider_api_user.dart b/example/lib/data/repositories/user/provider_api_user.dart new file mode 100644 index 000000000..1a44e8899 --- /dev/null +++ b/example/lib/data/repositories/user/provider_api_user.dart @@ -0,0 +1,61 @@ +import 'package:flutter_plugin_example/data/base/api_response.dart'; +import 'package:flutter_plugin_example/data/model/user.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; + +import 'contract_provider_user.dart'; + +class UserProviderApi implements UserProviderContract { + @override + Future createUser( + String username, String password, String emailAddress) { + return Future.value(User(username, password, emailAddress)); + } + + @override + Future currentUser() { + return ParseUser.currentUser(); + } + + @override + Future getCurrentUserFromServer() async { + return getApiResponse(await ParseUser.getCurrentUserFromServer()); + } + + @override + Future destroy(User user) async { + return getApiResponse(await user.destroy()); + } + + @override + Future login(User user) async { + return getApiResponse(await user.login()); + } + + @override + Future requestPasswordReset(User user) async { + return getApiResponse(await user.requestPasswordReset()); + } + + @override + Future save(User user) async { + return getApiResponse(await user.save()); + } + + @override + Future signUp(User user) async { + return getApiResponse(await user.signUp()); + } + + @override + Future verificationEmailRequest(User user) async { + return getApiResponse(await user.verificationEmailRequest()); + } + + @override + Future allUsers() async { + return getApiResponse(await ParseUser.all()); + } + + @override + void logout(User user) => user.logout(); +} diff --git a/example/lib/data/repositories/user/provider_db_user.dart b/example/lib/data/repositories/user/provider_db_user.dart new file mode 100644 index 000000000..d135418c8 --- /dev/null +++ b/example/lib/data/repositories/user/provider_db_user.dart @@ -0,0 +1,103 @@ +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/user.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:sembast/sembast.dart'; + +import 'contract_provider_user.dart'; + +class UserProviderDB implements UserProviderContract { + UserProviderDB(this._db, this._store); + + final Store _store; + final Database _db; + + @override + Future createUser( + String username, String password, String emailAddress) async { + final User user = User(username, password, emailAddress); + final Map values = convertItemToStorageMap(user); + final Record recordToAdd = Record(_store, values, user.objectId); + final Record recordFromDB = await _db.putRecord(recordToAdd); + return convertRecordToItem(record: recordFromDB); + } + + @override + Future currentUser() { + return null; + } + + @override + Future getCurrentUserFromServer() async { + return null; + } + + @override + Future destroy(User user) async { + await _store.delete(user.objectId); + return ApiResponse(true, 200, null, null); + } + + @override + Future login(User user) async { + return null; + } + + @override + Future requestPasswordReset(User user) async { + return null; + } + + @override + Future save(User user) async { + final Map values = convertItemToStorageMap(user); + final Record recordToAdd = Record(_store, values, user.objectId); + final Record recordFromDB = await _db.putRecord(recordToAdd); + return ApiResponse( + true, 200, [convertRecordToItem(record: recordFromDB)], null); + } + + @override + Future signUp(User user) { + return null; + } + + @override + Future verificationEmailRequest(User user) async { + return null; + } + + @override + Future allUsers() async { + return null; + } + + @override + void logout(User user) {} + + Map convertItemToStorageMap(User item) { + final Map values = Map(); + values['value'] = json.jsonEncode(item.toJson(full: true)); + values[keyVarObjectId] = item.objectId; + item.updatedAt != null + ? values[keyVarUpdatedAt] = item.updatedAt.millisecondsSinceEpoch + : values[keyVarCreatedAt] = DateTime.now().millisecondsSinceEpoch; + return values; + } + + User convertRecordToItem({Record record, Map values}) { + try { + values ??= record.value; + final User item = + User.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/user/repository_user.dart b/example/lib/data/repositories/user/repository_user.dart new file mode 100644 index 000000000..b9a5c26a0 --- /dev/null +++ b/example/lib/data/repositories/user/repository_user.dart @@ -0,0 +1,86 @@ +import 'package:flutter_plugin_example/data/base/api_response.dart'; +import 'package:flutter_plugin_example/data/model/user.dart'; +import 'package:sembast/sembast.dart'; + +import 'contract_provider_user.dart'; +import 'provider_api_user.dart'; +import 'provider_db_user.dart'; + +class UserRepository implements UserProviderContract { + static UserRepository init(Database dbConnection, + {UserProviderContract mockDBProvider, + UserProviderContract mockAPIProvider}) { + final UserRepository repository = UserRepository(); + + if (mockDBProvider != null) { + repository.db = mockDBProvider; + } else { + final Store store = dbConnection.getStore('reposutory_user'); + repository.db = UserProviderDB(dbConnection, store); + } + + if (mockAPIProvider != null) { + repository.api = mockAPIProvider; + } else { + repository.api = UserProviderApi(); + } + + return repository; + } + + UserProviderContract api; + UserProviderContract db; + + @override + Future createUser( + String username, String password, String emailAddress) async { + api.createUser(username, password, emailAddress); + + final User user = await api.createUser(username, password, emailAddress); + if (user != null) { + await db.createUser(username, password, emailAddress); + } + + return user; + } + + @override + Future currentUser() => db.currentUser(); + + @override + Future destroy(User user) async { + ApiResponse response = await api.destroy(user); + response = await db.destroy(user); + return response; + } + + @override + Future allUsers() => api.allUsers(); + + @override + Future getCurrentUserFromServer() => + api.getCurrentUserFromServer(); + + @override + Future login(User user) => api.login(user); + + @override + void logout(User user) => api.logout(user); + + @override + Future requestPasswordReset(User user) => + api.requestPasswordReset(user); + + @override + Future save(User user) async { + ApiResponse response = await api.save(user); + response = await db.save(user); + return response; + } + + @override + Future signUp(User user) => api.signUp(user); + + @override + Future verificationEmailRequest(User user) => api.signUp(user); +} 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 index c56b9312d..9536a8ea8 100644 --- 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 @@ -1,14 +1,16 @@ -// 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 'package:shared_preferences/shared_preferences.dart'; import '../repository_mock_utils.dart'; +// ignore_for_file: invalid_use_of_protected_member void main() { DietPlanProviderContract repository; + SharedPreferences.setMockInitialValues(Map()); Future getRepository() async { repository ??= DietPlanProviderApi(); @@ -35,11 +37,11 @@ void main() { expected.getObjectData()['objectId'] = null; // When - ApiResponse response = await repository.add(expected); + final ApiResponse response = await repository.add(expected); final DietPlan actual = response.result; // CLEAR FROM DB - response = await repository.remove(actual); + await deleteFromApi(response.results); // Then expect(actual.protein, expected.protein); @@ -59,12 +61,10 @@ void main() { // When final ApiResponse response = await repository.addAll(actual); - final List items = await response.result; + final List items = response.results; // CLEAR FROM DB - for (final DietPlan item in items) { - await repository.remove(item); - } + await deleteFromApi(response.results); // Then expect(response.success, true); @@ -77,13 +77,15 @@ void main() { dummy.getObjectData()['objectId'] = null; // When - ApiResponse response = await repository.add(dummy); + final ApiResponse response = await repository.add(dummy); final DietPlan expected = response.result; - response = await repository.getById(expected.objectId); - final DietPlan actual = response.result; + final ApiResponse updateResponse = + await repository.getById(expected.objectId); + final DietPlan actual = updateResponse.result; // CLEAR FROM DB - response = await repository.remove(actual); + await deleteFromApi(response.results); + await deleteFromApi(updateResponse.results); // Then expect(actual.objectId, expected.objectId); @@ -97,14 +99,15 @@ void main() { // 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); + await deleteFromApi(baseResponse.results); + await deleteFromApi(responseWithoutResult.results); + await deleteFromApi(responseWithResult.results); // Then expect(responseWithResult.success, true); @@ -127,12 +130,9 @@ void main() { // 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); - } + await deleteFromApi(response.results); // Then expect(response.success, true); @@ -143,7 +143,7 @@ void main() { // Given final DietPlan expected = getDummyDietPlan(); expected.getObjectData()['objectId'] = null; - ApiResponse response = await repository.add(expected); + final ApiResponse response = await repository.add(expected); final DietPlan initialResponse = response.result; // When @@ -153,7 +153,8 @@ void main() { final DietPlan actual = updateResponse.result; // CLEAR FROM DB - response = await repository.remove(actual); + await deleteFromApi(response.results); + await deleteFromApi(updateResponse.results); // Then expect(actual.protein, 10); @@ -177,12 +178,10 @@ void main() { item1.protein = 9; item2.protein = 10; final ApiResponse updateResponse = await repository.updateAll(actual); - final List updated = updateResponse.result; + final List updated = updateResponse.results; // CLEAR FROM DB - for (final DietPlan day in updated) { - await repository.remove(day); - } + await deleteFromApi(updateResponse.results); // Then expect(updated[0].protein, 9); 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 index d1d2e8394..f60dff301 100644 --- 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 @@ -3,15 +3,18 @@ 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:parse_server_sdk/parse_server_sdk.dart'; import 'package:sembast/sembast.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../repository_mock_utils.dart'; void main() { DietPlanProviderContract repository; + SharedPreferences.setMockInitialValues(Map()); Store _getStore(Database database) { - return database.getStore('repository_$keyDietPlan'); + return database.getStore('diet_plan_repository_test'); } Future getRepository() async { @@ -47,6 +50,9 @@ void main() { final ApiResponse response = await repository.add(expected); final DietPlan actual = response.result; + // CLEAR FROM DB + await deleteFromApi(response.results); + // Then expect(actual.objectId, expected.objectId); expect(actual.protein, expected.protein); @@ -67,7 +73,7 @@ void main() { // When final ApiResponse response = await repository.addAll(actual); - final List items = await response.result; + final List items = response.results; // Then expect(response.success, true); @@ -80,8 +86,12 @@ void main() { final DietPlan actual = getDummyDietPlan(); // When - await repository.add(actual); - final ApiResponse response = await repository.getById('1234abcd'); + final ApiResponse response = await repository.add(actual); + final ApiResponse updateResponse = await repository.getById('1234abcd'); + + // CLEAR FROM DB + await deleteFromApi(response.results); + await deleteFromApi(updateResponse.results); // Then final DietPlan expected = response.result; @@ -97,11 +107,15 @@ void main() { final List actual = List()..add(item1)..add(item2); // When - await repository.addAll(actual); + final ApiResponse response = await repository.addAll(actual); // Then - final ApiResponse response = await repository.getAll(); - final List expected = response.result; + final ApiResponse updateResponse = await repository.getAll(); + final List expected = updateResponse.results; + + // CLEAR FROM DB + await deleteFromApi(response.results); + await deleteFromApi(updateResponse.results); expect(2, expected.length); expect(actual[0].objectId, expected[0].objectId); @@ -112,14 +126,18 @@ void main() { // Given final DietPlan expected = getDummyDietPlan(); // ignore: invalid_use_of_protected_member - expected.getObjectData()['keyUpdatedAt'] = DateTime.now(); - await repository.add(expected); + expected.getObjectData()[keyVarUpdatedAt] = DateTime.now(); + final ApiResponse response = 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; + final ApiResponse updateResponse = await repository.getNewerThan(dateTime); + final List actual = updateResponse.results; + + // CLEAR FROM DB + await deleteFromApi(response.results); + await deleteFromApi(updateResponse.results); // Then expect(actual.isNotEmpty, true); @@ -130,12 +148,16 @@ void main() { // Given final DietPlan item = getDummyDietPlan(); item.protein = 1000; - await repository.add(item); + final ApiResponse apiResponse = await repository.add(item); // When item.protein = 1000; - final ApiResponse response = await repository.update(item); - final DietPlan userFood = response.result; + final ApiResponse updateResponse = await repository.update(item); + final DietPlan userFood = updateResponse.result; + + // CLEAR FROM DB + await deleteFromApi(apiResponse.results); + await deleteFromApi(updateResponse.results); // Then expect(item.objectId, userFood.objectId); @@ -155,13 +177,22 @@ void main() { item2.objectId = '${objectIdPrefix}1'; actual.add(item2); - await repository.addAll(actual); + final ApiResponse apiResponse = await repository.addAll(actual); + + // CLEAR FROM DB + await deleteFromApi(apiResponse.results); // When actual[0].protein = 1000; actual[1].protein = 1000; - final ApiResponse response = await repository.updateAll(actual); - final List expected = response.result; + final ApiResponse updateResponse = await repository.updateAll(actual); + final List expected = updateResponse.results; + + // CLEAR FROM DB + await deleteFromApi(updateResponse.results); + // CLEAR FROM DB + await deleteFromApi(updateResponse.results); + await deleteFromApi(apiResponse.results); // Then expect(actual[0].objectId, expected[0].objectId); 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 index e10e34fc6..adce1b58d 100644 --- a/example/test/data/repository/diet_plan/repository_diet_plan_test.dart +++ b/example/test/data/repository/diet_plan/repository_diet_plan_test.dart @@ -4,11 +4,13 @@ import 'package:flutter_plugin_example/data/repositories/diet_plan/contract_prov 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 'package:shared_preferences/shared_preferences.dart'; import '../repository_mock_utils.dart'; void main() { DietPlanRepository repository; + SharedPreferences.setMockInitialValues(Map()); DietPlanProviderContract apiRepository; DietPlanProviderContract dbRepository; @@ -23,19 +25,19 @@ void main() { when(repositoryApi.add(any)).thenAnswer((_) async => Future.value( - ApiResponse(true, 200, getDummyDietPlan(), null))); + 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))); + 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))); + ApiResponse(true, 200, [getDummyDietPlan()], null))); when(repositoryApi.getById(any)).thenAnswer((_) async => Future.value(ApiResponse(true, 200, mockList, null))); @@ -51,7 +53,7 @@ void main() { dbRepository = await getDBRepository(); final DietPlanRepository repository = DietPlanRepository.init(null, - repositoryDB: dbRepository, repositoryAPI: apiRepository); + mockDBProvider: dbRepository, mockAPIProvider: apiRepository); return repository; } diff --git a/example/test/data/repository/repository_mock_utils.dart b/example/test/data/repository/repository_mock_utils.dart index 6a89f3ca7..7d3a9e9ff 100644 --- a/example/test/data/repository/repository_mock_utils.dart +++ b/example/test/data/repository/repository_mock_utils.dart @@ -37,3 +37,11 @@ DietPlan getDummyDietPlan() { ..fat = 20 ..status = 0; } + +Future deleteFromApi(List results) async { + if (results != null && results.isNotEmpty) { + for (final ParseObject item in results) { + await item.delete(); + } + } +} diff --git a/lib/src/objects/parse_response.dart b/lib/src/objects/parse_response.dart index a1afeaee9..d278bbdd1 100644 --- a/lib/src/objects/parse_response.dart +++ b/lib/src/objects/parse_response.dart @@ -8,8 +8,6 @@ class ParseResponse { /// /// This is now deprecated - Please use results. This will contain a list of /// results, no need to check if its a list or a list of elements anymore. - /// - @deprecated dynamic result; /// All results stored as a list - Even if only one response is returned diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index 25d1a5c8b..11f1d2603 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -107,8 +107,9 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Current user is stored locally, but in case of a server update [bool] /// fromServer can be called and an updated version of the [User] object will be /// returned - static Future currentUser() { - return _getUserFromLocalStore(); + static Future currentUser({ParseCloneable user}) async { + final ParseCloneable cloneable = user ?? ParseUser._getEmptyUser(); + return await _getUserFromLocalStore(cloneable); } /// Registers a user on Parse Server @@ -318,7 +319,7 @@ class ParseUser extends ParseObject implements ParseCloneable { } } - static Future _getUserFromLocalStore() async { + static Future _getUserFromLocalStore(ParseCloneable object) async { final String userJson = (await ParseCoreData().getStore()).getString(keyParseStoreUser); @@ -326,8 +327,7 @@ class ParseUser extends ParseObject implements ParseCloneable { final Map userMap = json.decode(userJson); if (userMap != null) { ParseCoreData().setSessionId(userMap[keyParamSessionToken]); - final ParseUser user = parseDecode(userMap); - return user; + return object.clone(userMap); } } From 16a9fbf5436ae2b96c5ee345c7311aa0afe487ae Mon Sep 17 00:00:00 2001 From: wigginsp Date: Fri, 5 Apr 2019 13:14:54 +0100 Subject: [PATCH 14/25] Fixed ParseFile code saving --- lib/src/objects/parse_file.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/objects/parse_file.dart b/lib/src/objects/parse_file.dart index 594626658..bd2a53909 100644 --- a/lib/src/objects/parse_file.dart +++ b/lib/src/objects/parse_file.dart @@ -46,7 +46,7 @@ class ParseFile extends ParseObject { {'__type': keyFile, 'name': name, 'url': url}; @override - String toString() => json.encode(toString()); + String toString() => json.encode(toJson(full: true)); Future loadStorage() async { final Directory tempPath = await getTemporaryDirectory(); From 52de117669138dbaf43d0879751e532b9db277f7 Mon Sep 17 00:00:00 2001 From: wigginsp Date: Fri, 5 Apr 2019 13:15:47 +0100 Subject: [PATCH 15/25] ParseInstallation fix --- lib/src/objects/parse_installation.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index 155b7cdfa..e4ed0df9f 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -123,7 +123,7 @@ class ParseInstallation extends ParseObject { (await ParseCoreData().getStore()).getString(keyParseStoreInstallation); if (installationJson != null) { - final dynamic installationMap = parseDecode(json.decode(installationJson)); + final Map installationMap = json.decode(installationJson); if (installationMap != null) { return ParseInstallation()..fromJson(installationMap); From 9c08776b650c4782145ea232886c92cc9b1d2278 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 5 Apr 2019 20:16:27 +0100 Subject: [PATCH 16/25] ParseUser supporting custom user objects --- example/lib/data/model/user.dart | 3 ++- example/lib/{ui => }/main.dart | 43 ++++++++++++++++++++++---------- lib/src/objects/parse_user.dart | 19 +++++++++----- 3 files changed, 45 insertions(+), 20 deletions(-) rename example/lib/{ui => }/main.dart (89%) diff --git a/example/lib/data/model/user.dart b/example/lib/data/model/user.dart index 9084debe8..78a0f9761 100644 --- a/example/lib/data/model/user.dart +++ b/example/lib/data/model/user.dart @@ -35,7 +35,8 @@ class User extends ParseUser implements ParseCloneable { set name(String name) => set(keyName, name); String get displayPicture => get(keyDisplayPicture); - set displayPicture(String displayPicture) => set(keyDisplayPicture, displayPicture); + set displayPicture(String displayPicture) => + set(keyDisplayPicture, displayPicture); bool get proUser => get(keyProUser); set proUser(bool proUser) => set(keyProUser, proUser); diff --git a/example/lib/ui/main.dart b/example/lib/main.dart similarity index 89% rename from example/lib/ui/main.dart rename to example/lib/main.dart index e4d6b9751..b5426b9a0 100644 --- a/example/lib/ui/main.dart +++ b/example/lib/main.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_plugin_example/data/base/api_response.dart'; -import 'package:flutter_plugin_example/data/model/day.dart'; import 'package:flutter_plugin_example/data/model/diet_plan.dart'; import 'package:flutter_plugin_example/data/model/user.dart'; import 'package:flutter_plugin_example/data/repositories/diet_plan/repository_diet_plan.dart'; +import 'package:flutter_plugin_example/data/repositories/user/repository_user.dart'; import 'package:flutter_plugin_example/domain/constants/application_constants.dart'; import 'package:flutter_plugin_example/domain/utils/db_utils.dart'; import 'package:flutter_stetho/flutter_stetho.dart'; @@ -22,7 +22,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - DietPlanRepository repository; + DietPlanRepository dietPlanRepo; + UserRepository userRepo; @override void initState() { @@ -63,11 +64,12 @@ class _MyAppState extends State { Future runTestQueries() async { // Basic repository example - await repositoryAddItems(); - await repositoryGetAllItems(); + await repositoryAddUser(); + /*await repositoryAddItems(); + await repositoryGetAllItems()*/ //Basic usage - createItem(); + /*createItem(); getAllItems(); getAllItemsByName(); getSingleItem(); @@ -76,7 +78,7 @@ class _MyAppState extends State { initUser(); function(); functionWithParameters(); - test(); + test();*/ } Future test() async { @@ -93,9 +95,9 @@ class _MyAppState extends State { } } - final QueryBuilder query = QueryBuilder(Day()) - ..whereEqualTo(Day.keyOwner, user); - var item = await query.query(); + final QueryBuilder query = QueryBuilder(DietPlan()) + ..whereEqualTo(keyProtein, 30); + final ParseResponse item = await query.query(); print(item.toString()); } @@ -294,8 +296,22 @@ class _MyAppState extends State { } } + Future repositoryAddUser() async { + final User user = User('test_username', 'password', 'test@gmail.com'); + + final ApiResponse response = await userRepo.save(user); + + if (!response.success) { + await userRepo.login(user); + } + + final User currentUser = + await ParseUser.currentUser(customUserObject: User.clone()); + print(currentUser); + } + Future repositoryAddItems() async { - final List dietPlans = List(); + final List dietPlans = []; final List json = const JsonDecoder().convert(dietPlansToAdd); for (final Map element in json) { @@ -304,21 +320,22 @@ class _MyAppState extends State { } await initRepository(); - final ApiResponse response = await repository.addAll(dietPlans); + final ApiResponse response = await dietPlanRepo.addAll(dietPlans); if (response.success) { print(response.result); } } Future repositoryGetAllItems() async { - final ApiResponse response = await repository.getAll(); + final ApiResponse response = await dietPlanRepo.getAll(); if (response.success) { print(response.result); } } Future initRepository() async { - repository ??= DietPlanRepository.init(await getDB()); + dietPlanRepo ??= DietPlanRepository.init(await getDB()); + userRepo ??= UserRepository.init(await getDB()); } } diff --git a/lib/src/objects/parse_user.dart b/lib/src/objects/parse_user.dart index 11f1d2603..2cc6a7d18 100644 --- a/lib/src/objects/parse_user.dart +++ b/lib/src/objects/parse_user.dart @@ -107,9 +107,13 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Current user is stored locally, but in case of a server update [bool] /// fromServer can be called and an updated version of the [User] object will be /// returned - static Future currentUser({ParseCloneable user}) async { - final ParseCloneable cloneable = user ?? ParseUser._getEmptyUser(); - return await _getUserFromLocalStore(cloneable); + static Future currentUser({ParseCloneable customUserObject}) async { + + if (customUserObject != null) { + return await _getUserFromLocalStore(cloneable: customUserObject); + } else { + return await _getUserFromLocalStore(); + } } /// Registers a user on Parse Server @@ -319,15 +323,18 @@ class ParseUser extends ParseObject implements ParseCloneable { } } - static Future _getUserFromLocalStore(ParseCloneable object) async { + static Future _getUserFromLocalStore( + {ParseCloneable cloneable}) async { final String userJson = (await ParseCoreData().getStore()).getString(keyParseStoreUser); if (userJson != null) { final Map userMap = json.decode(userJson); - if (userMap != null) { + if (cloneable != null) { + return cloneable.clone(userMap); + } else { ParseCoreData().setSessionId(userMap[keyParamSessionToken]); - return object.clone(userMap); + return parseDecode(userMap); } } From 012539be660e4b715a737c7492eb26dd580ddf99 Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Sat, 6 Apr 2019 03:14:19 -0300 Subject: [PATCH 17/25] Add Support to Relational Queries / Counting Objects (#146) * Add Support to Relational Queries Add Support to Relational Queries * Add Support to Counting Objects Add Support to Counting Objects https://docs.parseplatform.org/rest/guide/#counting-objects --- lib/src/network/parse_query.dart | 62 +++++++++++++++++-- .../response/parse_response_builder.dart | 5 ++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/lib/src/network/parse_query.dart b/lib/src/network/parse_query.dart index d06536b07..d32a5c1ea 100644 --- a/lib/src/network/parse_query.dart +++ b/lib/src/network/parse_query.dart @@ -237,6 +237,22 @@ class QueryBuilder { '\"$column\":{\"\$within\":{\"\$box\": [{\"__type\": \"GeoPoint\",\"latitude\":$latitudeS,\"longitude\":$longitudeS},{\"__type\": \"GeoPoint\",\"latitude\":$latitudeN,\"longitude\":$longitudeN}]}}')); } + // Add a constraint to the query that requires a particular key's value match another QueryBuilder + void whereMatchesQuery(String column, QueryBuilder query) { + String inQuery = query._buildQueryRelational(query.object.className); + + queries.add(MapEntry( + _SINGLE_QUERY, '\"$column\":{\"\$inQuery\":$inQuery}')); + } + + //Add a constraint to the query that requires a particular key's value does not match another QueryBuilder + void whereDoesNotMatchQuery(String column, QueryBuilder query) { + String inQuery = query._buildQueryRelational(query.object.className); + + queries.add(MapEntry( + _SINGLE_QUERY, '\"$column\":{\"\$notInQuery\":$inQuery}')); + } + /// Finishes the query and calls the server /// /// Make sure to call this after defining your queries @@ -244,12 +260,29 @@ class QueryBuilder { return object.query(_buildQuery()); } + ///Counts the number of objects that match this query + Future count() async { + return object.query(_buildQueryCount()); + } + /// Builds the query for Parse String _buildQuery() { queries = _checkForMultipleColumnInstances(queries); return 'where={${buildQueries(queries)}}${getLimiters(limiters)}'; } + /// Builds the query relational for Parse + String _buildQueryRelational(String className) { + queries = _checkForMultipleColumnInstances(queries); + return '{\"where\":{${buildQueries(queries)}},\"className\":\"$className\"${getLimitersRelational(limiters)}}'; + } + + /// Builds the query for Parse + String _buildQueryCount() { + queries = _checkForMultipleColumnInstances(queries); + return 'where={${buildQueries(queries)}}&count=1'; + } + /// Runs through all queries and adds them to a query string String buildQueries(List> queries) { String queryBuilder = ''; @@ -284,17 +317,20 @@ class QueryBuilder { /// that the column and value are being queried against MapEntry _buildQueryWithColumnValueAndOperator( MapEntry columnAndValue, String queryOperator) { - final String key = columnAndValue.key; - final dynamic value = convertValueToCorrectType(parseEncode(columnAndValue.value)); + final dynamic value = + convertValueToCorrectType(parseEncode(columnAndValue.value)); if (queryOperator == _NO_OPERATOR_NEEDED) { - return MapEntry(_NO_OPERATOR_NEEDED, '\"$key\": ${jsonEncode(value)}'); + return MapEntry( + _NO_OPERATOR_NEEDED, '\"$key\": ${jsonEncode(value)}'); } else { String queryString = '\"$key\":'; - final Map queryOperatorAndValueMap = Map(); + final Map queryOperatorAndValueMap = + Map(); queryOperatorAndValueMap[queryOperator] = parseEncode(value); - final String formattedQueryOperatorAndValue = jsonEncode(queryOperatorAndValueMap); + final String formattedQueryOperatorAndValue = + jsonEncode(queryOperatorAndValueMap); queryString += '$formattedQueryOperatorAndValue'; return MapEntry(key, queryString); } @@ -336,7 +372,8 @@ class QueryBuilder { for (MapEntry queryToCompact in listOfQueriesCompact) { var queryToCompactValue = queryToCompact.value.toString(); queryToCompactValue = queryToCompactValue.replaceFirst("{", ""); - queryToCompactValue = queryToCompactValue.replaceRange(queryToCompactValue.length - 1, queryToCompactValue.length, ""); + queryToCompactValue = queryToCompactValue.replaceRange( + queryToCompactValue.length - 1, queryToCompactValue.length, ""); if (listOfQueriesCompact.first == queryToCompact) { queryEnd += queryToCompactValue.replaceAll(queryStart, ' '); } else { @@ -364,4 +401,17 @@ class QueryBuilder { }); return result; } + + /// Adds the limiters to the query relational, i.e. skip=10, limit=10 + String getLimitersRelational(Map map) { + String result = ''; + map.forEach((String key, dynamic value) { + if (result != null) { + result = result + ',\"$key":$value'; + } else { + result = '\"$key\":$value'; + } + }); + return result; + } } diff --git a/lib/src/objects/response/parse_response_builder.dart b/lib/src/objects/response/parse_response_builder.dart index 74075ca03..b7e06767b 100644 --- a/lib/src/objects/response/parse_response_builder.dart +++ b/lib/src/objects/response/parse_response_builder.dart @@ -71,6 +71,11 @@ class _ParseResponseBuilder { response.results = items; response.result = items; response.count = items.length; + } else if (map != null && map.length == 2 && map.containsKey('count')) { + final List results = [map['count']]; + response.results = results; + response.result = results; + response.count = map['count']; } else { final T item = _handleSingleResult(object, map, false); response.count = 1; From 8a0c3341f5606b4d2fc57d8a4bb30b6cae4405e6 Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Sat, 6 Apr 2019 09:11:51 -0300 Subject: [PATCH 18/25] Update the documentation with Relational Queries and Count Objects (#147) * Add Support to Relational Queries Add Support to Relational Queries * Add Support to Counting Objects Add Support to Counting Objects https://docs.parseplatform.org/rest/guide/#counting-objects * Update README.md Update documentation with example for Relational Queries and Counting Objects * Update README.md --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 37f1b35c6..157a13d12 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,54 @@ The features available are:- * Ascending * Descending * Plenty more! + +## Relational queries +If you want to retrieve objects where a field contains an object that matches another query, you can use the +__whereMatchesQuery__ condition. +For example, imagine you vave Post class and a Comment class, where each Comment has a pointer to its parent Post. +You can find comments on posts with images by doing: + +```dart + QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); + + QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereMatchesQuery('post', queryPost); + + 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 +__whereDoesNotMatchQuery__ condition. +Imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. +You can find comments on posts without images by doing: + +```dart + QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); + + QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereDoesNotMatchQuery('post', queryPost); + + 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; + } +``` ## Objects From efe937e4edc31fa0615f4b4f4ae542a49955b4c7 Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Sat, 6 Apr 2019 14:48:20 -0300 Subject: [PATCH 19/25] Update README.md - Small typo (#148) * Add Support to Relational Queries Add Support to Relational Queries * Add Support to Counting Objects Add Support to Counting Objects https://docs.parseplatform.org/rest/guide/#counting-objects * Update README.md Update documentation with example for Relational Queries and Counting Objects * Update README.md * Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 157a13d12..aeaefdc27 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ The features available are:- ## Relational queries If you want to retrieve objects where a field contains an object that matches another query, you can use the __whereMatchesQuery__ condition. -For example, imagine you vave Post class and a Comment class, where each Comment has a pointer to its parent Post. +For example, imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. You can find comments on posts with images by doing: ```dart From 4c0c56d7152d686ab5881d4b8f4b0ba18129282b Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Sat, 13 Apr 2019 15:07:24 -0300 Subject: [PATCH 20/25] BugFix LiveQuery - update readme.md (#150) * Add Support to Relational Queries Add Support to Relational Queries * Add Support to Counting Objects Add Support to Counting Objects https://docs.parseplatform.org/rest/guide/#counting-objects * Update README.md Update documentation with example for Relational Queries and Counting Objects * Update README.md * Update README.md * BugFix LiveQuery BugFix LiveQuery * Bugfix LiveQuery Bugfix LiveQuery * Update README.md Reorganization of sessions and inclusion of documentation on LiveQuery * Update README.md * Update README.md * Bugfix LiveQuery Bugfix LiveQuery * Changed to make optional clientKey Changed to make optional clientKey --- README.md | 263 +++++++++++++++++++------- lib/src/enums/parse_enum_api_rq.dart | 3 +- lib/src/network/parse_live_query.dart | 203 ++++++++++++++++---- 3 files changed, 358 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index aeaefdc27..2320c7f04 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,95 @@ Parse().initialize( masterKey: ApplicationConstants.keyParseMasterKey, clientKey: ApplicationConstants.keyParseClientKey, debug: true, - liveQuery: true, + liveQueryUrl: ApplicationConstants.keyLiveQueryUrl, autoSendSessionId: true, securityContext: securityContext); ``` +## Objects +You can create custom objects by calling: +```dart +var dietPlan = ParseObject('DietPlan') + ..set('Name', 'Ketogenic') + ..set('Fat', 65); +``` +You then have the ability to do the following with that object: +The features available are:- + * Get + * GetAll + * Create + * Save + * Query - By object Id + * Delete + * Complex queries as shown above + * Pin + * Plenty more + * Counters + * Array Operators + +## Custom Objects +You can create your own ParseObjects or convert your existing objects into Parse Objects by doing the following: + +```dart +class DietPlan extends ParseObject implements ParseCloneable { + + DietPlan() : super(_keyTableName); + DietPlan.clone(): this(); + + /// Looks strangely hacky but due to Flutter not using reflection, we have to + /// mimic a clone + @override clone(Map map) => DietPlan.clone()..fromJson(map); + + static const String _keyTableName = 'Diet_Plans'; + static const String keyName = 'Name'; + + String get name => get(keyName); + set name(String name) => set(keyName, name); +} + +``` + +## Add new values to objects +To add a variable to an object call and retrieve it, call + +```dart +dietPlan.set('RandomInt', 8); +var randomInt = dietPlan.get('RandomInt'); +``` + +## Save objects using pins +You can now save an object by calling .pin() on an instance of an object + +```dart +dietPlan.pin(); +``` + +and to retrieve it + +```dart +var dietPlan = DietPlan().fromPin('OBJECT ID OF OBJECT'); +``` + +## Increment Counter values in objects +Retrieve it, call + +```dart +var response = await dietPlan.increment("count", 1); + +``` + +## Array Operator in objects +Retrieve it, call + +```dart +var response = await dietPlan.add("listKeywords", ["a", "a","d"]); + +var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]); + +var response = await dietPlan.remove("listKeywords", ["a"]); + +``` + ## Queries Once you have setup the project and initialised the instance, you can then retreive data from your server by calling: ```dart @@ -63,7 +147,6 @@ var dietPlan = await DietPlan().getObject('R5EonpUDWy'); } ``` - ## Complex queries You can create complex queries to really put your database to the test: @@ -155,98 +238,136 @@ If you only care about the number of games played by a particular player: } ``` -## Objects +## Live Queries +This tool allows you to subscribe to a QueryBuilder you are interested in. Once subscribed, the server will notify clients +whenever a ParseObject that matches the QueryBuilder is created or updated, in real-time. -You can create custom objects by calling: -```dart -var dietPlan = ParseObject('DietPlan') - ..set('Name', 'Ketogenic') - ..set('Fat', 65); -``` -You then have the ability to do the following with that object: -The features available are:- - * Get - * GetAll - * Create - * Save - * Query - By object Id - * Delete - * Complex queries as shown above - * Pin - * Plenty more - * Counters - * Array Operators +Parse LiveQuery contains two parts, the LiveQuery server and the LiveQuery clients. In order to use live queries, you need +to set up both of them. -## Custom Objects -You can create your own ParseObjects or convert your existing objects into Parse Objects by doing the following: +The Parse Server configuration guide on the server is found here https://docs.parseplatform.org/parse-server/guide/#live-queries and is not part of this documentation. +Initialize the Parse Live Query by entering the parameter liveQueryUrl in Parse().initialize: ```dart -class DietPlan extends ParseObject implements ParseCloneable { - - DietPlan() : super(_keyTableName); - DietPlan.clone(): this(); - - /// Looks strangely hacky but due to Flutter not using reflection, we have to - /// mimic a clone - @override clone(Map map) => DietPlan.clone()..fromJson(map); - - static const String _keyTableName = 'Diet_Plans'; - static const String keyName = 'Name'; - - String get name => get(keyName); - set name(String name) => set(keyName, name); -} - + Parse().initialize( + ApplicationConstants.keyApplicationId, + ApplicationConstants.keyParseServerUrl, + clientKey: ApplicationConstants.keyParseClientKey, + debug: true, + liveQueryUrl: ApplicationConstants.keyLiveQueryUrl, + autoSendSessionId: true); ``` -## Add new values to objects - -To add a variable to an object call and retrieve it, call - +Declare LiveQuery: ```dart -dietPlan.set('RandomInt', 8); -var randomInt = dietPlan.get('RandomInt'); + final LiveQuery liveQuery = LiveQuery(); ``` -## Save objects using pins - -You can now save an object by calling .pin() on an instance of an object - +Set the QueryBuilder that will be monitored by LiveQuery: ```dart -dietPlan.pin(); + QueryBuilder query = + QueryBuilder(ParseObject('TestAPI')) + ..whereEqualTo('intNumber', 1); ``` - -and to retrieve it +__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 -var dietPlan = DietPlan().fromPin('OBJECT ID OF OBJECT'); + await liveQuery.subscribe(query); ``` -## Increment Counter values in objects - -Retrieve it, call +__Event Handling__ +We define several types of events you’ll get through a subscription object: +__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 -var response = await dietPlan.increment("count", 1); - + 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')); + }); ``` -## Array Operator in objects +__Update event__ +When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the +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')); + }); +``` -Retrieve it, call +__Enter event__ +When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder, +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')); + }); +``` +__Leave event__ +When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder, +you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. +Its content is the latest value of the ParseObject. ```dart -var response = await dietPlan.add("listKeywords", ["a", "a","d"]); + 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')); + }); +``` -var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]); +__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')); + }); +``` -var response = await dietPlan.remove("listKeywords", ["a"]); +__Unsubscribe__ +If you would like to stop receiving events from a QueryBuilder, you can just unsubscribe the subscription. +After that, you won’t get any events from the subscription object and will close the WebSocket connection to the +LiveQuery server. +```dart + await liveQuery.unSubscribe(); ``` - ## Users - You can create and control users just as normal using this SDK. To register a user, first create one : @@ -277,7 +398,6 @@ Other user features are:- * Queries ## Config - The SDK now supports Parse Config. A map of all configs can be grabbed from the server by calling : ```dart var response = await ParseConfig().getConfigs(); @@ -289,13 +409,8 @@ ParseConfig().addConfig('TestConfig', 'testing'); ``` ## Other Features of this library - Main: -* Users * Installation -* Objects -* Queries -* LiveQueries * GeoPoints * Files * Persistent storage @@ -313,11 +428,13 @@ User: * Save * Destroy * Queries +* Anonymous +* 3rd Party Authentication Objects: * Create new object * Extend Parse Object and create local objects that can be saved and retreived -* Queries: +* Queries ## Author:- This project was authored by Phill Wiggins. You can contact me at phill.wiggins@gmail.com diff --git a/lib/src/enums/parse_enum_api_rq.dart b/lib/src/enums/parse_enum_api_rq.dart index ea0b9b15d..73eafd895 100644 --- a/lib/src/enums/parse_enum_api_rq.dart +++ b/lib/src/enums/parse_enum_api_rq.dart @@ -30,5 +30,6 @@ enum ParseApiRQ { increment, decrement, getConfigs, - addConfig + addConfig, + liveQuery } diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index d04988682..fde65d862 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -1,52 +1,181 @@ part of flutter_parse_sdk; -/// Still under development +enum LiveQueryEvent { create, enter, update, leave, delete, error } + class LiveQuery { + LiveQuery({bool debug, ParseHTTPClient client, bool autoSendSessionId}) { + _client = client ?? + ParseHTTPClient( + sendSessionId: + autoSendSessionId ?? ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); - LiveQuery(ParseHTTPClient client) : client = client { - connectMessage = { - 'op': 'connect', - 'applicationId': client.data.applicationId, - }; + _debug = isDebugEnabled(objectLevelDebug: debug); + _sendSessionId = autoSendSessionId ?? ParseCoreData().autoSendSessionId; + } - final Map whereMap = Map(); + WebSocket _webSocket; + ParseHTTPClient _client; + bool _debug; + bool _sendSessionId; + IOWebSocketChannel _channel; + Map _connectMessage; + Map _subscribeMessage; + Map _unsubscribeMessage; + Map eventCallbacks = {}; + int _requestIdCount = 1; + final List _liveQueryEvent = [ + 'create', + 'enter', + 'update', + 'leave', + 'delete', + 'error' + ]; + final String _printConstLiveQuery = 'LiveQuery: '; - subscribeMessage = { - 'op': 'subscribe', - 'requestId': 1, - 'query': { - 'className': null, - 'where': whereMap, - } - }; + int _requestIdGenerator() { + return _requestIdCount++; } - final ParseHTTPClient client; - IOWebSocketChannel channel; - Map connectMessage; - Map subscribeMessage; - Map eventCallbacks = {}; + Future subscribe(QueryBuilder query) async { + String _liveQueryURL = _client.data.liveQueryURL; + if (_liveQueryURL.contains('https')) { + _liveQueryURL = _liveQueryURL.replaceAll('https', 'wss'); + } else if (_liveQueryURL.contains('http')) { + _liveQueryURL = _liveQueryURL.replaceAll('http', 'ws'); + } + + final String _className = query.object.className; + query.limiters.clear(); //Remove limites in LiveQuery + final String _where = query._buildQuery().replaceAll('where=', ''); + //Convert where condition to Map + Map _whereMap = Map(); + if (_where != '') { + _whereMap = json.decode(_where); + } + + final int requestId = _requestIdGenerator(); + + try { + _webSocket = await WebSocket.connect(_liveQueryURL); + + if (_webSocket != null && _webSocket.readyState == WebSocket.open) { + if (_debug) { + print('$_printConstLiveQuery: Socket opened'); + } + } else { + if (_debug) { + print('$_printConstLiveQuery: Error when connection client'); + return Future.value(null); + } + } + + _channel = IOWebSocketChannel(_webSocket); + _channel.stream.listen((dynamic message) { + if (_debug) { + print('$_printConstLiveQuery: Listen: ${message}'); + } + + final Map actionData = jsonDecode(message); + + if (eventCallbacks.containsKey(actionData['op'])) { + if (actionData.containsKey('object')) { + final Map map = actionData['object']; + final String className = map['className']; + if (className == '_User') { + eventCallbacks[actionData['op']]( + ParseUser._getEmptyUser().fromJson(map)); + } else { + eventCallbacks[actionData['op']]( + ParseObject(className).fromJson(map)); + } + } else { + eventCallbacks[actionData['op']](actionData); + } + } + }, onDone: () { + if (_debug) { + print('$_printConstLiveQuery: Done'); + } + }, onError: (error, StackTrace stackTrace) { + if (_debug) { + print( + '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); + } + return Future.value( + handleException(error, ParseApiRQ.liveQuery, _debug, _className)); + }); - Future subscribe(String className) async { - final WebSocket webSocket = await WebSocket.connect(client.data.liveQueryURL); - channel = IOWebSocketChannel(webSocket); - channel.sink.add(jsonEncode(connectMessage)); - final Map classNameMap = subscribeMessage['query']; - classNameMap['className'] = className; - channel.sink.add(jsonEncode(subscribeMessage)); - - channel.stream.listen((dynamic message) { - final Map actionData = jsonDecode(message); - if (eventCallbacks.containsKey(actionData['op'])) - eventCallbacks[actionData['op']](actionData); - }); + //The connect message is sent from a client to the LiveQuery server. + //It should be the first message sent from a client after the WebSocket connection is established. + _connectMessage = { + 'op': 'connect', + 'applicationId': _client.data.applicationId, + 'clientKey': _client.data.clientKey ?? '' + }; + if (_sendSessionId) { + _connectMessage['sessionToken'] = _client.data.sessionId; + } + + if (_debug) { + print('$_printConstLiveQuery: ConnectMessage: $_connectMessage'); + } + _channel.sink.add(jsonEncode(_connectMessage)); + + //After a client connects to the LiveQuery server, + //it can send a subscribe message to subscribe a ParseQuery. + _subscribeMessage = { + 'op': 'subscribe', + 'requestId': requestId, + 'query': { + 'className': _className, + 'where': _whereMap, + } + }; + if (_sendSessionId) { + _subscribeMessage['sessionToken'] = _client.data.sessionId; + } + + if (_debug) { + print('$_printConstLiveQuery: SubscribeMessage: $_subscribeMessage'); + } + + _channel.sink.add(jsonEncode(_subscribeMessage)); + + //Mount message for Unsubscribe + _unsubscribeMessage = { + 'op': 'unsubscribe', + 'requestId': requestId, + }; + } on Exception catch (e) { + if (_debug) { + print('$_printConstLiveQuery: Error: ${e.toString()}'); + } + return handleException(e, ParseApiRQ.liveQuery, _debug, _className); + } } - void on(String op, Function callback) { - eventCallbacks[op] = callback; + void on(LiveQueryEvent op, Function callback) { + eventCallbacks[_liveQueryEvent[op.index]] = callback; } - Future close() async { - await channel.sink.close(); + Future unSubscribe() async { + if (_channel != null) { + if (_channel.sink != null) { + if (_debug) { + print( + '$_printConstLiveQuery: UnsubscribeMessage: $_unsubscribeMessage'); + } + await _channel.sink.add(jsonEncode(_unsubscribeMessage)); + await _channel.sink.close(); + } + } + if (_webSocket != null && _webSocket.readyState == WebSocket.open) { + if (_debug) { + print('$_printConstLiveQuery: Socket closed'); + } + await _webSocket.close(); + } } } From 6f9e2fc2435dfaebf5c792377334c3b704cbf649 Mon Sep 17 00:00:00 2001 From: Rodrigo de Souza Marques Date: Sun, 14 Apr 2019 04:48:55 -0300 Subject: [PATCH 21/25] Bugfix parseEncode function with ParseObjects in List (#151) * Add Support to Relational Queries Add Support to Relational Queries * Add Support to Counting Objects Add Support to Counting Objects https://docs.parseplatform.org/rest/guide/#counting-objects * Update README.md Update documentation with example for Relational Queries and Counting Objects * Update README.md * Update README.md * BugFix LiveQuery BugFix LiveQuery * Bugfix LiveQuery Bugfix LiveQuery * Update README.md Reorganization of sessions and inclusion of documentation on LiveQuery * Update README.md * Update README.md * Bugfix LiveQuery Bugfix LiveQuery * Changed to make optional clientKey Changed to make optional clientKey * Bugfix parseEncode function with ParseObjects in List Bugfix parseEncode function with ParseObjects in List --- lib/src/utils/parse_encoder.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/parse_encoder.dart b/lib/src/utils/parse_encoder.dart index c64d439a4..f2929c376 100644 --- a/lib/src/utils/parse_encoder.dart +++ b/lib/src/utils/parse_encoder.dart @@ -20,6 +20,12 @@ dynamic parseEncode(dynamic value, {bool full}) { return _encodeDate(value); } + if (value is List) { + return value.map((dynamic value) { + return parseEncode(value); + }).toList(); + } + if (value is ParseGeoPoint) { return value; } @@ -40,10 +46,7 @@ dynamic parseEncode(dynamic value, {bool full}) { } Map _encodeUint8List(Uint8List value) { - return { - '__type': 'Bytes', - 'base64': base64.encode(value) - }; + return {'__type': 'Bytes', 'base64': base64.encode(value)}; } Map _encodeDate(DateTime date) { From 99ac1ad3eb35be4a285dfa539c3d10dff3f0f38a Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 14 Apr 2019 08:51:15 +0100 Subject: [PATCH 22/25] 1.0.17 - Cody tidy --- lib/src/network/parse_live_query.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index fde65d862..8ce3f9ccf 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -98,18 +98,18 @@ class LiveQuery { if (_debug) { print('$_printConstLiveQuery: Done'); } - }, onError: (error, StackTrace stackTrace) { + }, onError: (Error error, StackTrace stackTrace) { if (_debug) { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); } return Future.value( - handleException(error, ParseApiRQ.liveQuery, _debug, _className)); + handleException(Exception(error), ParseApiRQ.liveQuery, _debug, _className)); }); //The connect message is sent from a client to the LiveQuery server. //It should be the first message sent from a client after the WebSocket connection is established. - _connectMessage = { + _connectMessage = { 'op': 'connect', 'applicationId': _client.data.applicationId, 'clientKey': _client.data.clientKey ?? '' @@ -125,7 +125,7 @@ class LiveQuery { //After a client connects to the LiveQuery server, //it can send a subscribe message to subscribe a ParseQuery. - _subscribeMessage = { + _subscribeMessage = { 'op': 'subscribe', 'requestId': requestId, 'query': { @@ -144,7 +144,7 @@ class LiveQuery { _channel.sink.add(jsonEncode(_subscribeMessage)); //Mount message for Unsubscribe - _unsubscribeMessage = { + _unsubscribeMessage = { 'op': 'unsubscribe', 'requestId': requestId, }; From 20f4ed4cadaa825e02642c9df55a5d8aa18f9f18 Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 14 Apr 2019 08:52:33 +0100 Subject: [PATCH 23/25] ParseResponse accepts list results and count --- lib/src/network/parse_live_query.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 8ce3f9ccf..3663b0a6f 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -103,8 +103,8 @@ class LiveQuery { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); } - return Future.value( - handleException(Exception(error), ParseApiRQ.liveQuery, _debug, _className)); + return Future.value(handleException( + Exception(error), ParseApiRQ.liveQuery, _debug, _className)); }); //The connect message is sent from a client to the LiveQuery server. From 55aeae211c1179e0e002d5f6efb90cd1ab069776 Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 14 Apr 2019 09:00:33 +0100 Subject: [PATCH 24/25] Release 1.0.18 created --- CHANGELOG.md | 5 ++++- README.md | 2 +- lib/src/base/parse_constants.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c7ff7bc..b51a698db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ -## 1.0.17 +## 1.0.18 +## 1.0.17 +LiveQuery fix +Bug fixes ## 1.0.16 Bug fixes diff --git a/README.md b/README.md index 2320c7f04..e270ab562 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.17 + parse_server_sdk: ^1.0.18 ``` 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 b746b5569..530ab6dd6 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.17'; +const String keySdkVersion = '1.0.18'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points diff --git a/pubspec.yaml b/pubspec.yaml index af7d53b12..c084f0a07 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.16 +version: 1.0.18 homepage: https://github.com/phillwiggins/flutter_parse_sdk author: PhillWiggins From 9ef1217f0a81ac2129914e0a90519eb2d5af8d03 Mon Sep 17 00:00:00 2001 From: Phill Date: Sun, 14 Apr 2019 14:37:25 +0100 Subject: [PATCH 25/25] save fix --- lib/src/objects/parse_object.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/objects/parse_object.dart b/lib/src/objects/parse_object.dart index 80f3ac00f..74d0b1936 100644 --- a/lib/src/objects/parse_object.dart +++ b/lib/src/objects/parse_object.dart @@ -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(forApiRQ: true)); + final String body = json.encode(toJson()); final Response result = await _client.put(url, body: body); return handleResponse( this, result, ParseApiRQ.save, _debug, className);