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(); + } } }