From bec49c8fa2b8ed6c13b84297804d9edfde251b15 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Sun, 12 Nov 2023 10:03:13 +0200 Subject: [PATCH] add api_client including GraphQL query and mutation functionality including SSE streaming --- lib/main.dart | 35 ++++++++- lib/models/enums/search_event_type.dart | 6 ++ lib/models/server_sent_event.dart | 9 +++ lib/network/api_client.dart | 98 +++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 lib/models/enums/search_event_type.dart create mode 100644 lib/models/server_sent_event.dart create mode 100644 lib/network/api_client.dart diff --git a/lib/main.dart b/lib/main.dart index dda5554..1deb065 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:graphql/client.dart'; +import 'package:shopping_assistant_mobile_client/network/api_client.dart'; void main() { runApp(const MyApp()); @@ -57,7 +60,8 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { int _counter = 0; - void _incrementCounter() { + var client = ApiClient(); + Future _incrementCounter() async { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below @@ -66,6 +70,35 @@ class _MyHomePageState extends State { // called again, and so nothing would appear to happen. _counter++; }); + + const String startPersonalWishlistMutations = r''' + mutation startPersonalWishlist($dto: WishlistCreateDtoInput!) { + startPersonalWishlist(dto: $dto) { + createdById, id, name, type + } + } + '''; + + MutationOptions mutationOptions = MutationOptions( + document: gql(startPersonalWishlistMutations), + variables: const { + 'dto': { + 'firstMessageText': 'Gaming mechanical keyboard', + 'type': 'Product' + }, + } + ); + + var result = await client.mutate(mutationOptions); + print(jsonEncode(result)); + + var wishlistId = result?['startPersonalWishlist']['id']; + var sseStream = client.getServerSentEventStream( + 'api/productssearch/search/$wishlistId', + {'text': 'silent wireless mouse'}); + await for (var chunk in sseStream) { + print('${chunk.event}: ${chunk.data}'); + } } @override diff --git a/lib/models/enums/search_event_type.dart b/lib/models/enums/search_event_type.dart new file mode 100644 index 0000000..3634744 --- /dev/null +++ b/lib/models/enums/search_event_type.dart @@ -0,0 +1,6 @@ +enum SearchEventType { + wishlist, + message, + suggestion, + product +} diff --git a/lib/models/server_sent_event.dart b/lib/models/server_sent_event.dart new file mode 100644 index 0000000..48c35ea --- /dev/null +++ b/lib/models/server_sent_event.dart @@ -0,0 +1,9 @@ +import 'package:shopping_assistant_mobile_client/models/enums/search_event_type.dart'; + +class ServerSentEvent { + + SearchEventType event; + String data; + + ServerSentEvent(this.event, this.data); +} diff --git a/lib/network/api_client.dart b/lib/network/api_client.dart new file mode 100644 index 0000000..87c0633 --- /dev/null +++ b/lib/network/api_client.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:graphql/client.dart'; +import 'package:http/http.dart' as http; +import 'package:shopping_assistant_mobile_client/models/enums/search_event_type.dart'; +import 'package:shopping_assistant_mobile_client/models/global_instances/global_user.dart'; +import 'package:shopping_assistant_mobile_client/models/server_sent_event.dart'; +import 'package:shopping_assistant_mobile_client/network/authentication_service.dart'; + +class ApiClient { + final String _apiBaseUrl = 'https://shopping-assistant-api-dev.azurewebsites.net/'; + late String _accessToken; + + final AuthenticationService _authenticationService = AuthenticationService(); + + late GraphQLClient _graphqlClient; + final http.Client _httpClient = http.Client(); + + Future?> query(QueryOptions options) async { + await _setAuthentication(); + + final QueryResult result = await _graphqlClient.query(options); + + if (result.hasException) { + print(result.exception.toString()); + } + + return result.data; + } + + Future?> mutate(MutationOptions options) async { + await _setAuthentication(); + + final QueryResult result = await _graphqlClient.mutate(options); + + if (result.hasException) { + print(result.exception.toString()); + } + + return result.data; + } + + Stream getServerSentEventStream(String urlPath, Map requestBody) async* { + await _setAuthentication(); + + final url = Uri.parse('$_apiBaseUrl$urlPath'); + final request = http.Request('POST', url); + request.body = jsonEncode(requestBody); + request.headers.addAll({ + HttpHeaders.authorizationHeader: 'Bearer $_accessToken', + HttpHeaders.contentTypeHeader: 'application/json' + }); + final response = await _httpClient.send(request); + + var eventType = SearchEventType.message; + await for (var line in response.stream.transform(utf8.decoder).transform(const LineSplitter())) { + if (line.startsWith('event: ')) { + var type = line.substring('event: '.length); + switch (type) { + case 'Message': + eventType = SearchEventType.message; + break; + case 'Suggestion': + eventType = SearchEventType.suggestion; + break; + case 'Product': + eventType = SearchEventType.product; + break; + case 'Wishlist': + eventType = SearchEventType.wishlist; + break; + } + } + if (line.startsWith('data: ')) { + yield ServerSentEvent(eventType, line.substring('data: '.length)); + } + } + } + + Future _setAuthentication() async { + _accessToken = await _authenticationService.getAccessToken(); + + GlobalUser.id = _authenticationService.getIdFromAccessToken(_accessToken); + GlobalUser.email = _authenticationService.getEmailFromAccessToken(_accessToken); + GlobalUser.phone = _authenticationService.getPhoneFromAccessToken(_accessToken); + // GlobalUser.roles = _authenticationService.getRolesFromAccessToken(_accessToken); + + final httpLink = HttpLink('${_apiBaseUrl}graphql/', defaultHeaders: { + HttpHeaders.authorizationHeader: 'Bearer $_accessToken' + }); + + _graphqlClient = GraphQLClient( + cache: GraphQLCache(), + link: httpLink + ); + } +}